diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6a836578..b2942b2e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -29,9 +29,9 @@ jobs: node-version: [ 18.x, 20.x ] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: - version: "9.x" + version: 9 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} @@ -62,9 +62,9 @@ jobs: id-token: write steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: - version: "9.x" + version: 9 - uses: actions/setup-node@v2 with: node-version: 20.x diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bd018894..8cc3647b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: - version: "9.x" + version: 9 - uses: actions/setup-node@v3 with: node-version-file: '.node-version' diff --git a/README.md b/README.md index 12fe5727..e56fd4b7 100644 --- a/README.md +++ b/README.md @@ -327,7 +327,7 @@ Before initializing [Part One](#part-one) of the authorization code flow, the cl We can do this in Node using the native crypto package and a `base64urlencode` function: ```typescript -import crypto from "node:crypto"; +import crypto from "crypto"; const code_verifier = crypto.randomBytes(43).toString("hex"); ``` diff --git a/docs/.idea/docs.iml b/docs/.idea/docs.iml index ddbae112..a58c271d 100644 --- a/docs/.idea/docs.iml +++ b/docs/.idea/docs.iml @@ -6,6 +6,7 @@ + diff --git a/docs/docs/adapters/express.md b/docs/docs/adapters/express.md index 8798318c..3a9e6eea 100644 --- a/docs/docs/adapters/express.md +++ b/docs/docs/adapters/express.md @@ -1,31 +1,47 @@ # Express -[Express](https://expressjs.com/) +:::info -Adapts the [Express.Request](https://expressjs.com/en/api.html#req) and [Express.Response](https://expressjs.com/en/api.html#res) for use with `@jmondi/oauth2-server`. +Available in >2.0.0 -```typescript -import { - requestFromExpress, - handleExpressResponse, - handleExpressError, -} from "@jmondi/oauth2-server/express"; -``` +::: -```typescript -requestFromExpress(req: Express.Request): OAuthRequest; -``` +This adapter provides utility functions to convert between Express [Request](https://expressjs.com/en/api.html#req) and [Response](https://expressjs.com/en/api.html#res) objects and the `OAuthRequest`/`OAuthResponse` objects used by this package. -Helper function to return an OAuthRequest from an `Express.Request`. +## Functions -```typescript -handleExpressResponse(expressResponse: Express.Response, oauthResponse: OAuthResponse): void; +```ts +requestFromExpress(req: Express.Request): OAuthRequest ``` -Helper function that handles the express response after authorization. +```ts +handleExpressResponse(expressResponse: Express.Response, oauthResponse: OAuthResponse): void +``` -```typescript -handleExpressError(res: Express.Response, e: unknown | OAuthException): void; +```ts +handleExpressError(res: Express.Response, e: unknown | OAuthException): void ``` -Helper function that handles the express response if an error was thrown. +## Example + +```ts +import { requestFromExpress, handleExpressResponse, handleExpressError } from "@jmondi/oauth2-server/express"; +import express from 'express'; + +const app = express(); + +// ... + +app.post('/oauth2/token', async (req: express.Request, res: express.Response) => { + const authorizationServer = req.app.get('authorization_server'); + + try { + const oauthResponse = await authorizationServer + .respondToAccessTokenRequest(requestFromExpress(req)); + + handleExpressResponse(res, oauthResponse); + } catch (e) { + handleExpressError(res, e); + } +}); +``` diff --git a/docs/docs/adapters/fastify.md b/docs/docs/adapters/fastify.md index bdeba981..de4c34ba 100644 --- a/docs/docs/adapters/fastify.md +++ b/docs/docs/adapters/fastify.md @@ -1,31 +1,48 @@ # Fastify -Adapts the [Fastify.Request](https://fastify.dev/docs/latest/Reference/Request/) and [Fastify.Reply](https://fastify.dev/docs/latest/Reference/Reply/) for use with `@jmondi/oauth2-server`. - -```typescript -import { - requestFromFastify, - handleFastifyReply, - handleFastifyError, -} from "@jmondi/oauth2-server/fastify"; -``` +:::info -The following functions are imported directly from the adapter instead of the root package. +Available in >2.0.0 + +::: -```typescript -requestFromFastify(req: FastifyRequest): OAuthRequest; -``` -Helper function to return an OAuthRequest from an `FastifyRequest`. +This adapter provides utility functions to convert between Fastify [Request](https://fastify.dev/docs/latest/Reference/Request/) and [Reply](https://fastify.dev/docs/latest/Reference/Reply/) objects and the `OAuthRequest`/`OAuthResponse` objects used by this package. -```typescript -handleFastifyReply(fastifyReply: FasitfyReply, oauthResponse: OAuthResponse): void; +## Functions + +```ts +requestFromFastify(req: FastifyRequest): OAuthRequest ``` -Helper function that handles the express response after authorization. +```ts +handleFastifyReply(fastifyReply: FastifyReply, oauthResponse: OAuthResponse): void +``` -```typescript -handleFastifyError(reply: FasitfyReply, e: unknown | OAuthException): void; +```ts +handleFastifyError(reply: FastifyReply, e: unknown | OAuthException): void ``` -Helper function that handles the express response if an error was thrown. +## Example + +```ts +import { requestFromFastify, handleFastifyReply, handleFastifyError } from "@jmondi/oauth2-server/fastify"; +import fastify from 'fastify' + +const app = fastify() + +// ... + +app.post('/oauth2/token', async (request: fastify.Request, reply: fastify.Reply) => { + const authorizationServer = request.server.authorizationServer; + + try { + const oauthResponse = await authorizationServer + .respondToAccessTokenRequest(requestFromFastify(request)); + + handleFastifyReply(reply, oauthResponse); + } catch (e) { + handleFastifyError(reply, e); + } +}); +``` diff --git a/docs/docs/adapters/index.md b/docs/docs/adapters/index.md index 586b9be1..341d396f 100644 --- a/docs/docs/adapters/index.md +++ b/docs/docs/adapters/index.md @@ -9,4 +9,4 @@ Adapters are a set of helper functions to provide framework specific integration - [Express](./express) - If you're using Express, you can use the `@jmondi/oauth2-server/express` adapter. - [Fastify](./fastify) - If you're using Fastify, you can use the `@jmondi/oauth2-server/fastify` adapter. -- [VanillaJS](./vanilla) - If you're using Honojs, Sveltekit or Nextjs, you can use the `@jmondi/oauth2-server/vanilla` adapter. +- [VanillaJS](./vanilla) - Adapts the Fetch [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) so you can use Honojs, Sveltekit, Nextjs or whatever tool your using that uses the native [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) `@jmondi/oauth2-server/vanilla` adapter. diff --git a/docs/docs/adapters/vanilla.md b/docs/docs/adapters/vanilla.md index 4f6b9642..d4f83e16 100644 --- a/docs/docs/adapters/vanilla.md +++ b/docs/docs/adapters/vanilla.md @@ -10,32 +10,43 @@ Available in >3.4.0 ::: -Adapts the Fetch [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) for use with `@jmondi/oauth2-server`. - -```typescript -import { - requestFromVanilla, - handleVanillaReply, - handleVanillaError, -} from "@jmondi/oauth2-server/vanilla"; -``` +This adapter provides utility functions to convert between vanilla JavaScript [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects and the `OAuthRequest`/`OAuthResponse` objects used by the this package. -The following functions are imported directly from the adapter instead of the root package. +## Functions -```typescript -requestFromVanilla(req: Request): OAuthRequest; +```ts +responseFromVanilla(res: Response): OAuthResponse ``` -Helper function to return an OAuthRequest from an `VanillaRequest`. +```ts +requestFromVanilla(req: Request): OAuthRequest +``` -```typescript -handleVanillaReply(res: Response, oauthResponse: OAuthResponse): void; +```ts +responseToVanilla(oauthResponse: OAuthResponse): Response ``` -Helper function that handles the express response after authorization. +## Example -```typescript -handleVanillaError(res: Response, e: unknown | OAuthException): void; -``` +```ts +import { requestFromVanilla, responseToVanilla } from "@jmondi/oauth2-server/vanilla"; + +import { Hono } from 'hono' +const app = new Hono() + +// ... -Helper function that handles the express response if an error was thrown. +app.post('/oauth2/token', async (c) => { + const authorizationServer = c.get("authorization_server"); + + const oauthResponse = await authorizationServer + .respondToAccessTokenRequest(requestFromVanilla(request)) + .catch(e => { + error(400, e.message); + }); + + return responseToVanilla(oauthResponse); +}); + +export default app +``` diff --git a/docs/docs/authorization_server/configuration.mdx b/docs/docs/authorization_server/configuration.mdx index d25de70c..2d407a36 100644 --- a/docs/docs/authorization_server/configuration.mdx +++ b/docs/docs/authorization_server/configuration.mdx @@ -28,7 +28,7 @@ type AuthorizationServerOptions = { To configure these options, pass the value in as the last argument: -```typescript +```ts const authorizationServer = new AuthorizationServer( clientRepository, accessTokenRepository, diff --git a/docs/docs/authorization_server/index.mdx b/docs/docs/authorization_server/index.mdx index c7ca91fb..cea105fb 100644 --- a/docs/docs/authorization_server/index.mdx +++ b/docs/docs/authorization_server/index.mdx @@ -10,7 +10,7 @@ The `AuthorizationServer` is a core component of the OAuth 2.0 framework, respon To create an instance of the `AuthorizationServer`, use the following constructor: -```typescript +```ts const authorizationServer = new AuthorizationServer( clientRepository, accessTokenRepository, @@ -32,7 +32,7 @@ Parameters: By default, no grant types are enabled when creating an `AuthorizationServer`. Each grant type must be explicitly enabled using the `enableGrantType` method. This approach allows for fine-grained control over which OAuth 2.0 flows your server supports. -```typescript +```ts authorizationServer.enableGrantType("client_credentials"); authorizationServer.enableGrantType("refresh_token"); authorizationServer.enableGrantType({ @@ -49,7 +49,7 @@ Note that the Authorization Code grant requires additional repositories: `userRe You can enable multiple grant types on the same server: -```typescript +```ts const authorizationServer = new AuthorizationServer( clientRepository, accessTokenRepository, diff --git a/docs/docs/getting_started/endpoints.mdx b/docs/docs/getting_started/endpoints.mdx index e2dbf5ba..f797f177 100644 --- a/docs/docs/getting_started/endpoints.mdx +++ b/docs/docs/getting_started/endpoints.mdx @@ -8,7 +8,7 @@ sidebar_position: 4 The `/token` endpoint is a back channel endpoint that issues a usable access token. -```typescript +```ts app.post("/token", async (req: Express.Request, res: Express.Response) => { try { const oauthResponse = await authorizationServer.respondToAccessTokenRequest(req); @@ -26,7 +26,7 @@ The `/authorize` endpoint is a front channel endpoint that issues an authorizati The endpoint should redirect the user to login, and then to accept the scopes requested by the application, and only when the user accepts, should it send the user back to the clients redirect uri. -```typescript +```ts import { requestFromExpress } from "@jmondi/oauth2-server/express"; app.get("/authorize", async (req: Express.Request, res: Express.Response) => { @@ -75,7 +75,7 @@ app.get("/authorize", async (req: Express.Request, res: Express.Response) => { ## The Revoke Endpoint -:::tip Note +:::info Note Implementing this endpoint is optional, but recommended. RFC7009 “OAuth 2.0 Token Revocation” @@ -83,7 +83,7 @@ Implementing this endpoint is optional, but recommended. RFC7009 “OAuth 2.0 To The `/token/revoke` endpoint is a back channel endpoint that revokes an existing token. -```typescript +```ts app.post("/token/revoke", async (req: Express.Request, res: Express.Response) => { try { const oauthResponse = await authorizationServer.revoke(req); diff --git a/docs/docs/getting_started/entities.md b/docs/docs/getting_started/entities.md index 3f90d9ae..acb72d55 100644 --- a/docs/docs/getting_started/entities.md +++ b/docs/docs/getting_started/entities.md @@ -16,7 +16,7 @@ The Client Entity represents an application that requests access to protected re ::: -```typescript +```ts interface OAuthClient { id: string; name: string; @@ -50,7 +50,7 @@ type CodeChallengeMethod = "S256" | "plain"; The Token Entity represents access and refresh tokens issued to clients. -```typescript +```ts interface OAuthToken { accessToken: string; accessTokenExpiresAt: Date; @@ -67,7 +67,7 @@ interface OAuthToken { The User Entity represents the resource owner - typically the end-user who authorizes an application to access their account. -```typescript +```ts interface OAuthUser { id: string; [key: string]: any; @@ -80,7 +80,7 @@ Scopes are used to define and limit the extent of access granted to a client app For more information on OAuth 2.0 scopes, visit: https://www.oauth.com/oauth2-servers/scope/ -```typescript +```ts interface OAuthScope { name: string; [key: string]: any; diff --git a/docs/docs/getting_started/index.mdx b/docs/docs/getting_started/index.mdx index 860dcd74..bbf2a0c0 100644 --- a/docs/docs/getting_started/index.mdx +++ b/docs/docs/getting_started/index.mdx @@ -29,15 +29,21 @@ This section provides a high-level overview of setting up the OAuth2 server. 1. Set up the [AuthorizationServer](#the-authorization-server) with desired grant types 1. Implement the [Endpoints](./endpoints) -## Installation +### Installation Choose your preferred package manager to install @jmondi/oauth2-server: -## Basic Setup +### Implement Entities -### The Authorization Server +You are going to need to setup the entities that the OAuth2 server uses to store data. See the [full list of entities](./entities.md). + +### Implement Repositories + +Next you need to implement the repositories that the OAuth2 server uses to interact with the entities. See the [full list of repositories](./repositories). + +### Setup the Authorization Server The AuthorizationServer is the core component of the OAuth2 implementation. It requires repositories for managing clients, access tokens, and scopes. Grant types are opt-in and must be explicitly enabled. diff --git a/docs/docs/getting_started/repositories.md b/docs/docs/getting_started/repositories.mdx similarity index 89% rename from docs/docs/getting_started/repositories.md rename to docs/docs/getting_started/repositories.mdx index b2d2aa96..c206c106 100644 --- a/docs/docs/getting_started/repositories.md +++ b/docs/docs/getting_started/repositories.mdx @@ -2,13 +2,17 @@ sidebar_position: 3 --- +import RequiredForGrants from "@site/src/components/grants/RequiredForGrants"; + # Repository Interfaces ## Auth Code Repository OAuthAuthCodeRepository interface is utilized for managing OAuth authorization codes. It contains methods for retrieving an authorization code entity by its identifier, issuing a new authorization code, persisting an authorization code in the storage, checking if an authorization code has been revoked, and revoking an authorization code. -```typescript + + +```ts interface OAuthAuthCodeRepository { // Fetch auth code entity from storage by code getByIdentifier(authCodeCode: string): Promise; @@ -37,7 +41,9 @@ interface OAuthAuthCodeRepository { OAuthClientRepository interface is used for managing OAuth clients. It includes methods for fetching a client entity from storage by the client ID and for validating the client using the grant type and client secret. -```typescript + + +```ts interface OAuthClientRepository { // Fetch client entity from storage by client_id getByIdentifier(clientId: string): Promise; @@ -55,7 +61,9 @@ interface OAuthClientRepository { The OAuthScopeRepository interface handles scope management. It defines methods for finding all scopes by their names and for finalizing the scopes. In the finalization, additional scopes can be added or removed after they've been validated against the client scopes. -```typescript + + +```ts interface OAuthScopeRepository { // Find all scopes by scope names getAllByIdentifiers(scopeNames: string[]): Promise; @@ -77,7 +85,9 @@ interface OAuthScopeRepository { OAuthTokenRepository interface manages OAuth tokens. It contains methods for issuing a new token, persisting a token in the storage, issuing a refresh token, revoking tokens, and fetching a refresh token entity by the refresh token. -```typescript + + +```ts interface OAuthTokenRepository { // An async call that should return an OAuthToken that has not been // persisted to storage yet. @@ -114,7 +124,9 @@ interface OAuthTokenRepository { The OAuthUserRepository interface handles user management. It defines methods for fetching a user entity from storage by their credentials and optional grant type and client. This may involve validating the user's credentials. -```typescript + + +```ts interface OAuthUserRepository { // Fetch user entity from storage by identifier. A provided password may // be used to validate the users credentials. Grant type and client are provided diff --git a/docs/docs/grants/authorization_code.mdx b/docs/docs/grants/authorization_code.mdx index 8098d4c3..792360c0 100644 --- a/docs/docs/grants/authorization_code.mdx +++ b/docs/docs/grants/authorization_code.mdx @@ -6,6 +6,18 @@ sidebar_position: 2 A temporary code that the client will exchange for an access token. The user authorizes the application, they are redirected back to the application with a temporary code in the URL. The application exchanges that code for the access token. +:::note Enable this grant + +```ts +authorizationServer.enableGrantType({ + grant: "authorization_code", + userRepository, + authorizationCodeRepository, +}); +``` + +::: + ### Flow #### Part One @@ -19,7 +31,7 @@ The client redirects the user to the `/authorize` with the following query param - **code_challenge** must match the The code challenge as generated below, - **code_challenge_method** – Either `plain` or `S256`, depending on whether the challenge is the plain verifier string or the SHA256 hash of the string. If this parameter is omitted, the server will assume plain. -:::tip +:::info The client secret **should never** be used during the Part One of the authorization_code flow. ::: @@ -130,10 +142,10 @@ Before initializing [Part One](#part-one) of the authorization code flow, the cl We can do this in Node using the native crypto package and a `base64urlencode` function: -```typescript -import crypto from "node:crypto"; +```ts +import { randomBytes } from "crypto"; -const code_verifier = crypto.randomBytes(43).toString("hex"); +const code_verifier = randomBytes(43).toString("hex"); ``` @see [https://www.oauth.com/oauth2-servers/pkce/authorization-request/](https://www.oauth.com/oauth2-servers/pkce/authorization-request/) @@ -144,19 +156,19 @@ Now we need to create a `code_challenge` from our `code_verifier`. For devices that can perform a SHA256 hash, the code challenge is a BASE64-URL-encoded string of the SHA256 hash of the code verifier. -```typescript +```ts const code_challenge = base64urlencode(crypto.createHash("sha256").update(code_verifier).digest()); ``` Clients that do not have the ability to perform a SHA256 hash are permitted to use the plain `code_verifier` string as the `code_challenge`. -```typescript +```ts const code_challenge = code_verifier; ```
Need a base64urlencode function? - ```typescript + ```ts function base64urlencode(str: string) { return Buffer.from(str) .toString("base64") diff --git a/docs/docs/grants/client_credentials.mdx b/docs/docs/grants/client_credentials.mdx index f414f773..e22a68e8 100644 --- a/docs/docs/grants/client_credentials.mdx +++ b/docs/docs/grants/client_credentials.mdx @@ -9,6 +9,18 @@ import TabItem from "@theme/TabItem"; When applications request an access token to access their own resources, not on behalf of a user. +:::note Enable this grant + +```ts +authorizationServer.enableGrantType("client_credentials"); +``` + +::: + +:::warning +The client_credentials grant should only be used by clients that can hold a secret. No Browser or Native Mobile Apps should be using this grant. +::: + ### Flow The client sends a **POST** to the `/token` endpoint with the following body: @@ -73,9 +85,3 @@ Pragma: no-cache } ```
- -## Something - -:::warning -The client_credentials grant should only be used by clients that can hold a secret. No Browser or Native Mobile Apps should be using this grant. -::: diff --git a/docs/docs/grants/custom_grant.mdx b/docs/docs/grants/custom_grant.mdx index fe8b9d88..f037b196 100644 --- a/docs/docs/grants/custom_grant.mdx +++ b/docs/docs/grants/custom_grant.mdx @@ -2,9 +2,28 @@ sidebar_position: 7 --- -# Custom Grants +# Custom Grant ⚠️ -To implement a custom grant, you need to extend the `AbstractGrant` class. This guide will walk you through the steps to create your own custom grant. +To implement a custom grant, you may extend the `AbstractGrant` class. + +:::warning + +This is advanced usage. Make sure you understand the OAuth2.0 specification before implementing a custom grant. + +::: + +:::note Enable this grant + +```ts +const customGrant = new MyCustomGrant(...); + +authorizationServer.enableGrantTypes( + ["client_credentials", new DateInterval("1d")], + [customGrant, new DateInterval("1d")], +); +``` + +::: ## Extending the CustomGrant class @@ -18,20 +37,3 @@ export class MyCustomGrant extends CustomGrant { } ``` -Use your custom grant - -```typescript -const authorizationServer = new AuthorizationServer( - clientRepository, - accessTokenRepository, - scopeRepository, - new JwtService("secret-key"), -); - -const customGrant = new MyCustomGrant(...); - -authorizationServer.enableGrantTypes( - ["client_credentials", new DateInterval("1d")], - [customGrant, new DateInterval("1d")], -); -``` diff --git a/docs/docs/grants/implicit.mdx b/docs/docs/grants/implicit.mdx index 44b45cb1..4ad4ac36 100644 --- a/docs/docs/grants/implicit.mdx +++ b/docs/docs/grants/implicit.mdx @@ -1,14 +1,20 @@ --- -sidebar_position: 5 +sidebar_position: 8 --- -# Implicit Grant +# Implicit Grant ⚠️ ⚠️ :::warning Not Recommended -Using the Implicit Grant is no longer best practice + +This server supports the Implicit Grant, but its use is strongly discouraged due to security concerns. The OAuth 2.0 Security Best Current Practice (RFC 8252) recommends against using the Implicit Grant flow. + +For native and single-page applications, the recommended approach is to use the Authorization Code Grant with PKCE (Proof Key for Code Exchange) extension. This method provides better security without requiring a client secret. + +If you're developing a web application with a backend, consider using the standard Authorization Code Grant with a client secret stored securely on your server. + ::: -This grant is supported, but not documented. Best practice recommends using the Authorization Code Grant without a client secret for native and browser-based apps. + Please look at these great resources: diff --git a/docs/docs/grants/password.mdx b/docs/docs/grants/password.mdx index 3228f329..1abcde02 100644 --- a/docs/docs/grants/password.mdx +++ b/docs/docs/grants/password.mdx @@ -9,8 +9,15 @@ import TabItem from "@theme/TabItem"; The Password Grant is for first party clients that are able to hold secrets (ie not Browser or Native Mobile Apps) -:::warning -The client_credentials grant should only be used by clients that can hold a secret +:::note Enable this grant + +```ts +authorizationServer.enableGrantType({ + grant: "password", + userRepository, +}); +``` + ::: ### Flow @@ -74,14 +81,13 @@ The authorization server will respond with the following response Cache-Control: no-store Pragma: no-cache -{ -token_type: 'Bearer', -expires_in: 3600, -access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1MTJhYjlhNC1jNzg2LTQ4YTYtOGFkNi05NGM1M2E4ZGM2NTEiLCJleHAiOjE2MDE3NjcyOTksIm5iZiI6MTYwMTc2MzY5OSwiaWF0IjoxNjAxNzYzNjk5LCJqdGkiOiJuZXcgdG9rZW4iLCJjaWQiOiJ0ZXN0IGNsaWVudCIsInNjb3BlIjoiIn0.sX6SWc2Af8jn-izFnrLgNIcNuZz_tRLl2p7M3CzQwKg', -refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIzNTYxNWYyZi0xM2ZhLTQ3MzEtODNhMS05ZTM0NTU2YWIzOTAiLCJhY2Nlc3NfdG9rZW5faWQiOiJuZXcgdG9rZW4iLCJyZWZyZXNoX3Rva2VuX2lkIjoidGhpcy1pcy1teS1zdXBlci1zZWNyZXQtcmVmcmVzaC10b2tlbiIsInNjb3BlIjoiIiwidXNlcl9pZCI6IjUxMmFiOWE0LWM3ODYtNDhhNi04YWQ2LTk0YzUzYThkYzY1MSIsImV4cGlyZV90aW1lIjoxNjAxNzY3Mjk5LCJpYXQiOjE2MDE3NjM2OTh9.SSa7miIdk3bxyzg0f3M9jKBXWjPgD4QEw-AU3SYvBk0', -scope: 'contacts.read contacts.write' -} + { + token_type: 'Bearer', + expires_in: 3600, + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1MTJhYjlhNC1jNzg2LTQ4YTYtOGFkNi05NGM1M2E4ZGM2NTEiLCJleHAiOjE2MDE3NjcyOTksIm5iZiI6MTYwMTc2MzY5OSwiaWF0IjoxNjAxNzYzNjk5LCJqdGkiOiJuZXcgdG9rZW4iLCJjaWQiOiJ0ZXN0IGNsaWVudCIsInNjb3BlIjoiIn0.sX6SWc2Af8jn-izFnrLgNIcNuZz_tRLl2p7M3CzQwKg', + refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIzNTYxNWYyZi0xM2ZhLTQ3MzEtODNhMS05ZTM0NTU2YWIzOTAiLCJhY2Nlc3NfdG9rZW5faWQiOiJuZXcgdG9rZW4iLCJyZWZyZXNoX3Rva2VuX2lkIjoidGhpcy1pcy1teS1zdXBlci1zZWNyZXQtcmVmcmVzaC10b2tlbiIsInNjb3BlIjoiIiwidXNlcl9pZCI6IjUxMmFiOWE0LWM3ODYtNDhhNi04YWQ2LTk0YzUzYThkYzY1MSIsImV4cGlyZV90aW1lIjoxNjAxNzY3Mjk5LCJpYXQiOjE2MDE3NjM2OTh9.SSa7miIdk3bxyzg0f3M9jKBXWjPgD4QEw-AU3SYvBk0', + scope: 'contacts.read contacts.write' + } -``` + ``` -``` diff --git a/docs/docs/grants/refresh_token.mdx b/docs/docs/grants/refresh_token.mdx index e15efec5..dc2b22c2 100644 --- a/docs/docs/grants/refresh_token.mdx +++ b/docs/docs/grants/refresh_token.mdx @@ -9,6 +9,14 @@ import TabItem from "@theme/TabItem"; Access tokens eventually expire. The refresh token grant enables the client to obtain a new access_token from an existing refresh_token. +:::note Enable this grant + +```ts +authorizationServer.enableGrantType("refresh_token"); +``` + +::: + ### Flow A complete refresh token request will include the following parameters: diff --git a/docs/docs/grants/token_exchange.mdx b/docs/docs/grants/token_exchange.mdx index 6305c188..fe687a07 100644 --- a/docs/docs/grants/token_exchange.mdx +++ b/docs/docs/grants/token_exchange.mdx @@ -6,7 +6,7 @@ sidebar_position: 6 The [RFC 8693 - OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) facilitates the secure exchange of tokens for accessing different resources or services. This documentation guides you through enabling this grant type on your authorization server, detailing request and response handling to ensure robust and secure token management. -### Enable Grant +:::note Enable this grant To enable the token exchange grant, you'll need to provide your own implementation of `processTokenExchangeFn`. This function should orchestrate the exchange with the required third-party services based on your specific needs. @@ -39,6 +39,8 @@ authorizationServer.enableGrant({ }); ``` +::: + ### Flow The client sends a **POST** to the `/token` endpoint with the following body: diff --git a/docs/docs/upgrade_guide.md b/docs/docs/upgrade_guide.md index d7554a54..db04311e 100644 --- a/docs/docs/upgrade_guide.md +++ b/docs/docs/upgrade_guide.md @@ -16,7 +16,7 @@ In v2.x, `AuthorizationServer` constructor required all repositories. In v3.x, i **Before (v2.x):** -```typescript +```ts const authorizationServer = new AuthorizationServer( authCodeRepository, clientRepository, @@ -33,7 +33,7 @@ const authorizationServer = new AuthorizationServer( **After (v3.x):** -```typescript +```ts const authorizationServer = new AuthorizationServer( clientRepository, accessTokenRepository, @@ -52,17 +52,17 @@ In v3, `enableGrantType` has been updated for the **"authorization_code"** and * #### Authorization Code Grant -`AuthorizationCodeGrant` now requires a [AuthorizationCodeRepository](./getting_started/repositories.md#authorization-code-repository) and a [UserRepository](./getting_started/repositories.md#user-repository). +`AuthorizationCodeGrant` now requires a [AuthorizationCodeRepository](./getting_started/repositories.mdx#authorization-code-repository) and a [UserRepository](./getting_started/repositories.mdx#user-repository). **Before (v2.x):** -```typescript +```ts authorizationServer.enableGrantType("authorization_code"); ``` **After (v3.x):** -```typescript +```ts authorizationServer.enableGrantType({ grant: "authorization_code", userRepository, @@ -72,17 +72,17 @@ authorizationServer.enableGrantType({ #### Password Grant -`PasswordGrant` now requires a [UserRepository](./getting_started/repositories.md#user-repository). +`PasswordGrant` now requires a [UserRepository](./getting_started/repositories.mdx#user-repository). **Before (v2.x):** -```typescript +```ts authorizationServer.enableGrantType("password"); ``` **After (v3.x):** -```typescript +```ts authorizationServer.enableGrantType({ grant: "password", userRepository, diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index ee21219b..12122785 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -8,7 +8,7 @@ const config: Config = { plugins: [tailwindPlugin], tagline: "Standards-Compliant OAuth 2.0 Server in TypeScript, Utilizing JWT and Proof Key for Code Exchange (PKCE)", - favicon: "img/favicon.ico", + favicon: "favicon.ico", url: "https://tsoauth2server.com", baseUrl: "/", onBrokenLinks: "throw", @@ -18,7 +18,7 @@ const config: Config = { locales: ["en"], }, scripts: [ - { src: "https://plausible.io/js/script.js", defer: true, "data-domain": "tsoauth2server.com" } + { src: "https://plausible.io/js/script.js", defer: true, "data-domain": "tsoauth2server.com" }, ], presets: [ [ @@ -56,29 +56,29 @@ const config: Config = { { sidebarId: "mainSidebar", type: "docSidebar", - label: "Getting Started", - position: "right", + label: "Docs", + position: "left", }, { href: "/docs/authorization_server/configuration/", label: "Config", - position: "right", + position: "left", }, { href: "https://github.com/jasonraimondi/ts-oauth2-server", label: "GitHub", position: "right", }, - { - href: "https://www.npmjs.com/package/@jmondi/oauth2-server", - label: "NPM", - position: "right", - }, - { - href: "https://jsr.io/@jmondi/oauth2-server", - label: "JSR", - position: "right", - }, + // { + // href: "https://www.npmjs.com/package/@jmondi/oauth2-server", + // label: "NPM", + // position: "right", + // }, + // { + // href: "https://jsr.io/@jmondi/oauth2-server", + // label: "JSR", + // position: "right", + // }, ], }, footer: { @@ -90,9 +90,9 @@ const config: Config = { darkTheme: prismThemes.dracula, }, algolia: { - appId: 'JP2YS2S0EQ', - apiKey: 'bf2bc45ac2821dba462ee887527c1816', - indexName: 'tsoauth2server', + appId: "JP2YS2S0EQ", + apiKey: "bf2bc45ac2821dba462ee887527c1816", + indexName: "tsoauth2server", }, } satisfies Preset.ThemeConfig, }; diff --git a/docs/package.json b/docs/package.json index dc784120..73b18be3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "docusaurus start", + "start": "docusaurus start --port 8000", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", diff --git a/docs/src/components/MarkdownWrapper.tsx b/docs/src/components/MarkdownWrapper.tsx index 282998f4..540d5478 100644 --- a/docs/src/components/MarkdownWrapper.tsx +++ b/docs/src/components/MarkdownWrapper.tsx @@ -1,15 +1,15 @@ -import React from "react"; +import { ReactNode } from "react"; interface MDXWrapperProps { - children: React.ReactNode; + children: ReactNode; } -const MDXWrapper: React.FC = ({ children }) => { +function MDXWrapper({ children }) { return ( -
+
{children}
); -}; +} export default MDXWrapper; diff --git a/docs/src/components/grants/RequiredForGrants.tsx b/docs/src/components/grants/RequiredForGrants.tsx new file mode 100644 index 00000000..09bfd19a --- /dev/null +++ b/docs/src/components/grants/RequiredForGrants.tsx @@ -0,0 +1,48 @@ +export default function RequiredForGrants(props) { + const enabledGrants = props.grants; + const allGrants = [ + { + label: "Authorization Code", + href: "authorization_code", + }, + { + label: "Client Credentials", + href: "client_credentials", + }, + { + label: "Refresh Token", + href: "refresh_token", + }, + { + label: "Password", + href: "password", + }, + { + label: "Implicit", + href: "implicit", + }, + { + label: "Custom", + href: "custom", + }, + ] as const; + + const grants = allGrants.filter(g => enabledGrants.includes(g.href)); + + return ( +
+ Used in Grants: + + {grants.map(s => ( + + {s.label} + + ))} + +
+ ); +} diff --git a/docs/src/pages/_logos.tsx b/docs/src/pages/_logos.tsx new file mode 100644 index 00000000..ef8633e6 --- /dev/null +++ b/docs/src/pages/_logos.tsx @@ -0,0 +1,76 @@ +export function GithubLogo() { + return ( + + + + ); +} + +export function FastifyLogo() { + return ( + + + + ); +} + +export function ExpressLogo() { + return ( + + + + ); +} + +export function TSLogo() { + return ( + + + + ); +} + +export function NPMLogo() { + return ( + + + + + + + ); +} + +export function JSRLogo() { + return ( + + + + ); +} + +export function JSLogo() { + return ( + + + + ); +} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index a69dda63..b5cff519 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -12,64 +12,14 @@ import MarkdownWrapper from "@site/src/components/MarkdownWrapper"; import { CheckCircleIcon, LinkIcon } from "lucide-react"; import { Contributors } from "@site/src/components/Contributors"; import { Sponsors } from "@site/src/components/Sponsors"; - -export function GithubLogo() { - return ( - - - - ); -} - -export function NPMLogo() { - return ( - - - - - - - ); -} - -export function JSRLogo() { - return ( - - - - ); -} +import { + ExpressLogo, + FastifyLogo, + GithubLogo, + JSRLogo, + NPMLogo, + TSLogo, +} from "@site/src/pages/_logos"; function HeroButton({ href, children }) { return ( @@ -86,11 +36,11 @@ function FeatureListItem({ to, title }) { return (
  • - - + + {title} @@ -150,17 +100,17 @@ function Features() { ]; return ( -
    -
    -

    Supported Grants

    +
    +
    + Supported Grants
      {grants.map(({ to, title }) => { return ; })}
    -
    -

    Implemented RFCs

    +
    + Implemented RFCs
      {rfcs.map(({ to, title }) => { return ; @@ -171,6 +121,50 @@ function Features() { ); } +export function SectionTitle({ children }) { + return

      {children}

      ; +} + +export function Adapters() { + const adapters = [ + { + name: "ExpressJS", + logo: , + href: "/docs/adapters/express", + }, + { + name: "FastifyJS", + logo: , + href: "/docs/adapters/fastify", + }, + { + name: "Vanilla JS/TS", + logo: , + href: "/docs/adapters/vanilla", + }, + ] as const; + + return ( +
      + Built in Adapters +
      + {adapters.map(({ name, logo, href }) => { + return ( + + {logo} + {name} + + ); + })} +
      +
      + ); +} + export default function Home() { const { siteConfig } = useDocusaurusContext(); return ( @@ -196,52 +190,35 @@ export default function Home() {
    - - - -
    -
    - - - - - - - - - +
    + Contributors +
    + +
    + Sponsors +
    +
    - + +
    -

    Install

    + Install
    -
    -

    - Entities and Repositories{" "} - +

    -
    + +
    @@ -250,37 +227,54 @@ export default function Home() {
    -

    - The Authorization Server{" "} - + + The Authorization Server + -

    +
    -
    -

    - Which Grant?{" "} - +

    +
    +
    -
    -

    Contributors

    -
    - -
    -

    Sponsors

    -
    - -
    +
    + Source + - +
    +
    ); } diff --git a/docs/static/favicon-16x16.png b/docs/static/favicon-16x16.png new file mode 100644 index 00000000..78a8124e Binary files /dev/null and b/docs/static/favicon-16x16.png differ diff --git a/docs/static/favicon-32x32.png b/docs/static/favicon-32x32.png new file mode 100644 index 00000000..8634f7be Binary files /dev/null and b/docs/static/favicon-32x32.png differ diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico new file mode 100644 index 00000000..e829d7ca Binary files /dev/null and b/docs/static/favicon.ico differ diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico deleted file mode 100644 index 4b63061e..00000000 Binary files a/docs/static/img/favicon.ico and /dev/null differ diff --git a/jsr.json b/jsr.json index ef519248..8aae9cf0 100644 --- a/jsr.json +++ b/jsr.json @@ -5,6 +5,7 @@ ".": "./src/index.ts", "./vanilla": "./src/adapters/vanilla.ts", "./express": "./src/adapters/express.ts", - "./fastify": "./src/adapters/fastify.ts" + "./fastify": "./src/adapters/fastify.ts", + "./vanilla": "./src/adapters/vanilla.ts" } } diff --git a/src/code_verifiers/S256.verifier.ts b/src/code_verifiers/S256.verifier.ts index 896e009d..e11ee4cc 100644 --- a/src/code_verifiers/S256.verifier.ts +++ b/src/code_verifiers/S256.verifier.ts @@ -1,4 +1,4 @@ -import { createHash } from "node:crypto"; +import { createHash } from "crypto"; import { base64urlencode } from "../utils/base64.js"; import { ICodeChallenge } from "./verifier.js"; diff --git a/src/utils/token.ts b/src/utils/token.ts index 70413ac2..5411d45a 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes } from "crypto"; export function generateRandomToken(len = 80): string { return randomBytes(len / 2).toString("hex");