From 7e6bdab78b097196758c31f87af95b862eb67a37 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Tue, 10 Feb 2026 15:38:58 +0100 Subject: [PATCH 1/8] fix(server): handle OIDC token exchange errors gracefully instead of returning 500 --- packages/services/server/src/supertokens.ts | 10 +- .../src/supertokens/oidc-provider.test.ts | 137 ++++++++++++++++++ .../server/src/supertokens/oidc-provider.ts | 60 ++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 packages/services/server/src/supertokens/oidc-provider.test.ts diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts index 5862ede7b00..5432612cafa 100644 --- a/packages/services/server/src/supertokens.ts +++ b/packages/services/server/src/supertokens.ts @@ -16,6 +16,7 @@ import { createInternalApiCaller } from './api'; import { env } from './environment'; import { createOIDCSuperTokensProvider, + describeOIDCSignInError, getLoggerFromUserContext, getOIDCSuperTokensOverrides, type BroadcastOIDCIntegrationLog, @@ -368,7 +369,6 @@ const getEnsureUserOverrides = ( } return null; } - try { const response = await originalImplementation.thirdPartySignInUpPOST(input); @@ -405,6 +405,14 @@ const getEnsureUserOverrides = ( reason: e.reason, }; } + if (input.provider.id === 'oidc') { + const logger = getLoggerFromUserContext(input.userContext); + logger.error(e, 'OIDC sign-in/sign-up failed'); + return { + status: 'GENERAL_ERROR' as const, + message: describeOIDCSignInError(e), + }; + } throw e; } }, diff --git a/packages/services/server/src/supertokens/oidc-provider.test.ts b/packages/services/server/src/supertokens/oidc-provider.test.ts new file mode 100644 index 00000000000..2ac8656aa1c --- /dev/null +++ b/packages/services/server/src/supertokens/oidc-provider.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test } from 'vitest'; +import { describeOIDCSignInError } from './oidc-provider'; + +describe('describeOIDCSignInError', () => { + test('invalid_client error (e.g. expired client secret)', () => { + const error = new Error( + 'Received response with status 401 and body {"error":"invalid_client","error_description":"AAD2SKSASFSLKAF: The provided client secret keys for app \'369sdsds1-8513-4ssa-ae64-292942jsjs\' are expired."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('invalid client credentials'); + expect(message).toContain('client secret has expired'); + expect(message).not.toContain('AAD2SKSASFSLKAF'); + expect(message).not.toContain('369sdsds1-8513-4ssa-ae64-292942jsjs'); + }); + + test('invalid_grant error (e.g. expired authorization code)', () => { + const error = new Error( + 'Received response with status 400 and body {"error":"invalid_grant","error_description":"The authorization code has expired."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('authorization code has expired'); + expect(message).toContain('try signing in again'); + }); + + test('unauthorized_client error', () => { + const error = new Error( + 'Received response with status 403 and body {"error":"unauthorized_client","error_description":"The client is not authorized."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('rejected the client authorization'); + expect(message).toContain('OIDC integration configuration'); + }); + + test('invalid_request error', () => { + const error = new Error( + 'Received response with status 400 and body {"error":"invalid_request","error_description":"The request is missing a required parameter."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('rejected the token request as malformed'); + expect(message).toContain('token endpoint URL'); + }); + + test('unsupported_grant_type error', () => { + const error = new Error( + 'Received response with status 400 and body {"error":"unsupported_grant_type","error_description":"The authorization grant type is not supported."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('does not support the authorization code grant type'); + }); + + test('invalid_scope error', () => { + const error = new Error( + 'Received response with status 400 and body {"error":"invalid_scope","error_description":"The requested scope is invalid."}', + ); + const message = describeOIDCSignInError(error); + expect(message).toContain('rejected the requested scopes'); + expect(message).toContain('additional scopes'); + }); + + test('network error: ECONNREFUSED', () => { + const error = new Error('request to https://login.example.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:443'); + const message = describeOIDCSignInError(error); + expect(message).toContain('Could not connect'); + expect(message).toContain('endpoint URLs'); + }); + + test('network error: ENOTFOUND', () => { + const error = new Error('request to https://nonexistent.example.com/token failed, reason: getaddrinfo ENOTFOUND nonexistent.example.com'); + const message = describeOIDCSignInError(error); + expect(message).toContain('Could not connect'); + }); + + test('network error: ETIMEDOUT', () => { + const error = new Error('request to https://slow.example.com/token failed, reason: connect ETIMEDOUT'); + const message = describeOIDCSignInError(error); + expect(message).toContain('Could not connect'); + }); + + test('network error: fetch failed', () => { + const error = new TypeError('fetch failed'); + const message = describeOIDCSignInError(error); + expect(message).toContain('Could not connect'); + }); + + test('OIDC integration not found', () => { + const error = new Error('Could not find OIDC integration.'); + const message = describeOIDCSignInError(error); + expect(message).toContain('could not be found'); + expect(message).toContain('contact your organization administrator'); + }); + + test('userinfo endpoint returned non-200 status', () => { + const error = new Error("Received invalid status code. Could not retrieve user's profile info."); + const message = describeOIDCSignInError(error); + expect(message).toContain('user info endpoint returned an error'); + expect(message).toContain('verify the user info endpoint URL'); + }); + + test('userinfo endpoint returned non-JSON response', () => { + const error = new Error('Could not parse JSON response.'); + const message = describeOIDCSignInError(error); + expect(message).toContain('returned an invalid response'); + expect(message).toContain('verify the user info endpoint URL'); + }); + + test('userinfo endpoint missing required fields (sub, email)', () => { + const error = new Error('Could not parse profile info.'); + const message = describeOIDCSignInError(error); + expect(message).toContain('did not return the required fields'); + expect(message).toContain('sub, email'); + }); + + test('unknown error returns generic message', () => { + const error = new Error('Something completely unexpected happened'); + const message = describeOIDCSignInError(error); + expect(message).toContain('unexpected error'); + expect(message).toContain('verify your OIDC integration configuration'); + }); + + test('non-Error value is handled', () => { + const message = describeOIDCSignInError('string error with invalid_client'); + expect(message).toContain('invalid client credentials'); + }); + + test('no sensitive information is leaked in any branch', () => { + const sensitiveError = new Error( + 'Received response with status 401 and body {"error":"invalid_client","error_description":"AADAASJAD213122: The provided client secret keys for app \'3693bbf1-8513-4cda-ae64-77e3ca237f17\' are expired. Visit the Azure portal to create new keys for your app: https://aka.ms/NewClientSecret Trace ID: b8b7152f-4489-46ed-8b78-11ad45520300 Correlation ID: 45c48f07-0191-431d-8a19-ba8319a7cd18"}', + ); + const message = describeOIDCSignInError(sensitiveError); + expect(message).not.toContain('3693bbf1'); + expect(message).not.toContain('b8b7152f'); + expect(message).not.toContain('45c48f07'); + expect(message).not.toContain('aka.ms'); + expect(message).not.toContain('Trace ID'); + expect(message).not.toContain('Correlation ID'); + }); +}); diff --git a/packages/services/server/src/supertokens/oidc-provider.ts b/packages/services/server/src/supertokens/oidc-provider.ts index e930e1f5f50..d55ed6837b5 100644 --- a/packages/services/server/src/supertokens/oidc-provider.ts +++ b/packages/services/server/src/supertokens/oidc-provider.ts @@ -303,6 +303,66 @@ async function getOIDCConfigFromInput( return resolvedConfig; } +/** + * Classify an OIDC sign-in error into a user-safe description. + * Avoids leaking sensitive details (app IDs, trace IDs, internal URLs) + * while still pointing administrators toward the likely cause. + */ +export function describeOIDCSignInError(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + + if (message.includes('invalid_client')) { + return 'Authentication with your OIDC provider failed due to invalid client credentials. This commonly happens when the client secret has expired or the client ID is incorrect. Please review your OIDC integration settings.'; + } + + if (message.includes('invalid_grant')) { + return 'The authorization could not be completed. This can happen if the authorization code has expired. Please try signing in again.'; + } + + if (message.includes('unauthorized_client')) { + return 'Your OIDC provider rejected the client authorization. Please verify your OIDC integration configuration.'; + } + + if (message.includes('invalid_request')) { + return 'Your OIDC provider rejected the token request as malformed. This may indicate a misconfigured token endpoint URL. Please review your OIDC integration settings.'; + } + + if (message.includes('unsupported_grant_type')) { + return 'Your OIDC provider does not support the authorization code grant type. Please verify the provider supports the OAuth 2.0 authorization code flow.'; + } + + if (message.includes('invalid_scope')) { + return 'Your OIDC provider rejected the requested scopes. Please review the additional scopes configured in your OIDC integration settings.'; + } + + if ( + message.includes('ECONNREFUSED') || + message.includes('ENOTFOUND') || + message.includes('ETIMEDOUT') || + message.includes('fetch failed') + ) { + return "Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible."; + } + + if (message.includes('Could not find OIDC integration')) { + return 'The OIDC integration could not be found. It may have been removed or misconfigured. Please contact your organization administrator.'; + } + + if (message.includes("Could not retrieve user's profile info")) { + return "Your OIDC provider's user info endpoint returned an error. Please verify the user info endpoint URL in your OIDC integration settings is correct."; + } + + if (message.includes('Could not parse JSON response')) { + return "Your OIDC provider's user info endpoint returned an invalid response. Please verify the user info endpoint URL in your OIDC integration settings is correct."; + } + + if (message.includes('Could not parse profile info')) { + return "Your OIDC provider's user info endpoint did not return the required fields (sub, email). Please verify your OIDC provider is configured to include these claims."; + } + + return 'An unexpected error occurred while authenticating with your OIDC provider. Please verify your OIDC integration configuration or contact your administrator.'; +} + const fetchOIDCConfig = async ( internalApi: InternalApiCaller, logger: FastifyBaseLogger, From f014eed63828b29e5dad753b79a5ae451504c424 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Tue, 10 Feb 2026 15:44:08 +0100 Subject: [PATCH 2/8] pnpm prettier --- .../server/src/supertokens/oidc-provider.test.ts | 16 ++++++++++++---- .../server/src/supertokens/oidc-provider.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/services/server/src/supertokens/oidc-provider.test.ts b/packages/services/server/src/supertokens/oidc-provider.test.ts index 2ac8656aa1c..d66813fab32 100644 --- a/packages/services/server/src/supertokens/oidc-provider.test.ts +++ b/packages/services/server/src/supertokens/oidc-provider.test.ts @@ -58,20 +58,26 @@ describe('describeOIDCSignInError', () => { }); test('network error: ECONNREFUSED', () => { - const error = new Error('request to https://login.example.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:443'); + const error = new Error( + 'request to https://login.example.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:443', + ); const message = describeOIDCSignInError(error); expect(message).toContain('Could not connect'); expect(message).toContain('endpoint URLs'); }); test('network error: ENOTFOUND', () => { - const error = new Error('request to https://nonexistent.example.com/token failed, reason: getaddrinfo ENOTFOUND nonexistent.example.com'); + const error = new Error( + 'request to https://nonexistent.example.com/token failed, reason: getaddrinfo ENOTFOUND nonexistent.example.com', + ); const message = describeOIDCSignInError(error); expect(message).toContain('Could not connect'); }); test('network error: ETIMEDOUT', () => { - const error = new Error('request to https://slow.example.com/token failed, reason: connect ETIMEDOUT'); + const error = new Error( + 'request to https://slow.example.com/token failed, reason: connect ETIMEDOUT', + ); const message = describeOIDCSignInError(error); expect(message).toContain('Could not connect'); }); @@ -90,7 +96,9 @@ describe('describeOIDCSignInError', () => { }); test('userinfo endpoint returned non-200 status', () => { - const error = new Error("Received invalid status code. Could not retrieve user's profile info."); + const error = new Error( + "Received invalid status code. Could not retrieve user's profile info.", + ); const message = describeOIDCSignInError(error); expect(message).toContain('user info endpoint returned an error'); expect(message).toContain('verify the user info endpoint URL'); diff --git a/packages/services/server/src/supertokens/oidc-provider.ts b/packages/services/server/src/supertokens/oidc-provider.ts index d55ed6837b5..f98a4c804aa 100644 --- a/packages/services/server/src/supertokens/oidc-provider.ts +++ b/packages/services/server/src/supertokens/oidc-provider.ts @@ -341,7 +341,7 @@ export function describeOIDCSignInError(error: unknown): string { message.includes('ETIMEDOUT') || message.includes('fetch failed') ) { - return "Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible."; + return 'Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible.'; } if (message.includes('Could not find OIDC integration')) { From ba4c2e27aeeb12c98d7ec7cf74de53821f33f1d9 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Tue, 10 Feb 2026 15:44:19 +0100 Subject: [PATCH 3/8] changeset --- .changeset/single-metal-elephant.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/single-metal-elephant.md diff --git a/.changeset/single-metal-elephant.md b/.changeset/single-metal-elephant.md new file mode 100644 index 00000000000..04a93e5341c --- /dev/null +++ b/.changeset/single-metal-elephant.md @@ -0,0 +1,5 @@ +--- +'hive': patch +--- + +Handle OIDC token exchange errors gracefully instead of returning 500. Classifies OAuth 2.0 error codes into user-safe messages without leaking sensitive provider details. From 259812881fc34a09e55fdac90a1256b7bea32437 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Tue, 10 Feb 2026 15:48:43 +0100 Subject: [PATCH 4/8] slight change --- .../server/src/supertokens/oidc-provider.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/services/server/src/supertokens/oidc-provider.ts b/packages/services/server/src/supertokens/oidc-provider.ts index f98a4c804aa..0ef9684f5cd 100644 --- a/packages/services/server/src/supertokens/oidc-provider.ts +++ b/packages/services/server/src/supertokens/oidc-provider.ts @@ -335,15 +335,6 @@ export function describeOIDCSignInError(error: unknown): string { return 'Your OIDC provider rejected the requested scopes. Please review the additional scopes configured in your OIDC integration settings.'; } - if ( - message.includes('ECONNREFUSED') || - message.includes('ENOTFOUND') || - message.includes('ETIMEDOUT') || - message.includes('fetch failed') - ) { - return 'Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible.'; - } - if (message.includes('Could not find OIDC integration')) { return 'The OIDC integration could not be found. It may have been removed or misconfigured. Please contact your organization administrator.'; } @@ -360,6 +351,15 @@ export function describeOIDCSignInError(error: unknown): string { return "Your OIDC provider's user info endpoint did not return the required fields (sub, email). Please verify your OIDC provider is configured to include these claims."; } + if ( + message.includes('ECONNREFUSED') || + message.includes('ENOTFOUND') || + message.includes('ETIMEDOUT') || + message.includes('fetch failed') + ) { + return 'Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible.'; + } + return 'An unexpected error occurred while authenticating with your OIDC provider. Please verify your OIDC integration configuration or contact your administrator.'; } From 5e0b26314e462d76286da694aad6a1d7c4211364 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Tue, 10 Feb 2026 16:03:47 +0100 Subject: [PATCH 5/8] fix lint --- packages/services/server/src/supertokens/oidc-provider.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/services/server/src/supertokens/oidc-provider.test.ts b/packages/services/server/src/supertokens/oidc-provider.test.ts index d66813fab32..1c453796b61 100644 --- a/packages/services/server/src/supertokens/oidc-provider.test.ts +++ b/packages/services/server/src/supertokens/oidc-provider.test.ts @@ -1,4 +1,3 @@ -import { describe, expect, test } from 'vitest'; import { describeOIDCSignInError } from './oidc-provider'; describe('describeOIDCSignInError', () => { From 94a56a651e153c57d87edd28367349c5d4b1c927 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Thu, 12 Feb 2026 16:15:27 +0100 Subject: [PATCH 6/8] fix debug modal --- packages/services/server/src/supertokens.ts | 1 + .../settings/oidc-integration-section.tsx | 36 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts index 5432612cafa..d043969a47e 100644 --- a/packages/services/server/src/supertokens.ts +++ b/packages/services/server/src/supertokens.ts @@ -369,6 +369,7 @@ const getEnsureUserOverrides = ( } return null; } + try { const response = await originalImplementation.thirdPartySignInUpPOST(input); diff --git a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx index 90d5deb738d..95fe2859f0d 100644 --- a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx +++ b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx @@ -1467,23 +1467,25 @@ function DebugOIDCIntegrationModal(props: { Here you can listen to the live logs for debugging your OIDC integration. - { - return ( -
- - {logRow.message} -
- ); - }} - /> +
+ { + return ( +
+ + {logRow.message} +
+ ); + }} + /> +