@zeushq/nextjs-zidentity
Version:
Next.js SDK for signin in with Zeus Identity
140 lines (124 loc) • 4.53 kB
text/typescript
import { IncomingMessage, ServerResponse } from 'http';
import { NextApiRequest, NextApiResponse } from 'next';
import { ClientFactory } from '../zsession';
import { AccessTokenError } from '../utils/errors';
import { intersect, match } from '../utils/arrays';
import { SessionCache, fromTokenSet } from '../session';
import { NextConfig } from '../config';
/**
* Custom options to get an Access Token.
*
* @category Server
*/
export interface AccessTokenRequest {
/**
* A list of desired scopes for your Access Token.
*/
scopes?: string[];
/**
* If set to `true`, a new Access Token will be requested with the Refresh Token grant, regardless of whether
* the Access Token has expired or not.
*/
refresh?: boolean;
}
/**
* Response from requesting an Access Token.
*
* @category Server
*/
export interface GetAccessTokenResult {
/**
* Access token returned from the token cache.
*/
accessToken?: string | undefined;
}
/**
* Get an Access Token to access an external API.
*
* @category Server
*/
export type GetAccessToken = (
req: IncomingMessage | NextApiRequest,
res: ServerResponse | NextApiResponse,
accessTokenRequest?: AccessTokenRequest
) => Promise<GetAccessTokenResult>;
/**
* @ignore
*/
export default function accessTokenFactory(
config: NextConfig,
getClient: ClientFactory,
sessionCache: SessionCache
): GetAccessToken {
return async (req, res, accessTokenRequest): Promise<GetAccessTokenResult> => {
const session = sessionCache.get(req, res);
if (!session) {
throw new AccessTokenError('invalid_session', 'The user does not have a valid session.');
}
if (!session.accessToken && !session.refreshToken) {
throw new AccessTokenError('invalid_session', 'The user does not have a valid access token.');
}
if (!session.accessTokenExpiresAt) {
throw new AccessTokenError(
'access_token_expired',
'Expiration information for the access token is not available. The user will need to sign in again.'
);
}
if (accessTokenRequest && accessTokenRequest.scopes) {
const persistedScopes = session.accessTokenScope;
if (!persistedScopes || persistedScopes.length === 0) {
throw new AccessTokenError(
'insufficient_scope',
'An access token with the requested scopes could not be provided. The user will need to sign in again.'
);
}
const matchingScopes = intersect(accessTokenRequest.scopes, persistedScopes.split(' '));
if (!match(accessTokenRequest.scopes, [...matchingScopes])) {
throw new AccessTokenError(
'insufficient_scope',
`Could not retrieve an access token with scopes "${accessTokenRequest.scopes.join(
' '
)}". The user will need to sign in again.`
);
}
}
// Check if the token has expired.
// There is an edge case where we might have some clock skew where our code assumes the token is still valid.
// Adding a skew of 1 minute to compensate.
if (!session.refreshToken && session.accessTokenExpiresAt * 1000 - 60000 < Date.now()) {
throw new AccessTokenError(
'access_token_expired',
'The access token expired and a refresh token is not available. The user will need to sign in again.'
);
}
// Check if the token has expired.
// There is an edge case where we might have some clock skew where our code assumes the token is still valid.
// Adding a skew of 1 minute to compensate.
if (
(session.refreshToken && session.accessTokenExpiresAt * 1000 - 60000 < Date.now()) ||
(session.refreshToken && accessTokenRequest && accessTokenRequest.refresh)
) {
const client = await getClient();
const tokenSet = await client.refresh(session.refreshToken);
// Update the session.
const newSession = fromTokenSet(tokenSet, config);
Object.assign(session, {
...newSession,
refreshToken: newSession.refreshToken || session.refreshToken,
user: { ...session.user, ...newSession.user }
});
// Return the new access token.
return {
accessToken: tokenSet.access_token
};
}
// We don't have an access token.
if (!session.accessToken) {
throw new AccessTokenError('invalid_session', 'The user does not have a valid access token.');
}
// The access token is not expired and has sufficient scopes;
return {
accessToken: session.accessToken
};
};
}