/ts-oauth2-server

A standards compliant implementation of an OAuth 2.0 authorization server for Node that utilizes JWT and Proof Key for Code Exchange (PKCE), written in TypeScript.

Primary LanguageTypeScriptMIT LicenseMIT

TypeScript OAuth2.0 Server

GitHub Workflow Status Test Coverage Maintainability GitHub package.json version NPM Downloads

@jmondi/oauth2-server is a standards compliant implementation of an OAuth 2.0 authorization server for Node, written in TypeScript.

Requires node >= 12

The following RFCs are implemented:

Out of the box it supports the following grants:

Adapters are included for the following frameworks:

The included adapters are just helper functions, really any framework should be supported. Take a look at the adapter implementations for express and fastify to learn how you can implement one for your favorite tool!

Getting Started

Save some eye strain, use the documentation site

Install

pnpm add @jmondi/oauth2-server

Endpoints

The server uses two endpoints, GET /authorize and POST /token.

The Token Endpoint is a back channel endpoint that issues a use-able access token.

The Authorize Endpoint is a front channel endpoint that issues an authorization code. The authorization code can then be exchanged to the AuthorizationServer endpoint for a use-able access token.

The Token Endpoint

import {
 handleExpressResponse,
 handleExpressError,
} from "@jmondi/oauth2-server/dist/adapters/express";

app.post("/token", async (req: Express.Request, res: Express.Response) => {
 const request = requestFromExpress(req);
 try {
  const oauthResponse = await authorizationServer.respondToAccessTokenRequest(request);
  return handleExpressResponse(res, oauthResponse);
 } catch (e) {
  handleExpressError(e, res);
  return;
 }
});

Authorize Endpoint

The /authorize endpoint is a front channel endpoint that issues an authorization code. The authorization code can then be exchanged to the AuthorizationServer endpoint for a useable access token.

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.

We are able to add in scope acceptance and 2FA into our authentication flow.

import { requestFromExpress } from "@jmondi/oauth2-server/dist/adapters/express";

app.get("/authorize", async (req: Express.Request, res: Express.Response) => {
  const request = requestFromExpress(req);

  try {
    // Validate the HTTP request and return an AuthorizationRequest.
    const authRequest = await authorizationServer.validateAuthorizationRequest(request);

    // You will probably redirect the user to a login endpoint. 
    if (!req.user) {
      res.redirect("/login")
      return;
    }
    // After login, the user should be redirected back with user in the session.
    // You will need to manage the authorization query on the round trip.
    // The auth request object can be serialized and saved into a user's session.

    // Once the user has logged in set the user on the AuthorizationRequest
    authRequest.user = req.user;
    
    // Once the user has approved or denied the client update the status
    // (true = approved, false = denied)
    authRequest.isAuthorizationApproved = getIsAuthorizationApprovedFromSession();

    // If the user has not approved the client's authorization request, 
    // the user should be redirected to the approval screen.
    if (!authRequest.isAuthorizationApproved) {
      // This form will ask the user to approve the client and the scopes requested.
      // "Do you authorize Jason to: read contacts? write contacts?"
      res.redirect("/scopes")
      return;
    }

    // At this point the user has approved the client for authorization.
    // Any last authorization requests such as Two Factor Authentication (2FA) can happen here.


    // Redirect back to redirect_uri with `code` and `state` as url query params.
    const oauthResponse = await authorizationServer.completeAuthorizationRequest(authRequest);
    return handleExpressResponse(res, oauthResponse);
  } catch (e) {
    handleExpressError(e, res);
  }
});

Authorization Server

The AuthorizationServer depends on the 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.

const authorizationServer = new AuthorizationServer(
  authCodeRepository,
  clientRepository,
  accessTokenRepository,
  scopeRepository,
  userRepository,
  new JwtService("secret-key"),
);

// Enable as many or as few grants as you'd like. 
// You can pass in an optional tuple of [GrantType, DateInterval]
// to set Access Token TTL per grant type.
authorizationServer.enableGrantTypes(
  ["client_credentials", new DateInterval("1d")],
  ["authorization_code", new DateInterval("15m")],
  "refresh_token",
);
authorizationServer.enableGrantType("implicit"); // implicit grant is not recommended
authorizationServer.enableGrantType("password"); // password grant is not recommended

By default, all access tokens have a 1 hour time to live. An optional second parameter in enableGrantType allows the Access Token TTL for each grant type to be set.

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;

type AuthorizationServerOptions = {
  requiresPKCE: true;
  requiresS256: false;
  notBeforeLeeway: 0;
  tokenCID: "name"|"id"; // in v2.x default is "name", in 3.x default will be "id"
}
  • requiresPKCE - Enabled 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.
  • requiresS256 - Disabled by default. If you want to require all clients to use S256, you can enable that here.
  • notBeforeLeeway - Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value.
  • tokenCID - Sets the accessToken.cid to either the client.id or client.name. In v2.x default is "name", in 3.x default will be "id".

To configure these options, pass the value in as the last argument:

const authorizationServer = new AuthorizationServer(
  authCodeRepository,
  clientRepository,
  accessTokenRepository,
  scopeRepository,
  userRepository,
  new JwtService("secret-key"),
  {
    requiresS256: true, // default is false
  }
);

Repositories

There are a few repositories you are going to need to implement in order to create an AuthorizationServer.

Auth Code Repository

Client Repository

Scope Repository

Token Repository

User Repository

Entities

And a few entities.

Auth Code Entity

Client Entity

Scope Entity

Token Entity

User Entity

Grants

Grants are different ways a client can obtain an access_token that will authorize it to use the resource server.

Which Grant?

Deciding which grant to use depends on the type of client the end user will be using.

+-------+
| Start |
+-------+
    V
    |
    
    |
+------------------------+              +-----------------------+
| Have a refresh token?  |>----Yes----->|  Refresh Token Grant  |
+------------------------+              +-----------------------+
    V
    |
    No
    |
+---------------------+                
|     Who is the      |                  +--------------------------+
| Access token owner? |>---A Machine---->| Client Credentials Grant |
+---------------------+                  +--------------------------+
    V
    |
    |
   A User
    |
    |
+----------------------+                
| What type of client? |   
+----------------------+     
    |
    |                                 +---------------------------+
    |>-----------Server App---------->| Auth Code Grant with PKCE |
    |                                 +---------------------------+
    |
    |                                 +---------------------------+
    |>-------Browser Based App------->| Auth Code Grant with PKCE |
    |                                 +---------------------------+
    |
    |                                 +---------------------------+
    |>-------Native Mobile App------->| Auth Code Grant with PKCE |
                                      +---------------------------+

Client Credentials Grant

Full Docs

When applications request an access token to access their own resources, not on behalf of a user.

Flow

The client sends a POST to the /token endpoint with the following body:

  • grant_type must be set to client_credentials
  • client_id is the client identifier you received when you first created the application
  • client_secret is the client secret
  • scope is a string with a space delimited list of requested scopes. The requested scopes must be valid for the client.

The authorization server will respond with the following response.

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and can be used to authenticate into the resource server
  • scope is a space delimited list of scopes the token has access to

Authorization Code Grant (w/ PKCE)

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.

Flow

Part One

The client redirects the user to the /authorize with the following query parameters:

  • response_type must be set to code
  • client_id is the client identifier you received when you first created the application
  • redirect_uri indicates the URL to return the user to after authorization is complete, such as org.example.app://redirect
  • state is a random string generated by your application, which you’ll verify later
  • 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.

The user will be asked to login to the authorization server and approve the client and requested scopes.

If the user approves the client, they will be redirected from the authorization server to the provided redirect_uri with the following fields in the query string:

  • code is the authorization code that will soon be exchanged for a token
  • state is the random string provided and should be compared against the initially provided state
Part Two

The client sends a POST to the /token endpoint with the following body:

  • grant_type must be set to authorization_code
  • client_id is the client identifier you received when you first created the application
  • client_secret (optional) is the client secret and should only be provided if the client is confidential
  • redirect_uri
  • code_verifier
  • code is the authorization code from the query string

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Code Verifier

The code_verifier is part of the extended “PKCE” and helps mitigate the threat of having authorization codes intercepted.

Before initializing Part One of the authorization code flow, the client first creats a code_verifier. This is a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long.

We can do this in Node using the native crypto package and a base64urlencode function:

import crypto from "crypto";

const code_verifier = crypto.randomBytes(43).toString("hex");

https://www.oauth.com/oauth2-servers/pkce/authorization-request/

Code Challenge

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.

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.

const code_challenge = code_verifier;

Refresh Token

Access tokens eventually expire. The refresh token grant enables the client to obtain a new access_token from an existing refresh_token.

Flow

A complete refresh token request will include the following parameters:

  • grant_type must be set to refresh_token
  • client_id is the client identifier you received when you first created the application
  • client_secret if the client is confidential (has a secret), this must be provided
  • refresh_token must be the signed token previously issued to the client
  • scope (optional) the requested scope must not include any additional scopes that were not previously issued to the original token

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Password Grant

The Password Grant is for first party clients that are able to hold secrets (ie not Browser or Native Mobile Apps)

Flow

A complete refresh token request will include the following parameters:

  • grant_type must be set to password
  • client_id is the client identifier you received when you first created the application
  • client_secret if the client is confidential (has a secret), this must be provided
  • username
  • password
  • scope (optional)

The authorization server will respond with the following response

  • token_type will always be Bearer
  • expires_in is the time the token will live in seconds
  • access_token is a JWT signed token and is used to authenticate into the resource server
  • refresh_token is a JWT signed token and can be used in with the refresh grant
  • scope is a space delimited list of scopes the token has access to

Implicit Grant

This grant is supported in the AuthorizationServer, but not recommended to use and thus is not documented. Industry best practice recommends using the Authorization Code Grant w/ PKCE for clients such as native and browser-based apps.

Please look at these great resources:

Revoke Tokens (RFC7009 “OAuth 2.0 Token Revocation”)

Note: Implementing this endpoint is optional.

The /token/revoke endpoint is a back channel endpoint that revokes an existing token. Implementing this endpoint is optional.

app.post("/token/revoke", async (req: Express.Request, res: Express.Response) => {
  try {
    const oauthResponse = await authorizationServer.revoke(req);
    return handleExpressResponse(res, oauthResponse);
  } catch (e) {
    handleExpressError(e, res);
    return;
  }
});

Thanks

This project is inspired by the PHP League's OAuth2 Server. Check out the PHP League's other packages for some other great PHP projects.

Star History

Star History Chart