TypeScript OAuth2.0 Server
@jmondi/oauth2-server
is a standards compliant implementation of an OAuth 2.0 authorization server for Node, written in TypeScript.
Requires node >= 12
Out of the box it supports the following grants:
The following RFCs are implemented:
Adapters are included for the following frameworks:
Getting Started
Save some eye strain, use the documentation site
Install
npm install --save @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,
responseFromExpress,
} from "@jmondi/oauth2-server/dist/adapters/express";
app.post("/token", async (req: Express.Request, res: Express.Response) => {
try {
const oauthResponse = await authorizationServer.respondToAccessTokenRequest(req, responseFromExpress(res));
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 {
const authRequest = await authorizationServer.validateAuthorizationRequest(request);
if (!req.user) {
res.redirect("/login")
return;
}
authRequest.user = req.user;
authRequest.isAuthorizationApproved = getIsAuthorizationApprovedFromSession();
if (!authRequest.isAuthorizationApproved) {
res.redirect("/scopes")
return;
}
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"),
);
authorizationServer.enableGrantTypes(
["client_credentials", new DateInterval("1d")],
["authorization_code", new DateInterval("15m")],
"refresh_token",
);
authorizationServer.enableGrantType("implicit");
authorizationServer.enableGrantType("password");
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;
}
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"),
{
requiresPKCE: 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("hex")
);
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:
Todo
Definitely:
- feat: add optional refresh token version to expire refresh tokens
Maybe:
- feat?: token introspection
- feat?: allow users to only use individual grant, and not require all entities/repositories
Maybe:
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.