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 469d4462..7eb375f6 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 c54b2095..b7bd837c 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 6dbeddd1..9c449a51 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 173e104e..8e6f0e21 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',