Skip to content

Commit

Permalink
Merge pull request #16 from jasonraimondi/optional-config
Browse files Browse the repository at this point in the history
Authorization server optional config cleanup
  • Loading branch information
jasonraimondi authored Apr 26, 2021
2 parents ca2a8db + 296fba9 commit a7e8600
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 22 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
48 changes: 47 additions & 1 deletion docs/getting_started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,53 @@ yarn add @jmondi/oauth2-server
</code-block>
</code-group>

### 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

Expand Down
40 changes: 35 additions & 5 deletions docs/grants/authorization_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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`.

Expand All @@ -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
Expand All @@ -149,4 +178,5 @@ function base64urlencode(str: string) {
.replace(/\//g, "_")
.replace(/=/g, "");
}
```
```
:::
16 changes: 10 additions & 6 deletions src/authorization_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,27 @@ export class AuthorizationServer {
),
};

private readonly options: AuthorizationServerOptions;

constructor(
private readonly authCodeRepository: OAuthAuthCodeRepository,
private readonly clientRepository: OAuthClientRepository,
private readonly tokenRepository: OAuthTokenRepository,
private readonly scopeRepository: OAuthScopeRepository,
private readonly userRepository: OAuthUserRepository,
private readonly jwt: JwtInterface,
private readonly options: AuthorizationServerOptions = {
options?: Partial<AuthorizationServerOptions>,
) {
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;
}
Expand Down
8 changes: 6 additions & 2 deletions src/grants/abstract/abstract.grant.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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[] = [
Expand Down
5 changes: 3 additions & 2 deletions src/grants/abstract/grant.interface.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/grants/auth_code.grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
Expand Down Expand Up @@ -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",
Expand All @@ -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()}.`,
Expand Down
6 changes: 3 additions & 3 deletions test/unit/grants/auth_code.grant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit a7e8600

Please sign in to comment.