From b886e85a3d6d6121712db60fc46fb49cfc5b249f Mon Sep 17 00:00:00 2001 From: Jason Raimondi Date: Mon, 26 Apr 2021 13:12:04 -0700 Subject: [PATCH 1/2] refactor: authorization server optional config --- src/authorization_server.ts | 16 ++++++++++------ src/grants/abstract/abstract.grant.ts | 8 ++++++-- src/grants/abstract/grant.interface.ts | 5 +++-- src/grants/auth_code.grant.ts | 6 +++--- test/unit/grants/auth_code.grant.spec.ts | 6 +++--- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/authorization_server.ts b/src/authorization_server.ts index e4263a65..5839b925 100644 --- a/src/authorization_server.ts +++ b/src/authorization_server.ts @@ -68,6 +68,8 @@ export class AuthorizationServer { ), }; + private readonly options: AuthorizationServerOptions; + constructor( private readonly authCodeRepository: OAuthAuthCodeRepository, private readonly clientRepository: OAuthClientRepository, @@ -75,16 +77,18 @@ export class AuthorizationServer { private readonly scopeRepository: OAuthScopeRepository, private readonly userRepository: OAuthUserRepository, private readonly jwt: JwtInterface, - private readonly options: AuthorizationServerOptions = { + options?: Partial, + ) { + this.options = { requiresPKCE: true, - useUrlEncode: false, - }, - ) {} + useUrlEncode: true, + ...options, + } + } enableGrantType(grantType: GrantIdentifier, accessTokenTTL: DateInterval = new DateInterval("1h")): void { const grant = this.availableGrants[grantType]; - grant.requiresPKCE = this.options.requiresPKCE; - grant.useUrlEncode = this.options.useUrlEncode; + grant.options = this.options; this.enabledGrantTypes[grantType] = grant; this.grantTypeAccessTokenTTL[grantType] = accessTokenTTL; } diff --git a/src/grants/abstract/abstract.grant.ts b/src/grants/abstract/abstract.grant.ts index 2f50df2d..0dfdeee7 100644 --- a/src/grants/abstract/abstract.grant.ts +++ b/src/grants/abstract/abstract.grant.ts @@ -1,3 +1,4 @@ +import { AuthorizationServerOptions } from "../../authorization_server"; import { isClientConfidential, OAuthClient } from "../../entities/client.entity"; import { OAuthScope } from "../../entities/scope.entity"; import { OAuthToken } from "../../entities/token.entity"; @@ -32,8 +33,11 @@ export interface ITokenData { } export abstract class AbstractGrant implements GrantInterface { - public requiresPKCE = true; - public useUrlEncode = true; + public readonly options: AuthorizationServerOptions = { + requiresPKCE: true, + useUrlEncode: true, + }; + protected readonly scopeDelimiterString = " "; protected readonly supportedGrantTypes: GrantIdentifier[] = [ diff --git a/src/grants/abstract/grant.interface.ts b/src/grants/abstract/grant.interface.ts index 3feaa74f..388199ee 100644 --- a/src/grants/abstract/grant.interface.ts +++ b/src/grants/abstract/grant.interface.ts @@ -1,3 +1,4 @@ +import { AuthorizationServerOptions } from "../../authorization_server"; import { AuthorizationRequest } from "../../requests/authorization.request"; import { RequestInterface } from "../../requests/request"; import { ResponseInterface } from "../../responses/response"; @@ -6,8 +7,8 @@ import { DateInterval } from "../../utils/date_interval"; export type GrantIdentifier = "authorization_code" | "client_credentials" | "refresh_token" | "password" | "implicit"; export interface GrantInterface { - requiresPKCE: boolean; - useUrlEncode: boolean; + options: AuthorizationServerOptions; + identifier: GrantIdentifier; canRespondToAccessTokenRequest(request: RequestInterface): boolean; diff --git a/src/grants/auth_code.grant.ts b/src/grants/auth_code.grant.ts index 5b951838..b893118c 100644 --- a/src/grants/auth_code.grant.ts +++ b/src/grants/auth_code.grant.ts @@ -106,7 +106,7 @@ export class AuthCodeGrant extends AbstractAuthorizedGrant { verifier = this.codeChallengeVerifiers.S256; } - if (!verifier.verifyCodeChallenge(codeVerifier, validatedPayload.code_challenge, this.useUrlEncode)) { + if (!verifier.verifyCodeChallenge(codeVerifier, validatedPayload.code_challenge, this.options.useUrlEncode)) { throw OAuthException.invalidGrant("Failed to verify code challenge."); } } @@ -157,7 +157,7 @@ export class AuthCodeGrant extends AbstractAuthorizedGrant { const codeChallenge = this.getQueryStringParameter("code_challenge", request); - if (this.requiresPKCE && !codeChallenge) { + if (this.options.requiresPKCE && !codeChallenge) { throw OAuthException.invalidRequest( "code_challenge", "The authorization server requires public clients to use PKCE RFC-7636", @@ -167,7 +167,7 @@ export class AuthCodeGrant extends AbstractAuthorizedGrant { if (codeChallenge) { const codeChallengeMethod = this.getQueryStringParameter("code_challenge_method", request, "plain"); - if (!REGEXP_CODE_CHALLENGE.test(this.useUrlEncode ? base64decode(codeChallenge) : codeChallenge)) { + if (!REGEXP_CODE_CHALLENGE.test(this.options.useUrlEncode ? base64decode(codeChallenge) : codeChallenge)) { throw OAuthException.invalidRequest( "code_challenge", `Code challenge must follow the specifications of RFC-7636 and match ${REGEXP_CODE_CHALLENGE.toString()}.`, diff --git a/test/unit/grants/auth_code.grant.spec.ts b/test/unit/grants/auth_code.grant.spec.ts index 3ec480d4..6d6ac8d5 100644 --- a/test/unit/grants/auth_code.grant.spec.ts +++ b/test/unit/grants/auth_code.grant.spec.ts @@ -173,7 +173,7 @@ describe("authorization_code grant", () => { state: "state-is-a-secret", }, }); - grant.requiresPKCE = false; + grant.options.requiresPKCE = false; // act const authorizationRequest = await grant.validateAuthorizationRequest(request); @@ -320,7 +320,7 @@ describe("authorization_code grant", () => { // it("uses clients redirect url if request ", async () => {}); it("is successful without pkce flow", async () => { - grant.requiresPKCE = false; + grant.options.requiresPKCE = false; const authorizationRequest = new AuthorizationRequest("authorization_code", client, "http://example.com"); authorizationRequest.isAuthorizationApproved = true; authorizationRequest.state = "abc123"; @@ -396,7 +396,7 @@ describe("authorization_code grant", () => { }); it("is successful without pkce", async () => { - grant.requiresPKCE = false; + grant.options.requiresPKCE = false; authorizationRequest = new AuthorizationRequest("authorization_code", client, "http://example.com"); authorizationRequest.isAuthorizationApproved = true; authorizationRequest.user = user; From 296fba9b31305fa39a40079c2dacba4365ac75b2 Mon Sep 17 00:00:00 2001 From: Jason Raimondi Date: Mon, 26 Apr 2021 13:12:56 -0700 Subject: [PATCH 2/2] docs: add docs for authorization server optional config --- README.md | 26 +++++++++++++++++ docs/getting_started/README.md | 48 ++++++++++++++++++++++++++++++- docs/grants/authorization_code.md | 40 ++++++++++++++++++++++---- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1985d495..9e2861e6 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,32 @@ authorizationServer.enableGrantType("client_credentials", new DateInterval("5h") authorizationServer.enableGrantType("authorization_code", new DateInterval("2h")); ``` +The authorization server has a few optional settings with the following default values; + +```typescript +AuthorizationServerOptions { + requiresPKCE: true; + useUrlEncode: true; +} +``` + +To configure these options, pass the value in as the last argument: + +```typescript +const authorizationServer = new AuthorizationServer( + authCodeRepository, + clientRepository, + accessTokenRepository, + scopeRepository, + userRepository, + new JwtService("secret-key"), + { + requiresPKCE: false, // default is true + useUrlEncode: false, // default is true + } +); +``` + ### Repositories There are a few repositories you are going to need to implement in order to create an `AuthorizationServer`. diff --git a/docs/getting_started/README.md b/docs/getting_started/README.md index 3015b81a..9b34fd8b 100644 --- a/docs/getting_started/README.md +++ b/docs/getting_started/README.md @@ -18,7 +18,53 @@ yarn add @jmondi/oauth2-server -### Getting Started +## The Authorization Server + +The AuthorizationServer depends on [the repositories](#repositories). By default, no grants are enabled; each grant is opt-in and must be enabled when creating the AuthorizationServer. + +You can enable any grant types you would like to support. + +```typescript +const authorizationServer = new AuthorizationServer( + authCodeRepository, + clientRepository, + accessTokenRepository, + scopeRepository, + userRepository, + new JwtService("secret-key"), +); +authorizationServer.enableGrantType("client_credentials"); +authorizationServer.enableGrantType("authorization_code"); +authorizationServer.enableGrantType("refresh_token"); +authorizationServer.enableGrantType("implicit"); // implicit grant is not recommended +authorizationServer.enableGrantType("password"); // password grant is not recommended +``` + +The authorization server has a few optional settings with the following default values; + +```typescript +AuthorizationServerOptions { + requiresPKCE: true; + useUrlEncode: true; +} +``` + +To configure these options, pass the value in as the last argument: + +```typescript +const authorizationServer = new AuthorizationServer( + authCodeRepository, + clientRepository, + accessTokenRepository, + scopeRepository, + userRepository, + new JwtService("secret-key"), + { + requiresPKCE: false, // default is true + useUrlEncode: false, // default is true + } +); +``` ## The Token Endpoint diff --git a/docs/grants/authorization_code.md b/docs/grants/authorization_code.md index 99626825..8ef98277 100644 --- a/docs/grants/authorization_code.md +++ b/docs/grants/authorization_code.md @@ -102,7 +102,27 @@ Pragma: no-cache ``` ::: -### Code Verifier +### PKCE + +PKCE ([RFC 7636](https://tools.ietf.org/html/rfc7636)) is an extension to the [Authorization Code flow](https://oauth.net/2/grant-types/authorization-code/) to prevent several attacks and to be able to securely perform the OAuth exchange from public clients. + +By default, PKCE is enabled and encouraged for all users. If you need to support a legacy client system without PKCE, you can disable PKCE with the authorization server: + +``` +const authorizationServer = new AuthorizationServer( + authCodeRepository, + clientRepository, + accessTokenRepository, + scopeRepository, + userRepository, + new JwtService("secret-key"), + { + requiresPKCE: false, + } +); +``` + +#### Code Verifier The `code_verifier` is part of the extended [“PKCE”](https://tools.ietf.org/html/rfc7636) and helps mitigate the threat of having authorization codes intercepted. @@ -116,10 +136,20 @@ import crypto from "crypto"; const code_verifier = crypto.randomBytes(43).toString("hex"); ``` -https://www.oauth.com/oauth2-servers/pkce/authorization-request/ +@see [https://www.oauth.com/oauth2-servers/pkce/authorization-request/](https://www.oauth.com/oauth2-servers/pkce/authorization-request/) + +::: tip +You can opt out of the base64 url encode with the following [AuthorizationServer option](../getting_started/#the-authorization-server): + +```typescript +{ + useUrlEncode: false, +} +``` +::: -### Code Challenge +#### Code Challenge Now we need to create a `code_challenge` from our `code_verifier`. @@ -138,7 +168,6 @@ Clients that do not have the ability to perform a SHA256 hash are permitted to u ```typescript const code_challenge = code_verifier; ``` -::: ::: details Need a base64urlencode function? ```typescript @@ -149,4 +178,5 @@ function base64urlencode(str: string) { .replace(/\//g, "_") .replace(/=/g, ""); } -``` \ No newline at end of file +``` +:::