@@ -6,7 +6,7 @@ import { hasSSOAccess } from '@/lib/billing'
66import { env } from '@/lib/core/config/env'
77import { REDACTED_MARKER } from '@/lib/core/security/redaction'
88
9- const logger = createLogger ( 'SSO-Register ' )
9+ const logger = createLogger ( 'SSORegisterRoute ' )
1010
1111const mappingSchema = z
1212 . object ( {
@@ -43,6 +43,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
4343 ] )
4444 . default ( [ 'openid' , 'profile' , 'email' ] ) ,
4545 pkce : z . boolean ( ) . default ( true ) ,
46+ authorizationEndpoint : z . string ( ) . url ( ) . optional ( ) ,
47+ tokenEndpoint : z . string ( ) . url ( ) . optional ( ) ,
48+ userInfoEndpoint : z . string ( ) . url ( ) . optional ( ) ,
49+ jwksEndpoint : z . string ( ) . url ( ) . optional ( ) ,
4650 } ) ,
4751 z . object ( {
4852 providerType : z . literal ( 'saml' ) ,
@@ -64,12 +68,10 @@ const ssoRegistrationSchema = z.discriminatedUnion('providerType', [
6468
6569export async function POST ( request : NextRequest ) {
6670 try {
67- // SSO plugin must be enabled in Better Auth
6871 if ( ! env . SSO_ENABLED ) {
6972 return NextResponse . json ( { error : 'SSO is not enabled' } , { status : 400 } )
7073 }
7174
72- // Check plan access (enterprise) or env var override
7375 const session = await getSession ( )
7476 if ( ! session ?. user ?. id ) {
7577 return NextResponse . json ( { error : 'Authentication required' } , { status : 401 } )
@@ -116,7 +118,16 @@ export async function POST(request: NextRequest) {
116118 }
117119
118120 if ( providerType === 'oidc' ) {
119- const { clientId, clientSecret, scopes, pkce } = body
121+ const {
122+ clientId,
123+ clientSecret,
124+ scopes,
125+ pkce,
126+ authorizationEndpoint,
127+ tokenEndpoint,
128+ userInfoEndpoint,
129+ jwksEndpoint,
130+ } = body
120131
121132 const oidcConfig : any = {
122133 clientId,
@@ -127,48 +138,102 @@ export async function POST(request: NextRequest) {
127138 pkce : pkce ?? true ,
128139 }
129140
130- // Add manual endpoints for providers that might need them
131- // Common patterns for OIDC providers that don't support discovery properly
141+ oidcConfig . authorizationEndpoint = authorizationEndpoint
142+ oidcConfig . tokenEndpoint = tokenEndpoint
143+ oidcConfig . userInfoEndpoint = userInfoEndpoint
144+ oidcConfig . jwksEndpoint = jwksEndpoint
145+
146+ const needsDiscovery =
147+ ! oidcConfig . authorizationEndpoint || ! oidcConfig . tokenEndpoint || ! oidcConfig . jwksEndpoint
148+
149+ if ( needsDiscovery ) {
150+ const discoveryUrl = `${ issuer . replace ( / \/ $ / , '' ) } /.well-known/openid-configuration`
151+ try {
152+ logger . info ( 'Fetching OIDC discovery document for missing endpoints' , {
153+ discoveryUrl,
154+ hasAuthEndpoint : ! ! oidcConfig . authorizationEndpoint ,
155+ hasTokenEndpoint : ! ! oidcConfig . tokenEndpoint ,
156+ hasJwksEndpoint : ! ! oidcConfig . jwksEndpoint ,
157+ } )
158+
159+ const discoveryResponse = await fetch ( discoveryUrl , {
160+ headers : { Accept : 'application/json' } ,
161+ } )
162+
163+ if ( ! discoveryResponse . ok ) {
164+ logger . error ( 'Failed to fetch OIDC discovery document' , {
165+ status : discoveryResponse . status ,
166+ statusText : discoveryResponse . statusText ,
167+ } )
168+ return NextResponse . json (
169+ {
170+ error : `Failed to fetch OIDC discovery document from ${ discoveryUrl } . Status: ${ discoveryResponse . status } . Provide all endpoints explicitly or verify the issuer URL.` ,
171+ } ,
172+ { status : 400 }
173+ )
174+ }
175+
176+ const discovery = await discoveryResponse . json ( )
177+
178+ oidcConfig . authorizationEndpoint =
179+ oidcConfig . authorizationEndpoint || discovery . authorization_endpoint
180+ oidcConfig . tokenEndpoint = oidcConfig . tokenEndpoint || discovery . token_endpoint
181+ oidcConfig . userInfoEndpoint = oidcConfig . userInfoEndpoint || discovery . userinfo_endpoint
182+ oidcConfig . jwksEndpoint = oidcConfig . jwksEndpoint || discovery . jwks_uri
183+
184+ logger . info ( 'Merged OIDC endpoints (user-provided + discovery)' , {
185+ providerId,
186+ issuer,
187+ authorizationEndpoint : oidcConfig . authorizationEndpoint ,
188+ tokenEndpoint : oidcConfig . tokenEndpoint ,
189+ userInfoEndpoint : oidcConfig . userInfoEndpoint ,
190+ jwksEndpoint : oidcConfig . jwksEndpoint ,
191+ } )
192+ } catch ( error ) {
193+ logger . error ( 'Error fetching OIDC discovery document' , {
194+ error : error instanceof Error ? error . message : 'Unknown error' ,
195+ discoveryUrl,
196+ } )
197+ return NextResponse . json (
198+ {
199+ error : `Failed to fetch OIDC discovery document from ${ discoveryUrl } . Please verify the issuer URL is correct or provide all endpoints explicitly.` ,
200+ } ,
201+ { status : 400 }
202+ )
203+ }
204+ } else {
205+ logger . info ( 'Using explicitly provided OIDC endpoints (all present)' , {
206+ providerId,
207+ issuer,
208+ authorizationEndpoint : oidcConfig . authorizationEndpoint ,
209+ tokenEndpoint : oidcConfig . tokenEndpoint ,
210+ userInfoEndpoint : oidcConfig . userInfoEndpoint ,
211+ jwksEndpoint : oidcConfig . jwksEndpoint ,
212+ } )
213+ }
214+
132215 if (
133- issuer . includes ( 'okta.com' ) ||
134- issuer . includes ( 'auth0.com' ) ||
135- issuer . includes ( 'identityserver' )
216+ ! oidcConfig . authorizationEndpoint ||
217+ ! oidcConfig . tokenEndpoint ||
218+ ! oidcConfig . jwksEndpoint
136219 ) {
137- const baseUrl = issuer . includes ( '/oauth2/default' )
138- ? issuer . replace ( '/oauth2/default' , '' )
139- : issuer . replace ( '/oauth' , '' ) . replace ( '/v2.0' , '' ) . replace ( '/oauth2' , '' )
140-
141- // Okta-style endpoints
142- if ( issuer . includes ( 'okta.com' ) ) {
143- oidcConfig . authorizationEndpoint = `${ baseUrl } /oauth2/default/v1/authorize`
144- oidcConfig . tokenEndpoint = `${ baseUrl } /oauth2/default/v1/token`
145- oidcConfig . userInfoEndpoint = `${ baseUrl } /oauth2/default/v1/userinfo`
146- oidcConfig . jwksEndpoint = `${ baseUrl } /oauth2/default/v1/keys`
147- }
148- // Auth0-style endpoints
149- else if ( issuer . includes ( 'auth0.com' ) ) {
150- oidcConfig . authorizationEndpoint = `${ baseUrl } /authorize`
151- oidcConfig . tokenEndpoint = `${ baseUrl } /oauth/token`
152- oidcConfig . userInfoEndpoint = `${ baseUrl } /userinfo`
153- oidcConfig . jwksEndpoint = `${ baseUrl } /.well-known/jwks.json`
154- }
155- // Generic OIDC endpoints (IdentityServer, etc.)
156- else {
157- oidcConfig . authorizationEndpoint = `${ baseUrl } /connect/authorize`
158- oidcConfig . tokenEndpoint = `${ baseUrl } /connect/token`
159- oidcConfig . userInfoEndpoint = `${ baseUrl } /connect/userinfo`
160- oidcConfig . jwksEndpoint = `${ baseUrl } /.well-known/jwks`
161- }
220+ const missing : string [ ] = [ ]
221+ if ( ! oidcConfig . authorizationEndpoint ) missing . push ( 'authorizationEndpoint' )
222+ if ( ! oidcConfig . tokenEndpoint ) missing . push ( 'tokenEndpoint' )
223+ if ( ! oidcConfig . jwksEndpoint ) missing . push ( 'jwksEndpoint' )
162224
163- logger . info ( 'Using manual OIDC endpoints for provider' , {
164- providerId,
165- provider : issuer . includes ( 'okta.com' )
166- ? 'Okta'
167- : issuer . includes ( 'auth0.com' )
168- ? 'Auth0'
169- : 'Generic' ,
170- authEndpoint : oidcConfig . authorizationEndpoint ,
225+ logger . error ( 'Missing required OIDC endpoints after discovery merge' , {
226+ missing,
227+ authorizationEndpoint : oidcConfig . authorizationEndpoint ,
228+ tokenEndpoint : oidcConfig . tokenEndpoint ,
229+ jwksEndpoint : oidcConfig . jwksEndpoint ,
171230 } )
231+ return NextResponse . json (
232+ {
233+ error : `Missing required OIDC endpoints: ${ missing . join ( ', ' ) } . Please provide these explicitly or verify the issuer supports OIDC discovery.` ,
234+ } ,
235+ { status : 400 }
236+ )
172237 }
173238
174239 providerConfig . oidcConfig = oidcConfig
0 commit comments