diff --git a/.changeset/single-metal-elephant.md b/.changeset/single-metal-elephant.md new file mode 100644 index 00000000000..ae257f5cc16 --- /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. Fix OIDC debug log modal not displaying the log area. diff --git a/packages/services/server/src/supertokens.ts b/packages/services/server/src/supertokens.ts index 5862ede7b00..4bfddbd65e8 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, @@ -405,6 +406,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..1c453796b61 --- /dev/null +++ b/packages/services/server/src/supertokens/oidc-provider.test.ts @@ -0,0 +1,144 @@ +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..0ef9684f5cd 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('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."; + } + + 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.'; +} + const fetchOIDCConfig = async ( internalApi: InternalApiCaller, logger: FastifyBaseLogger, 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} +
+ ); + }} + /> +