@@ -2,8 +2,9 @@ import { createHash } from "node:crypto";
22import { decodeJwt } from "jose" ;
33import { Hono } from "@emulators/core" ;
44import { beforeEach , describe , expect , it } from "vitest" ;
5- import { Store , WebhookDispatcher , authMiddleware , type TokenMap } from "@emulators/core" ;
5+ import { Store , WebhookDispatcher , authMiddleware , createServer , type TokenMap } from "@emulators/core" ;
66import { getOktaStore , oktaPlugin , seedFromConfig } from "../index.js" ;
7+ import { manifest } from "../manifest.js" ;
78
89const base = "http://localhost:4000" ;
910
@@ -156,6 +157,89 @@ async function exchangeCode(
156157 } ) ;
157158}
158159
160+ function dcrRequestBody ( tokenEndpointAuthMethod : "none" | "client_secret_post" = "none" ) {
161+ return {
162+ redirect_uris : [ "http://localhost:3000/dcr-callback" ] ,
163+ client_name : "Executor DCR Client" ,
164+ grant_types : [ "authorization_code" , "refresh_token" ] ,
165+ response_types : [ "code" ] ,
166+ token_endpoint_auth_method : tokenEndpointAuthMethod ,
167+ scope : "openid profile email offline_access" ,
168+ } ;
169+ }
170+
171+ async function registerClient (
172+ app : Hono ,
173+ registrationEndpoint = `${ base } /oauth2/default/v1/clients` ,
174+ tokenEndpointAuthMethod : "none" | "client_secret_post" = "none" ,
175+ ) : Promise < Record < string , unknown > > {
176+ const res = await app . request ( registrationEndpoint , {
177+ method : "POST" ,
178+ headers : { "Content-Type" : "application/json" , Accept : "application/json" } ,
179+ body : JSON . stringify ( dcrRequestBody ( tokenEndpointAuthMethod ) ) ,
180+ } ) ;
181+ expect ( res . status ) . toBe ( 201 ) ;
182+ return ( await res . json ( ) ) as Record < string , unknown > ;
183+ }
184+
185+ async function completePkceCodeFlow (
186+ app : Hono ,
187+ store : Store ,
188+ options : {
189+ authorizationEndpoint ?: string ;
190+ tokenEndpoint ?: string ;
191+ clientId : string ;
192+ clientSecret ?: string ;
193+ redirectUri ?: string ;
194+ includeClientSecret ?: boolean ;
195+ } ,
196+ ) : Promise < Record < string , unknown > > {
197+ const redirectUri = options . redirectUri ?? "http://localhost:3000/dcr-callback" ;
198+ const verifier = "executor-pkce-verifier-12345" ;
199+ const challenge = createHash ( "sha256" ) . update ( verifier ) . digest ( "base64url" ) ;
200+ const authorizationEndpoint = options . authorizationEndpoint ?? `${ base } /oauth2/default/v1/authorize` ;
201+
202+ const authorizeUrl = new URL ( authorizationEndpoint ) ;
203+ authorizeUrl . searchParams . set ( "client_id" , options . clientId ) ;
204+ authorizeUrl . searchParams . set ( "redirect_uri" , redirectUri ) ;
205+ authorizeUrl . searchParams . set ( "response_type" , "code" ) ;
206+ authorizeUrl . searchParams . set ( "state" , "executor-state" ) ;
207+ authorizeUrl . searchParams . set ( "code_challenge" , challenge ) ;
208+ authorizeUrl . searchParams . set ( "code_challenge_method" , "S256" ) ;
209+ authorizeUrl . searchParams . set ( "scope" , "openid profile email offline_access" ) ;
210+
211+ const authorizeRes = await app . request ( authorizeUrl . toString ( ) , { headers : { Accept : "text/html" } } ) ;
212+ expect ( authorizeRes . status ) . toBe ( 200 ) ;
213+
214+ const { code, state } = await getAuthCode ( app , store , {
215+ authServerId : "default" ,
216+ clientId : options . clientId ,
217+ redirectUri,
218+ scope : "openid profile email offline_access" ,
219+ state : "executor-state" ,
220+ codeChallenge : challenge ,
221+ codeChallengeMethod : "S256" ,
222+ } ) ;
223+ expect ( code ) . toBeTruthy ( ) ;
224+ expect ( state ) . toBe ( "executor-state" ) ;
225+
226+ const tokenUrl = new URL ( options . tokenEndpoint ?? `${ base } /oauth2/default/v1/token` ) ;
227+ const tokenRes = await app . request ( tokenUrl . toString ( ) , {
228+ method : "POST" ,
229+ headers : { "Content-Type" : "application/x-www-form-urlencoded" , Accept : "application/json" } ,
230+ body : new URLSearchParams ( {
231+ grant_type : "authorization_code" ,
232+ code,
233+ client_id : options . clientId ,
234+ ...( options . includeClientSecret === false ? { } : { client_secret : options . clientSecret ?? "" } ) ,
235+ redirect_uri : redirectUri ,
236+ code_verifier : verifier ,
237+ } ) . toString ( ) ,
238+ } ) ;
239+ expect ( tokenRes . status ) . toBe ( 200 ) ;
240+ return ( await tokenRes . json ( ) ) as Record < string , unknown > ;
241+ }
242+
159243describe ( "Okta plugin integration" , ( ) => {
160244 let app : Hono ;
161245 let store : Store ;
@@ -180,7 +264,7 @@ describe("Okta plugin integration", () => {
180264 expect ( body . introspection_endpoint ) . toBe ( `${ base } /oauth2/v1/introspect` ) ;
181265 expect ( body . registration_endpoint ) . toBe ( `${ base } /oauth2/v1/clients` ) ;
182266 expect ( body . code_challenge_methods_supported ) . toEqual ( [ "plain" , "S256" ] ) ;
183- expect ( body . token_endpoint_auth_methods_supported ) . toEqual ( [ "client_secret_post " , "client_secret_basic " , "none" ] ) ;
267+ expect ( body . token_endpoint_auth_methods_supported ) . toEqual ( [ "client_secret_basic " , "client_secret_post " , "none" ] ) ;
184268 expect ( body . request_parameter_supported ) . toBe ( false ) ;
185269 } ) ;
186270
@@ -202,6 +286,40 @@ describe("Okta plugin integration", () => {
202286 expect ( body . token_endpoint ) . toBe ( `${ base } /oauth2/custom-as/v1/token` ) ;
203287 } ) ;
204288
289+ it ( "returns path-insert metadata for both well-known suffixes" , async ( ) => {
290+ for ( const path of [
291+ "/.well-known/oauth-authorization-server/oauth2/default" ,
292+ "/.well-known/openid-configuration/oauth2/default" ,
293+ ] ) {
294+ const res = await app . request ( `${ base } ${ path } ` , { headers : { Accept : "application/json" } } ) ;
295+ expect ( res . status ) . toBe ( 200 ) ;
296+ const body = ( await res . json ( ) ) as Record < string , unknown > ;
297+ expect ( body . issuer ) . toBe ( `${ base } /oauth2/default` ) ;
298+ expect ( body . authorization_endpoint ) . toBe ( `${ base } /oauth2/default/v1/authorize` ) ;
299+ expect ( body . token_endpoint ) . toBe ( `${ base } /oauth2/default/v1/token` ) ;
300+ expect ( body . registration_endpoint ) . toBe ( `${ base } /oauth2/default/v1/clients` ) ;
301+ expect ( body . response_types_supported ) . toContain ( "code" ) ;
302+ expect ( body . code_challenge_methods_supported ) . toContain ( "S256" ) ;
303+ }
304+ } ) ;
305+
306+ it ( "returns the same auth-server metadata for suffix and path-insert aliases" , async ( ) => {
307+ const paths = [
308+ "/oauth2/default/.well-known/openid-configuration" ,
309+ "/oauth2/default/.well-known/oauth-authorization-server" ,
310+ "/.well-known/openid-configuration/oauth2/default" ,
311+ "/.well-known/oauth-authorization-server/oauth2/default" ,
312+ ] ;
313+ const documents = await Promise . all (
314+ paths . map ( async ( path ) => {
315+ const res = await app . request ( `${ base } ${ path } ` , { headers : { Accept : "application/json" } } ) ;
316+ expect ( res . status ) . toBe ( 200 ) ;
317+ return ( await res . json ( ) ) as Record < string , unknown > ;
318+ } ) ,
319+ ) ;
320+ expect ( documents . every ( ( doc ) => JSON . stringify ( doc ) === JSON . stringify ( documents [ 0 ] ) ) ) . toBe ( true ) ;
321+ } ) ;
322+
205323 it ( "returns 404 for unknown custom auth server" , async ( ) => {
206324 const res = await app . request ( `${ base } /oauth2/does-not-exist/.well-known/openid-configuration` ) ;
207325 expect ( res . status ) . toBe ( 404 ) ;
@@ -346,6 +464,126 @@ describe("Okta plugin integration", () => {
346464 } ) ;
347465 } ) ;
348466
467+ describe ( "dynamic client registration" , ( ) => {
468+ it ( "registers public and confidential clients with RFC 7591 responses" , async ( ) => {
469+ const publicClient = await registerClient ( app , `${ base } /oauth2/default/v1/clients` , "none" ) ;
470+ expect ( typeof publicClient . client_id ) . toBe ( "string" ) ;
471+ expect ( publicClient . client_secret ) . toBeUndefined ( ) ;
472+ expect ( publicClient . client_id_issued_at ) . toEqual ( expect . any ( Number ) ) ;
473+ expect ( publicClient . client_secret_expires_at ) . toBe ( 0 ) ;
474+ expect ( publicClient . client_name ) . toBe ( "Executor DCR Client" ) ;
475+ expect ( publicClient . redirect_uris ) . toEqual ( [ "http://localhost:3000/dcr-callback" ] ) ;
476+ expect ( publicClient . grant_types ) . toEqual ( [ "authorization_code" , "refresh_token" ] ) ;
477+ expect ( publicClient . response_types ) . toEqual ( [ "code" ] ) ;
478+ expect ( publicClient . token_endpoint_auth_method ) . toBe ( "none" ) ;
479+
480+ const confidentialClient = await registerClient ( app , `${ base } /oauth2/default/v1/clients` , "client_secret_post" ) ;
481+ expect ( typeof confidentialClient . client_id ) . toBe ( "string" ) ;
482+ expect ( typeof confidentialClient . client_secret ) . toBe ( "string" ) ;
483+ expect ( confidentialClient . token_endpoint_auth_method ) . toBe ( "client_secret_post" ) ;
484+ } ) ;
485+
486+ it ( "registered public client completes authorization-code PKCE S256 exchange" , async ( ) => {
487+ const client = await registerClient ( app , `${ base } /oauth2/default/v1/clients` , "none" ) ;
488+ const tokenBody = await completePkceCodeFlow ( app , store , {
489+ clientId : client . client_id as string ,
490+ includeClientSecret : false ,
491+ } ) ;
492+ expect ( ( tokenBody . access_token as string ) . startsWith ( "okta_" ) ) . toBe ( true ) ;
493+ expect ( ( tokenBody . refresh_token as string ) . startsWith ( "r_okta_" ) ) . toBe ( true ) ;
494+ const claims = decodeJwt ( tokenBody . id_token as string ) ;
495+ expect ( claims . aud ) . toBe ( client . client_id ) ;
496+ expect ( claims . iss ) . toBe ( `${ base } /oauth2/default` ) ;
497+ } ) ;
498+
499+ it ( "rejects invalid metadata with an RFC 6749 error envelope" , async ( ) => {
500+ const res = await app . request ( `${ base } /oauth2/default/v1/clients` , {
501+ method : "POST" ,
502+ headers : { "Content-Type" : "application/json" , Accept : "application/json" } ,
503+ body : JSON . stringify ( { ...dcrRequestBody ( ) , redirect_uris : [ ] } ) ,
504+ } ) ;
505+ expect ( res . status ) . toBe ( 400 ) ;
506+ const body = ( await res . json ( ) ) as Record < string , unknown > ;
507+ expect ( body ) . toEqual ( {
508+ error : "invalid_client_metadata" ,
509+ error_description : "redirect_uris must be a non-empty array of strings." ,
510+ } ) ;
511+ } ) ;
512+
513+ it ( "rejects invalid redirect URIs with invalid_redirect_uri" , async ( ) => {
514+ const res = await app . request ( `${ base } /oauth2/default/v1/clients` , {
515+ method : "POST" ,
516+ headers : { "Content-Type" : "application/json" , Accept : "application/json" } ,
517+ body : JSON . stringify ( { ...dcrRequestBody ( ) , redirect_uris : [ "not a url" ] } ) ,
518+ } ) ;
519+ expect ( res . status ) . toBe ( 400 ) ;
520+ const body = ( await res . json ( ) ) as Record < string , unknown > ;
521+ expect ( body . error ) . toBe ( "invalid_redirect_uri" ) ;
522+ expect ( body . error_description ) . toBe ( "Each redirect_uri must be an absolute HTTP or HTTPS URL." ) ;
523+ } ) ;
524+
525+ it ( "satisfies executor metadata probe then registers and completes PKCE" , async ( ) => {
526+ const metadataRes = await app . request ( `${ base } /.well-known/oauth-authorization-server/oauth2/default` , {
527+ headers : { Accept : "application/json" } ,
528+ } ) ;
529+ expect ( metadataRes . status ) . toBe ( 200 ) ;
530+ const metadata = ( await metadataRes . json ( ) ) as Record < string , unknown > ;
531+ expect ( metadata . registration_endpoint ) . toBe ( `${ base } /oauth2/default/v1/clients` ) ;
532+
533+ const client = await registerClient ( app , metadata . registration_endpoint as string , "none" ) ;
534+ const tokenBody = await completePkceCodeFlow ( app , store , {
535+ authorizationEndpoint : metadata . authorization_endpoint as string ,
536+ tokenEndpoint : metadata . token_endpoint as string ,
537+ clientId : client . client_id as string ,
538+ includeClientSecret : false ,
539+ } ) ;
540+ expect ( tokenBody . token_type ) . toBe ( "Bearer" ) ;
541+ expect ( tokenBody . scope ) . toBe ( "openid profile email offline_access" ) ;
542+ } ) ;
543+
544+ it ( "fires an armed registration fault once and records it in the ledger" , async ( ) => {
545+ const server = createServer ( oktaPlugin , { baseUrl : base , manifest } ) ;
546+ oktaPlugin . seed ?.( server . store , base ) ;
547+
548+ const arm = await server . app . request ( `${ base } /_emulate/faults` , {
549+ method : "POST" ,
550+ headers : { "Content-Type" : "application/json" } ,
551+ body : JSON . stringify ( {
552+ match : { method : "POST" , pathPattern : "/oauth2/default/v1/clients" } ,
553+ response : {
554+ status : 503 ,
555+ body : { error : "temporarily_unavailable" , error_description : "Injected failure." } ,
556+ } ,
557+ } ) ,
558+ } ) ;
559+ expect ( arm . status ) . toBe ( 200 ) ;
560+
561+ const first = await server . app . request ( `${ base } /oauth2/default/v1/clients` , {
562+ method : "POST" ,
563+ headers : { "Content-Type" : "application/json" , Accept : "application/json" } ,
564+ body : JSON . stringify ( dcrRequestBody ( ) ) ,
565+ } ) ;
566+ expect ( first . status ) . toBe ( 503 ) ;
567+ expect ( ( ( await first . json ( ) ) as Record < string , unknown > ) . error ) . toBe ( "temporarily_unavailable" ) ;
568+
569+ const second = await server . app . request ( `${ base } /oauth2/default/v1/clients` , {
570+ method : "POST" ,
571+ headers : { "Content-Type" : "application/json" , Accept : "application/json" } ,
572+ body : JSON . stringify ( dcrRequestBody ( ) ) ,
573+ } ) ;
574+ expect ( second . status ) . toBe ( 201 ) ;
575+
576+ const ledgerRes = await server . app . request ( `${ base } /_emulate/ledger` ) ;
577+ expect ( ledgerRes . status ) . toBe ( 200 ) ;
578+ const ledger = ( await ledgerRes . json ( ) ) as { entries : Array < Record < string , unknown > > } ;
579+ const faulted = ledger . entries . find (
580+ ( entry ) => entry . path === "/oauth2/default/v1/clients" && entry . faulted === true ,
581+ ) ;
582+ expect ( faulted ) . toBeDefined ( ) ;
583+ expect ( ( faulted ?. response as Record < string , unknown > ) . status ) . toBe ( 503 ) ;
584+ } ) ;
585+ } ) ;
586+
349587 describe ( "PKCE" , ( ) => {
350588 it ( "supports S256 code challenge" , async ( ) => {
351589 const verifier = "pkce-verifier-12345" ;
0 commit comments