@zeushq/nextjs-zidentity
Version:
Next.js SDK for signin in with Zeus Identity
178 lines (157 loc) • 5.5 kB
text/typescript
import { IncomingMessage, ServerResponse } from 'http';
import { generators } from 'openid-client';
import { JWKS, JWS, JWK } from 'jose';
import { signing as deriveKey } from './utils/hkdf';
import { get as getCookie, clear as clearCookie, set as setCookie } from './utils/cookies';
import { Config } from './config';
export interface StoreOptions {
sameSite?: boolean | 'lax' | 'strict' | 'none';
value?: string;
}
const header = { alg: 'HS256', b64: false, crit: ['b64'] };
const getPayload = (cookie: string, value: string): Buffer => Buffer.from(`${cookie}=${value}`);
const flattenedJWSFromCookie = (cookie: string, value: string, signature: string): JWS.FlattenedJWS => ({
protected: Buffer.from(JSON.stringify(header))
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_'),
payload: getPayload(cookie, value),
signature
});
const generateSignature = (cookie: string, value: string, key: JWK.Key): string => {
const payload = getPayload(cookie, value);
return JWS.sign.flattened(payload, key, header).signature;
};
const verifySignature = (cookie: string, value: string, signature: string, keystore: JWKS.KeyStore): boolean => {
try {
return !!JWS.verify(flattenedJWSFromCookie(cookie, value, signature), keystore, {
algorithms: ['HS256'],
crit: ['b64']
});
} catch (err) {
return false;
}
};
const getCookieValue = (cookie: string, value: string, keystore: JWKS.KeyStore): string | undefined => {
if (!value) {
return undefined;
}
let signature;
[value, signature] = value.split('.');
if (verifySignature(cookie, value, signature, keystore)) {
return value;
}
return undefined;
};
export const generateCookieValue = (cookie: string, value: string, key: JWK.Key): string => {
const signature = generateSignature(cookie, value, key);
return `${value}.${signature}`;
};
export default class TransientStore {
private currentKey: JWK.Key | undefined;
private keyStore: JWKS.KeyStore;
constructor(private config: Config) {
let current;
const secret = config.secret;
const secrets = Array.isArray(secret) ? secret : [secret];
const keystore = new JWKS.KeyStore();
secrets.forEach((secretString, i) => {
const key = JWK.asKey(deriveKey(secretString));
if (i === 0) {
current = key;
}
keystore.add(key);
});
this.currentKey = current;
this.keyStore = keystore;
}
/**
* Set a cookie with a value or a generated nonce.
*
* @param {String} key Cookie name to use.
* @param {IncomingMessage} _req Server Request object.
* @param {ServerResponse} res Server Response object.
* @param {Object} opts Options object.
* @param {String} opts.sameSite SameSite attribute of "None," "Lax," or "Strict". Default is "None."
* @param {String} opts.value Cookie value. Omit this key to store a generated value.
*
* @return {String} Cookie value that was set.
*/
save(
key: string,
_req: IncomingMessage,
res: ServerResponse,
{ sameSite = 'none', value = this.generateNonce() }: StoreOptions
): string {
const isSameSiteNone = sameSite === 'none';
const { domain, path, secure } = this.config.session.cookie;
const basicAttr = {
httpOnly: true,
secure,
domain,
path
};
{
const cookieValue = generateCookieValue(key, value, this.currentKey as JWK.Key);
// Set the cookie with the SameSite attribute and, if needed, the Secure flag.
setCookie(res, key, cookieValue, {
...basicAttr,
sameSite,
secure: isSameSiteNone ? true : basicAttr.secure
});
}
if (isSameSiteNone && this.config.legacySameSiteCookie) {
const cookieValue = generateCookieValue(`_${key}`, value, this.currentKey as JWK.Key);
// Set the fallback cookie with no SameSite or Secure attributes.
setCookie(res, `_${key}`, cookieValue, basicAttr);
}
return value;
}
/**
* Get a cookie value then delete it.
*
* @param {String} key Cookie name to use.
* @param {IncomingMessage} req Express Request object.
* @param {ServerResponse} res Express Response object.
*
* @return {String|undefined} Cookie value or undefined if cookie was not found.
*/
read(key: string, req: IncomingMessage, res: ServerResponse): string | undefined {
const cookie = getCookie(req, key);
const { domain, path } = this.config.session.cookie;
let value = getCookieValue(key, cookie, this.keyStore);
clearCookie(res, key, { domain, path });
if (this.config.legacySameSiteCookie) {
const fallbackKey = `_${key}`;
if (!value) {
const fallbackCookie = getCookie(req, fallbackKey);
value = getCookieValue(fallbackKey, fallbackCookie, this.keyStore);
}
clearCookie(res, fallbackKey, { domain, path });
}
return value;
}
/**
* Generates a nonce value.
* @return {String}
*/
generateNonce(): string {
return generators.nonce();
}
/**
* Generates a code_verifier value.
* @return {String}
*/
generateCodeVerifier(): string {
return generators.codeVerifier();
}
/**
* Calculates a code_challenge value for a given codeVerifier
* @param {String} codeVerifier Code Verifier to calculate the code_challenge value from.
* @return {String}
*/
calculateCodeChallenge(codeVerifier: string): string {
return generators.codeChallenge(codeVerifier);
}
}