diff --git a/lib/resources/oidc.js b/lib/resources/oidc.js index 22a11cf21..ad598ac61 100644 --- a/lib/resources/oidc.js +++ b/lib/resources/oidc.js @@ -43,6 +43,7 @@ const ONE_HOUR = 60 * 60 * 1000; // * https://bugs.chromium.org/p/chromium/issues/detail?id=1056543 const CODE_VERIFIER_COOKIE = (HTTPS_ENABLED ? '__Secure-' : '') + 'ocv'; const NEXT_COOKIE = (HTTPS_ENABLED ? '__Secure-' : '') + 'next'; // eslint-disable-line no-multi-spaces +const STATE_COOKIE = (HTTPS_ENABLED ? '__Secure-' : '') + 'oidcstate'; const callbackCookieProps = { httpOnly: true, secure: HTTPS_ENABLED, @@ -102,15 +103,22 @@ module.exports = (service, endpoint) => { const client = await getClient(); const code_verifier = generators.codeVerifier(); // eslint-disable-line camelcase + // State is not strictly required because PKCE (code_challenge) protects us from CSRF attacks. + // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-countermeasures-4 + // We create a nonce (state) anyway because some IDPs (e.g. Authentik) will send the nonce + // back to us. If it doesn't explicitly get one, it sends back an empty string. + const state = generators.state(); const code_challenge = generators.codeChallenge(code_verifier); // eslint-disable-line camelcase const authUrl = client.authorizationUrl({ scope: SCOPES.join(' '), resource: `${envDomain}/v1`, + state, code_challenge, code_challenge_method: CODE_CHALLENGE_METHOD, }); + res.cookie(STATE_COOKIE, state, { ...callbackCookieProps, maxAge: ONE_HOUR }); res.cookie(CODE_VERIFIER_COOKIE, code_verifier, { ...callbackCookieProps, maxAge: ONE_HOUR }); const { next } = req.query; @@ -129,15 +137,17 @@ module.exports = (service, endpoint) => { service.get('/oidc/callback', endpoint.html(async (container, _, req, res) => { try { + const state = req.cookies[STATE_COOKIE]; const code_verifier = req.cookies[CODE_VERIFIER_COOKIE]; // eslint-disable-line camelcase const next = req.cookies[NEXT_COOKIE]; // eslint-disable-line no-multi-spaces + res.clearCookie(STATE_COOKIE, callbackCookieProps); res.clearCookie(CODE_VERIFIER_COOKIE, callbackCookieProps); res.clearCookie(NEXT_COOKIE, callbackCookieProps); // eslint-disable-line no-multi-spaces const client = await getClient(); const params = client.callbackParams(req); - const tokenSet = await client.callback(getRedirectUri(), params, { code_verifier }); + const tokenSet = await client.callback(getRedirectUri(), params, { state, code_verifier }); const { access_token } = tokenSet;