Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/oauth-provider-config-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'seamless-auth-api': patch
---

Apply OAuthProviderConfigSchema defaults to providers configured via OAUTH_PROVIDERS. The
env value was parsed with a raw JSON.parse, so per-provider fields like subjectJsonPath and
emailJsonPath stayed undefined and OAuth profile extraction failed with a generic
"OAuth login failed". The OAuth callback now also logs the underlying error. Fixes #49.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Direct provider wiring currently lives in [src/config/directMessaging.ts](/Users
- Be careful around token response shapes and bearer auth. Browser-cookie auth mode has been removed.
- Preserve existing local worktree changes unless the user explicitly asks you to clean them up.
- Keep comments minimal. Comment only when the code genuinely needs explaining (a non-obvious reason or gotcha); do not narrate what the code plainly does.
- Do not use em dashes (—) in public-facing text: commit messages, code comments, PR/issue descriptions, changesets, and docs. Use a comma, parentheses, or a separate sentence instead.

## Before You Finish A Change

Expand Down
6 changes: 5 additions & 1 deletion src/controllers/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
verifyOAuthState,
} from '../services/oauthService.js';
import { issueSessionAndRespond } from '../services/sessionIssuance.js';
import getLogger from '../utils/logger.js';

const logger = getLogger('oauth');

function allowedReturnTo(value: string | undefined, origins: string[]) {
if (!value) return undefined;
Expand Down Expand Up @@ -152,7 +155,8 @@ export async function finishOAuthLogin(req: Request, res: Response) {
req,
res,
});
} catch {
} catch (error) {
logger.error(`OAuth callback failed for provider ${provider.id}: ${error}`);
await AuthEventService.log({
type: 'oauth_login_failed',
req,
Expand Down
8 changes: 8 additions & 0 deletions src/utils/parseEnvConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* See LICENSE file in the project root for full license information
*/

import { z } from 'zod';

import { SYSTEM_CONFIG_ENV_MAP } from '../config/systemConfig.envMap.js';
import { OAuthProviderConfigSchema } from '../schemas/systemConfig.schema.js';

export function parseSystemConfigEnvValue(key: keyof typeof SYSTEM_CONFIG_ENV_MAP, raw: string) {
switch (key) {
Expand All @@ -18,6 +21,11 @@ export function parseSystemConfigEnvValue(key: keyof typeof SYSTEM_CONFIG_ENV_MA
.filter(Boolean);

case 'oauth_providers':
// Validate through the schema so per-provider defaults (subjectJsonPath,
// emailJsonPath, ...) are applied; raw JSON.parse leaves them undefined and
// OAuth profile extraction then silently fails.
return z.array(OAuthProviderConfigSchema).parse(JSON.parse(raw));

case 'lockout_policy':
return JSON.parse(raw);

Expand Down
34 changes: 34 additions & 0 deletions tests/unit/utils/parseSystemConfigEnvValue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,40 @@ describe('parseSystemConfigEnvValue', () => {
});
});

describe('oauth_providers parsing', () => {
it('applies per-provider schema defaults (e.g. subjectJsonPath)', () => {
const raw = JSON.stringify([
{
id: 'mock',
name: 'Mock',
clientId: 'client',
clientSecretEnv: 'MOCK_SECRET',
authorizationUrl: 'https://idp.test/authorize',
tokenUrl: 'https://idp.test/token',
userInfoUrl: 'https://idp.test/userinfo',
},
]);

const result = parseSystemConfigEnvValue('oauth_providers', raw) as Array<
Record<string, unknown>
>;

expect(result[0]).toMatchObject({
enabled: true,
subjectJsonPath: 'sub',
emailJsonPath: 'email',
emailVerifiedJsonPath: 'email_verified',
scopes: [],
allowSignup: true,
accountLinking: 'email',
});
});

it('throws on an invalid provider entry', () => {
expect(() => parseSystemConfigEnvValue('oauth_providers', '[{"id":"x"}]')).toThrow();
});
});

describe('invalid key', () => {
it('throws for unknown key', () => {
expect(() => parseSystemConfigEnvValue('invalid_key' as any, 'value')).toThrow(
Expand Down
Loading