sso: normalize provider config via discovery

This commit is contained in:
Arunavo Ray
2025-10-22 16:33:33 +05:30
parent e4e54722cf
commit 847823bbf8
6 changed files with 507 additions and 68 deletions

View File

@@ -4,6 +4,7 @@ import { requireAuth } from "@/lib/utils/auth-helpers";
import { db, ssoProviders } from "@/lib/db";
import { nanoid } from "nanoid";
import { eq } from "drizzle-orm";
import { normalizeOidcProviderConfig, OidcConfigError, type RawOidcConfig } from "@/lib/sso/oidc-config";
// GET /api/sso/providers - List all SSO providers
export async function GET(context: APIContext) {
@@ -45,10 +46,12 @@ export async function POST(context: APIContext) {
tokenEndpoint,
jwksEndpoint,
userInfoEndpoint,
discoveryEndpoint,
mapping,
providerId,
organizationId,
scopes,
pkce,
} = body;
// Validate required fields
@@ -79,22 +82,51 @@ export async function POST(context: APIContext) {
);
}
// Create OIDC config object
const oidcConfig = {
clientId,
clientSecret,
authorizationEndpoint,
tokenEndpoint,
jwksEndpoint,
userInfoEndpoint,
scopes: scopes || ["openid", "email", "profile"],
mapping: mapping || {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
// Clean issuer URL (remove trailing slash); validate format
let cleanIssuer = issuer;
try {
const issuerUrl = new URL(issuer.toString().trim());
cleanIssuer = issuerUrl.toString().replace(/\/$/, "");
} catch {
return new Response(
JSON.stringify({ error: `Invalid issuer URL format: ${issuer}` }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
let normalized;
try {
normalized = await normalizeOidcProviderConfig(cleanIssuer, {
clientId,
clientSecret,
authorizationEndpoint,
tokenEndpoint,
jwksEndpoint,
userInfoEndpoint,
discoveryEndpoint,
scopes,
pkce,
mapping,
});
} catch (error) {
if (error instanceof OidcConfigError) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
throw error;
}
const storedOidcConfig = {
...normalized.oidcConfig,
mapping: normalized.mapping,
};
// Insert new provider
@@ -102,9 +134,9 @@ export async function POST(context: APIContext) {
.insert(ssoProviders)
.values({
id: nanoid(),
issuer,
issuer: cleanIssuer,
domain,
oidcConfig: JSON.stringify(oidcConfig),
oidcConfig: JSON.stringify(storedOidcConfig),
userId: user.id,
providerId,
organizationId,
@@ -156,7 +188,9 @@ export async function PUT(context: APIContext) {
tokenEndpoint,
jwksEndpoint,
userInfoEndpoint,
discoveryEndpoint,
scopes,
pkce,
organizationId,
} = body;
@@ -179,26 +213,62 @@ export async function PUT(context: APIContext) {
// Parse existing config
const existingConfig = JSON.parse(existingProvider.oidcConfig);
const effectiveIssuer = issuer || existingProvider.issuer;
// Create updated OIDC config
const updatedOidcConfig = {
...existingConfig,
clientId: clientId || existingConfig.clientId,
clientSecret: clientSecret || existingConfig.clientSecret,
authorizationEndpoint: authorizationEndpoint || existingConfig.authorizationEndpoint,
tokenEndpoint: tokenEndpoint || existingConfig.tokenEndpoint,
jwksEndpoint: jwksEndpoint || existingConfig.jwksEndpoint,
userInfoEndpoint: userInfoEndpoint || existingConfig.userInfoEndpoint,
scopes: scopes || existingConfig.scopes || ["openid", "email", "profile"],
let cleanIssuer = effectiveIssuer;
try {
const issuerUrl = new URL(effectiveIssuer.toString().trim());
cleanIssuer = issuerUrl.toString().replace(/\/$/, "");
} catch {
return new Response(
JSON.stringify({ error: `Invalid issuer URL format: ${effectiveIssuer}` }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const mergedConfig: RawOidcConfig = {
clientId: clientId ?? existingConfig.clientId,
clientSecret: clientSecret ?? existingConfig.clientSecret,
authorizationEndpoint: authorizationEndpoint ?? existingConfig.authorizationEndpoint,
tokenEndpoint: tokenEndpoint ?? existingConfig.tokenEndpoint,
jwksEndpoint: jwksEndpoint ?? existingConfig.jwksEndpoint,
userInfoEndpoint: userInfoEndpoint ?? existingConfig.userInfoEndpoint,
discoveryEndpoint: discoveryEndpoint ?? existingConfig.discoveryEndpoint,
scopes: scopes ?? existingConfig.scopes,
pkce: pkce ?? existingConfig.pkce,
mapping: existingConfig.mapping,
};
let normalized;
try {
normalized = await normalizeOidcProviderConfig(cleanIssuer, mergedConfig);
} catch (error) {
if (error instanceof OidcConfigError) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
throw error;
}
const storedOidcConfig = {
...normalized.oidcConfig,
mapping: normalized.mapping,
};
// Update provider
const [updatedProvider] = await db
.update(ssoProviders)
.set({
issuer: issuer || existingProvider.issuer,
issuer: cleanIssuer,
domain: domain || existingProvider.domain,
oidcConfig: JSON.stringify(updatedOidcConfig),
oidcConfig: JSON.stringify(storedOidcConfig),
organizationId: organizationId !== undefined ? organizationId : existingProvider.organizationId,
updatedAt: new Date(),
})
@@ -259,4 +329,4 @@ export async function DELETE(context: APIContext) {
} catch (error) {
return createSecureErrorResponse(error, "SSO providers API");
}
}
}