A library for authenticating and authorising users within the Guardian's identity platform hosted by Okta, using OAuth2 and OpenId Connect.
This is a slimmed down and reverse engineered version of Okta's official JS SDK that is tailored to the Guardian's needs.
Specifically, it provides a subset of the functionality of the official SDK. This is to reduce the size of the library and to ensure that we only expose the functionality that we need. Our library only uses some of the same classes and methods as the official SDK, but the implementation of them is our own.
Browsers will need to support the following features or polyfill them:
import { IdentityAuth } from '@guardian/identity-auth';
// Initialise the library with your configuration values
const identityAuth = new IdentityAuth({
issuer: 'https://profile.theguardian.com/oauth2/SERVER_ID',
clientId: 'example-client-id',
redirectUri: 'https://theguardian.com',
scopes: ['openid', 'profile'], // and any other scopes you need
});
// Check if the user is logged in and return the current auth state
const authState = await identityAuth.isSignedInWithAuthState();
authState.isAuthenticated; // true or false
authState?.accessToken; // the user's access token object
authState?.idToken; // the user's id token object
// or boolean only
const isLoggedIn = await identityAuth.isSignedIn();
// see rest of API below for more methods
Applications will need to be registered in Okta before they can use this library. Please contact the Identity team to do this. They will provide you with the configuration values you need to initialise the library.
In general you use the Access Token to make API requests to Guardian services on behalf of the user, and you use the ID Token to read the user's identity information within your application.
This is what replaces the SC_GU_U
cookie that is used in the legacy identity platform.
import type { IdentityAuthOptions } from '@guardian/identity-auth';
import { IdentityAuth } from '@guardian/identity-auth';
/**
* Defines the options that are required to configure the IdentityAuth
* Ask the Identity team for the values to use for your app.
*
* https://developer.okta.com/docs/reference/api/oidc/
*
* @param clientId - The client ID of your app
* @param issuer - The issuer of the tokens
* @param scopes - The scopes that your app requires
* @param redirectUri - The redirect URI of your app
* @param autoRenew - Whether to automatically renew the tokens, defaults to `true`
* @param renewGracePeriod - The time in seconds before the access token expires to renew the token, defaults to 60 seconds
* @param idCookieSessionRefresh - If switched on (set to `true`, ), will refresh the okta session and identity cookie (GU_U), if required (once every 30 days), by redirecting the user to `profile`'s `/signin/refresh` endpoint. Should be based on the id-cookie-refresh (idCookieRefresh) switch if being supplied by theguardian.com, defaults to `false`
* @param oauthTimeout - The time in milliseconds to wait for the OAuth2 authorization code flow to complete, defaults to 30 seconds (30000 milliseconds)
* @param maxClockSkew - The maximum time drift in seconds between the client and server, defaults to 300 seconds (5 minutes), based on the default maximum tolerance of the Kerberos protocol
* @param strictClockSkewCheck - If switched on (set to `true`), will throw an error if the clock skew is greater than the `maxClockSkew`, defaults to `false`
*/
const config: IdentityAuthOptions = {
issuer: 'https://profile.theguardian.com/oauth2/SERVER_ID',
clientId: 'example-client-id',
redirectUri: 'https://theguardian.com',
scopes: ['openid', 'profile'], // and any other scopes you need
autoRenew: true, // optional, defaults to true
renewGracePeriod: 60, // optional, defaults to 60 seconds
idCookieSessionRefresh: false, // optional, defaults to false
oauthTimeout: 30000, // optional, defaults to 30 seconds (30000 milliseconds)
maxClockSkew: 300, // optional, defaults to 300 seconds (5 minutes)
strictClockSkewCheck: false, // optional, defaults to false;
};
const identityAuth = new IdentityAuth(config);
IdentityAuth
exposes the following:
Checks if the user is signed in, and updates the auth state as necessary, returns the current auth state
This performs side effects.
- If the user has a GU_SO cookie, they have recently signed out, so we should clear their tokens (side effect)
- If user tokens already exist, they are signed in
- If the user doesn't have tokens, but they have a GU_U cookie, they are "maybe" signed in
- We can try to get tokens without prompting/redirecting the user for credentials (side effect)
- If the user doesn't have tokens or a GU_U cookie, they are not signed in
Returns the current auth state.
import { OAuthError } from '@guardian/identity-auth';
try {
// Check if the user is logged in and return the current auth state
const authState = await identityAuth.isSignedInWithAuthState();
console.log(authState.isAuthenticated); // true or false
console.log(authState?.accessToken); // the user's access token
console.log(authState?.idToken); // the user's id token
// e.g. read the id token claims to use in your application
if (authState.idToken) {
const claims = authState.idToken.claims;
console.log('identity id', claims.legacy_identity_id);
console.log('username', claims.identity_username);
}
// e.g use the access token to make an API request
if (authState.accessToken) {
const accessTokenString = authState.accessToken.accessToken;
const response = await fetch('https://some-endpoint.theguardian.com', {
headers: {
Authorization: `Bearer ${accessTokenString}`,
},
});
// Handle the response
}
} catch (error) {
if (error instanceof OAuthError) {
// Handle OAuth errors
}
// Handle other errors
}
Checks if the user is signed in, returns a boolean. Same as the isSignedInWithAuthState
method, but without the auth state.
import { OAuthError } from '@guardian/identity-auth';
// Check if the user is logged in and return a boolean
try {
const isLoggedIn = await identityAuth.isSignedIn();
} catch (error) {
if (error instanceof OAuthError) {
// Handle OAuth errors
}
// Handle other errors
}
The authStateManager
is an instance of AuthStateManager
, which manages the auth state of the user.
The IdentityAuthState
type is exported for convenience.
Returns the current auth state.
const authState = identityAuth.authStateManager.getAuthState();
authState.isAuthenticated; // true or false
authState?.accessToken; // the user's access token
authState?.idToken; // the user's id token
Subscribes to changes in the auth state. The callback will be called with the new auth state whenever it changes.
import type { IdentityAuthState } from '@guardian/identity-auth';
const handler = (authState: IdentityAuthState) => {
// do something with the new auth state
console.log(authState);
};
identityAuth.authStateManager.subscribe(handler);
Unsubscribes from changes in the auth state for a given handler.
identityAuth.authStateManager.unsubscribe(handler);
The tokenManager
is an instance of TokenManager
, which manages the Manages the storage and retrieval of tokens.
Clears the access and id tokens from storage.
identityAuth.tokenManager.clear();
Gets the tokens from storage asynchronously, can refresh tokens if required and verify them. Returns tokens or undefined
.
verifyTokens
- Iftrue
, the tokens will be verified before being returned. Defaults totrue
.refreshIfRequired
- Iftrue
, the tokens will be refreshed if they are expired. Defaults tofalse
.
const tokens = await identityAuth.tokenManager.getTokens({
verifyTokens: true,
refreshIfRequired: true,
});
tokens.accessToken; // the user's access token
tokens.idToken; // the user's id token
Gets the tokens from storage synchronously, does not refresh tokens or verify them.
Returns tokens or undefined
.
const tokens = identityAuth.tokenManager.getTokensSync();
tokens.accessToken; // the user's access token
tokens.idToken; // the user's id token
Attempts to renew the tokens, regardless of whether they are expired or not
Returns tokens or undefined
.
const renewedTokens = await identityAuth.tokenManager.renew();
Sets the tokens in local storage, very unlikely you'll need to manually use this unless you're manually retrieving tokens using the token
class.
import type { AccessToken, IDToken } from '@guardian/identity-auth';
const accessToken: AccessToken = {
...
}
const idToken: IDToken = {
...
}
const tokens = {
accessToken,
idToken,
};
identityAuth.tokenManager.setTokens(tokens);
The token
object is an instance of Token
, which manages the retrieval of tokens by performing the OAuth Authorization Code Flow with PKCE, exchanging the authorization code for tokens and verifying the ID and access tokens.
Decodes the access and id tokens, returns the decoded tokens, but does not verify that they are valid.
Verification can additionally done by calling verifyTokens
.
import { OAuthError } from '@guardian/identity-auth';
try {
// tokens from somewhere
const accessTokenRaw = '...';
const idTokenRaw = '...';
const nonce = '...';
const tokens = identityAuth.token.decodeTokens({
accessTokenRaw,
accessTokenClockSkew: 0,
idTokenRaw,
idTokenClockSkew: 0,
nonce,
});
await identityAuth.token.verifyTokens(tokens.idToken, tokens.accessToken);
} catch (error) {
// handle error
if (error instanceof OAuthError) {
// handle OAuth error
error.error; // the error returned by the OAuth server
error.error_description; // the error description returned by the OAuth server
error.message; // the error message
}
// handle other errors
}
Performs the Authorization Code Flow with PKCE, exchanging the authorization code for tokens and verifying the ID token, without prompting the user and using a hidden iframe. Returns TokenResponse
or throws an OAuthError
.
import { OAuthError } from '@guardian/identity-auth';
try {
const tokenResponse = await identityAuth.token.getWithoutPrompt();
tokenResponse.tokens.accessToken; // the user's access token
tokenResponse.tokens.idToken; // the user's id token
tokenResponse.state; // the state parameter passed to the authorize endpoint
// set the tokens in local storage
identityAuth.tokenManager.setTokens(tokenResponse.tokens);
} catch (error) {
// handle error
if (error instanceof OAuthError) {
// handle OAuth error
error.error; // the error returned by the OAuth server
error.error_description; // the error description returned by the OAuth server
error.message; // the error message
}
// handle other errors
}
Verifies the ID token, checking the signature and claims, and verifies the access token using the at_hash
claim from the IDToken.
idToken
- The ID token to verifyaccessToken
- The access token to verify
Returns void
or throws an OAuthError
.
import { OAuthError } from '@guardian/identity-auth';
try {
// tokens from somewhere
const tokens = identityAuth.tokenManager.getTokensSync();
await identityAuth.token.verifyTokens(tokens.idToken, tokens.accessToken);
} catch (error) {
// handle error
if (error instanceof OAuthError) {
// handle OAuth error
error.error; // the error returned by the OAuth server
error.error_description; // the error description returned by the OAuth server
error.message; // the error message
}
// handle other errors
}
The autoRenew
object is an instance of AutoRenew
, which manages the automatic renewal of tokens based on the expiry time of the tokens and the autoRenew
options (autoRenew
and renewGracePeriod
).
Starts the automatic renewal of tokens, will automatically be started, no need to call, but exposed for use in other internal classes.
We export a number of types for convenience, which can be useful when using the library. See the types themselves for more information.
import type {
IdentityAuthOptions,
IdentityAuthState,
AccessToken,
AccessTokenClaims,
CustomClaims,
IDToken,
IDTokenClaims,
} from '@guardian/identity-auth';
// also the error which isn't a type, but is useful for checking if an error is an OAuth error
// e.g if (error instanceof OAuthError) { ... }
import { OAuthError } from '@guardian/identity-auth';
Depending on the scopes you request, you may get custom claims in the Access Token or ID Token. If you don't create a type for these claims, then typescript will complain about them not existing.
You can add custom claims for you application by creating a type that extends the CustomClaims
type.
A claim value can be a string
, number
, boolean
, string[]
, number[]
or boolean[]
.
type CustomAccessTokenClaims = CustomClaims & {
foo: string;
};
type CustomIdTokenClaims = CustomClaims & {
bar: number[];
baz: boolean;
};
// pass the types to the IdentityAuth class as generics
const auth = new IdentityAuth<CustomAccessTokenClaims, CustomIdTokenClaims>({
...
});
// the custom claims will be available on the AccessToken and IDToken types
const authState = auth.authStateManager.getAuthState();
authState.accessToken.foo; // string
authState.idToken.bar; // number[]
authState.idToken.baz; // boolean
Due to the nature of this library, if you want to make changes to it, we suggest you speak to the Identity team first regarding the nature of the changes you want to make, e.g. for bug fixes, new features, etc.