OAuth2 client for Node and browsers
This package contains an OAuth2 client. It aims to be a fully-featured OAuth2
utility library, for Node.js, Browsers and written in Typescript.
This OAuth2 client is only 4KB gzipped, it has 0 dependencies and
relies on modern APIs like fetch() and Web Crypto which are built-in
since Node 18.
Highlights
- 16KB minified (5KB gzipped).
- No dependencies.
authorization_code grant with optional PKCE support.
password and client_credentials grant.
- a
fetch() wrapper that automatically adds Bearer tokens and refreshes them.
- OAuth2 endpoint discovery via the Server metadata document (RFC8414).
- OAuth2 Token Introspection (RFC7662).
- Resource Indicators for OAuth 2.0 (RFC8707).
- OAuth2 Token Revocation (RFC7009).
- OAuth 2.0 Multiple Response Type Encoding Practices
Installation
npm i @badgateway/oauth2-client
Usage
To get started, set up the Client class.
import { OAuth2Client } from '@badgateway/oauth2-client';
const client = new OAuth2Client({
server: 'https://my-auth-server/',
clientId: '...',
clientSecret: '...',
tokenEndpoint: '/token',
authorizationEndpoint: '/authorize',
discoveryEndpoint: '/.well-known/oauth2-authorization-server',
});
Tokens
Many functions use or return a 'OAuth2Token' type. This type has the following
shape:
export type OAuth2Token = {
accessToken: string;
refreshToken: string | null;
expiresAt: number | null;
idToken?: string;
};
client_credentials grant.
const token = await client.clientCredentials();
Refreshing tokens
const newToken = await client.refreshToken(oldToken);
password grant:
const token = await client.password({
username: '..',
password: '..',
});
authorization_code
The authorization_code flow is the flow for browser-based applications,
and roughly consists of 3 major steps:
- Redirect the user to an authorization endpoint, where they log in.
- Authorization endpoint redirects back to app with a 'code' query
parameter.
- The
code is exchanged for a access and refresh token.
This library provides support for these steps, but there's no requirement
to use its functionality as the system is mostly stateless.
import { OAuth2Client, generateCodeVerifier } from 'client';
const client = new OAuth2Client({
server: 'https://authserver.example/',
clientId: '...',
tokenEndpoint: '/token',
authorizationEndpoint: '/authorize',
});
Redirecting the user to the authorization server
const codeVerifier = await generateCodeVerifier();
document.location = await client.authorizationCode.getAuthorizeUri({
redirectUri: 'https://my-app.example/',
state: 'some-string',
codeVerifier,
scope: ['scope1', 'scope2'],
});
Handling the redirect back to the app and obtain token
const oauth2Token = await client.authorizationCode.getTokenFromCodeRedirect(
document.location,
{
redirectUri: 'https://my-app.example/',
state: 'some-string',
codeVerifier,
}
);
Fetch Wrapper
When using an OAuth2-protected API, typically you will need to obtain an Access
token, and then add this token to each request using an Authorization: Bearer
header.
Because access tokens have a limited lifetime, and occasionally needs to be
refreshed this is a bunch of potential plumbing.
To make this easier, this library has a 'fetch wrapper'. This is effectively
just like a regular fetch function, except it automatically adds the header
and will automatically refresh tokens when needed.
Usage:
import { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';
const client = new OAuth2Client({
server: 'https://my-auth-server',
clientId: 'my-client-id'
});
const fetchWrapper = new OAuth2Fetch({
client: client,
getNewToken: async () => {
return client.clientCredentials();
return client.authorizationCode.getToken({
code: '..',
redirectUri: '..',
});
return null;
},
onError: (err) => {
}
});
After set up, you can just call fetch on the new object to call your API, and
the library will ensure there's always a Bearer header.
const response = fetchWrapper.fetch('https://my-api', {
method: 'POST',
body: 'Hello world'
});
Storing tokens for later use with FetchWrapper
To keep a user logged in between sessions, you may want to avoid full
reauthentication. To do this, you'll need to store authentication token
somewhere.
The fetch wrapper has 2 functions to help with this:
const fetchWrapper = new OAuth2Fetch({
client: client,
getNewToken: async () => {
},
storeToken: (token) => {
document.localStorage.setItem('token-store', JSON.stringify(token));
},
getStoredToken: () => {
const token = document.localStorage.getItem('token-store');
if (token) return JSON.parse(token);
return null;
}
});
Fetch Middleware function
It might be preferable to use this library as a more traditional 'middleware'.
The OAuth2Fetch object also exposes a mw function that returns a middleware
for fetch.
const mw = oauth2.mw();
const response = mw(
myRequest,
req => fetch(req)
);
This syntax looks a bit wild if you're not used to building middlewares, but
this effectively allows you to 'decorate' existing request libraries with
functionality from this oauth2 library.
A real example using the Ketting
library:
import { Client } from 'ketting';
import { OAuth2Client, OAuth2Fetch } from '@badgateway/oauth2-client';
const oauth2Client = new OAuth2Client({
server: 'https://my-auth.example',
clientId: 'foo',
});
const oauth2Fetch = new OAuth2Fetch({
client: oauth2Client,
});
const ketting = new Client('http://api-root');
ketting.use(oauth2Fetch.mw());
Introspection
Introspection (RFC7662) lets you find more information about a token,
such as whether it's valid, which user it belongs to, which oauth2 client
was used to generate it, etc.
To be able to use it, your authorization server must have support for the
introspection endpoint. It's location will be automatically detected using
the Metadata discovery document.
import { OAuth2Client } from '@badgateway/oauth2-client';
const client = new Client({
server: 'https://auth-server.example/',
clientId: '...',
clientSecret: '...',
});
const token = client.clientCredentials();
console.log(client.introspect(token));
OAuth2 client_id and client_secret encoding
OAuth2 allows users to encode the client_id and client_secret either in a
Authorization: Basic header or in the POST request body.
Real-world OAuth2 servers may support one or the other, or both. The OAuth2
spec requires that servers support the Authorization header, and don't
recommend using the body.
By default, this library will use the Authorization header. OAuth2 also
requires that clients percent-encode the client_id and client_secret, but
in practice many popular servers break if you do this. By default this library
will not percent encode any characters except the : character.
You can change this behavior using the authenticatioMethod flag:
const client = new OAuth2Client({
server: 'https://auth-server.example/',
clientId: '...',
clientSecret: '...',
authenticationMethod: 'client_secret_post',
});
The following 3 values are currently supported:
client_secret_post - Encode in POST body
client_secret_basic - Encode in Authorization header using the strict
standard rules.
client_secret_basic_interop - Encode in Authorization header using less
strict rules. This is the default and more likely to work with popular
servers (at least some Google and Ebay APIs want this).
The current OAuth 2.1 draft switches the recommendation to use
client_secret_post by default instead. When that document stabilizes and gets
released, this library will also switch to use client_secret_post by default
in a major release.
If your OAuth2 server supports POST, we recommend you use client_secret_post
as this is more likely to work without a hitch.
If you configured the client using the OAuth2 discovery document, and the
server indicates it prefers client_secret_basic we will also default to the
strict form.
Support for older Node versions
This package works out of the box with modern browsers and Node 18.
For Node 16 and below, use a 2.x version of this package and add
polyfills. The README.md for the 2.x branch of this package contains more information
on the exact steps for older Node versions.