@zeushq/nextjs-zidentity
Version:
Next.js SDK for signin in with Zeus Identity
100 lines (85 loc) • 3.32 kB
text/typescript
import { IncomingMessage, ServerResponse } from 'http';
import urlJoin from 'url-join';
import { strict as assert } from 'assert';
import { Config, LoginOptions } from '../config';
import TransientStore, { StoreOptions } from '../transient-store';
import { encodeState } from '../hooks/get-login-state';
import { ClientFactory } from '../client';
import createDebug from '../utils/debug';
const debug = createDebug('handlers');
function getRedirectUri(config: Config): string {
return urlJoin(config.baseURL, config.routes.callback);
}
export type HandleLogin = (req: IncomingMessage, res: ServerResponse, options?: LoginOptions) => Promise<void>;
export default function loginHandlerFactory(
config: Config,
getClient: ClientFactory,
transientHandler: TransientStore
): HandleLogin {
return async (req, res, options = {}) => {
const client = await getClient();
const returnTo = options.returnTo || config.baseURL;
const opts = {
returnTo,
getLoginState: config.getLoginState,
...options
};
// Ensure a redirect_uri, merge in configuration options, then passed-in options.
opts.authorizationParams = {
redirect_uri: getRedirectUri(config),
...config.authorizationParams,
...(opts.authorizationParams || {})
};
const transientOpts: StoreOptions = {
sameSite: opts.authorizationParams.response_mode === 'form_post' ? 'none' : 'lax'
};
const stateValue = await opts.getLoginState(req as any, opts);
if (typeof stateValue !== 'object') {
throw new Error('Custom state value must be an object.');
}
stateValue.nonce = transientHandler.generateNonce();
stateValue.returnTo = stateValue.returnTo || opts.returnTo;
const usePKCE = (opts.authorizationParams.response_type as string).includes('code');
if (usePKCE) {
debug('response_type includes code, the authorization request will use PKCE');
stateValue.code_verifier = transientHandler.generateCodeVerifier();
}
const authParams = {
...opts.authorizationParams,
nonce: transientHandler.save('nonce', req, res, transientOpts),
state: transientHandler.save('state', req, res, {
...transientOpts,
value: encodeState(stateValue)
}),
...(opts.authorizationParams.signup ? {
signup: true
} : undefined),
...(usePKCE
? {
code_challenge: transientHandler.calculateCodeChallenge(
transientHandler.save('code_verifier', req, res, transientOpts)
),
code_challenge_method: 'S256'
}
: undefined)
};
const validResponseTypes = ['id_token', 'code id_token', 'code'];
assert(
validResponseTypes.includes(authParams.response_type as string),
`response_type should be one of ${validResponseTypes.join(', ')}`
);
assert(/\bopenid\b/.test(authParams.scope as string), 'scope should contain "openid"');
if (authParams.max_age) {
transientHandler.save('max_age', req, res, {
...transientOpts,
value: authParams.max_age.toString()
});
}
const authorizationUrl = client.authorizationUrl(authParams);
debug('redirecting to %s', authorizationUrl);
res.writeHead(302, {
Location: authorizationUrl
});
res.end();
};
}