From 9aaaca1b8d9c34f2587c361abeae7a13e3392331 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 15 Jul 2024 18:49:06 +0200 Subject: [PATCH] feat: Support OIDC signed UserInfo responses Some OIDC providers support signed UserInfo response, to enhance security. The OIDC client should be free to ask for the user info sgnature, however in certain situations (e.g egov applications) where security matters, the OIDC providers might chose to enforce this sugnature. Planka was not supported signed UserInfo response, which resulted in an misleading exception 'invalidCodeOrNonce'. Introduce the proper configurations to parametrize the OIDC client, and a dedicated exception to improve the developer experience. Specifications: "The UserInfo Claims MUST be returned as the members of a JSON object unless a signed or encrypted response was requested during Client Registration." --- .../api/controllers/access-tokens/exchange-using-oidc.js | 7 +++++++ server/api/helpers/users/get-or-create-one-using-oidc.js | 5 +++++ server/api/hooks/oidc/index.js | 1 + server/config/custom.js | 1 + 4 files changed, 14 insertions(+) diff --git a/server/api/controllers/access-tokens/exchange-using-oidc.js b/server/api/controllers/access-tokens/exchange-using-oidc.js index 469d446..7eb375f 100644 --- a/server/api/controllers/access-tokens/exchange-using-oidc.js +++ b/server/api/controllers/access-tokens/exchange-using-oidc.js @@ -13,6 +13,9 @@ const Errors = { MISSING_VALUES: { missingValues: 'Unable to retrieve required values (email, name)', }, + INVALID_USERINFO_SIGNATURE: { + invalidUserInfoSignature: "Invalid signature on userInfo due to client misconfiguration" + } }; module.exports = { @@ -40,6 +43,9 @@ module.exports = { missingValues: { responseType: 'unprocessableEntity', }, + invalidUserInfoSignature: { + responseType: 'unauthorized', + }, }, async fn(inputs) { @@ -51,6 +57,7 @@ module.exports = { sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`); return Errors.INVALID_CODE_OR_NONCE; }) + .intercept('invalidUserInfoSignature', () => Errors.INVALID_USERINFO_SIGNATURE) .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE) .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE) .intercept('missingValues', () => Errors.MISSING_VALUES); diff --git a/server/api/helpers/users/get-or-create-one-using-oidc.js b/server/api/helpers/users/get-or-create-one-using-oidc.js index c54b209..b7bd837 100644 --- a/server/api/helpers/users/get-or-create-one-using-oidc.js +++ b/server/api/helpers/users/get-or-create-one-using-oidc.js @@ -11,6 +11,7 @@ module.exports = { }, exits: { + invalidUserInfoSignature: {}, invalidCodeOrNonce: {}, missingValues: {}, emailAlreadyInUse: {}, @@ -34,6 +35,10 @@ module.exports = { ); userInfo = await client.userinfo(tokenSet); } catch (e) { + if (e instanceof SyntaxError && e.message.includes('Unexpected token e in JSON at position 0')) { + sails.log.warn('Error while exchanging OIDC code: userInfo response is signed.'); + throw 'invalidUserInfoSignature'; + } sails.log.warn(`Error while exchanging OIDC code: ${e}`); throw 'invalidCodeOrNonce'; } diff --git a/server/api/hooks/oidc/index.js b/server/api/hooks/oidc/index.js index 6dbeddd..9c449a5 100644 --- a/server/api/hooks/oidc/index.js +++ b/server/api/hooks/oidc/index.js @@ -30,6 +30,7 @@ module.exports = function defineOidcHook(sails) { client_secret: sails.config.custom.oidcClientSecret, redirect_uris: [sails.config.custom.oidcRedirectUri], response_types: ['code'], + userinfo_signed_response_alg: sails.config.custom.oidcUserinfoSignedResponseAlg, }); }, diff --git a/server/config/custom.js b/server/config/custom.js index 173e104..8e6f0e2 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -39,6 +39,7 @@ module.exports.custom = { oidcIssuer: process.env.OIDC_ISSUER, oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientSecret: process.env.OIDC_CLIENT_SECRET, + oidcUserinfoSignedResponseAlg: process.env.OIDC_USERINFO_SIGNED_RESPONSE_ALG, oidcScopes: process.env.OIDC_SCOPES || 'openid email profile', oidcResponseMode: process.env.OIDC_RESPONSE_MODE || 'fragment', oidcDefaultResponseMode: process.env.OIDC_DEFAULT_RESPONSE_MODE === 'true',