@@ -316,6 +316,140 @@ function checkCompliance() {
316316 return false ;
317317}
318318
319+ // Validate the composed header/payload as an OAuth 2.0 JWT access token per
320+ // RFC 9068 (JWT Profile for OAuth 2.0 Access Tokens). Output goes to the same
321+ // Compliance Output box. Header (§2.1): typ MUST be "at+jwt" and the token MUST
322+ // be signed (alg present, not "none"). Required claims (§2.2): iss, exp, aud,
323+ // sub, client_id, iat, jti. scope is conditionally recommended (§2.2.3);
324+ // auth_time/acr/amr are optional (§2.2.1) and only type-checked if present.
325+ function checkRfc9068Compliance ( ) {
326+ var results = [ ] ;
327+ function pass ( c , m ) { results . push ( 'PASS ' + c + ': ' + m ) ; }
328+ function fail ( c , m ) { results . push ( 'FAIL ' + c + ': ' + m ) ; }
329+ function skip ( c , m ) { results . push ( 'SKIP ' + c + ': ' + m ) ; }
330+
331+ var header , payload ;
332+ try {
333+ header = parseJson ( 'jwt_tools_header' , 'JWT Header' ) ;
334+ } catch ( e ) {
335+ setVal ( 'compliance_output' , 'FAIL header: ' + e . message ) ;
336+ return false ;
337+ }
338+ try {
339+ payload = parseJson ( 'jwt_tools_payload' , 'JWT Payload' ) ;
340+ } catch ( e ) {
341+ setVal ( 'compliance_output' , 'FAIL payload: ' + e . message ) ;
342+ return false ;
343+ }
344+
345+ results . push ( 'RFC 9068 — OAuth 2.0 JWT Access Token' ) ;
346+
347+ // ---- Header (RFC 9068 §2.1) ----
348+ // typ is REQUIRED and MUST be "at+jwt" (the "application/" prefix is allowed).
349+ if ( header . typ === undefined ) {
350+ fail ( 'typ' , 'Missing — MUST be "at+jwt" (RFC 9068 §2.1)' ) ;
351+ } else if ( typeof header . typ !== 'string' ) {
352+ fail ( 'typ' , '"typ" must be a string' ) ;
353+ } else if ( header . typ === 'at+jwt' || header . typ === 'application/at+jwt' ) {
354+ pass ( 'typ' , '"' + header . typ + '"' ) ;
355+ } else {
356+ fail ( 'typ' , '"' + header . typ + '" — MUST be "at+jwt" (RFC 9068 §2.1)' ) ;
357+ }
358+
359+ // The token MUST be signed; alg is REQUIRED and MUST NOT be "none".
360+ if ( ! header . alg ) {
361+ fail ( 'alg' , 'Missing — access tokens MUST be signed (RFC 9068 §2.1)' ) ;
362+ } else if ( typeof header . alg !== 'string' ) {
363+ fail ( 'alg' , '"alg" must be a string' ) ;
364+ } else if ( header . alg === 'none' ) {
365+ fail ( 'alg' , '"none" is not permitted — access tokens MUST be signed (RFC 9068 §2.1)' ) ;
366+ } else {
367+ pass ( 'alg' , header . alg ) ;
368+ }
369+
370+ // ---- Required claims (RFC 9068 §2.2) ----
371+ function requireString ( name ) {
372+ if ( payload [ name ] === undefined ) fail ( name , 'Missing required claim (RFC 9068 §2.2)' ) ;
373+ else if ( typeof payload [ name ] !== 'string' ) fail ( name , 'Must be a string' ) ;
374+ else pass ( name , '"' + payload [ name ] + '"' ) ;
375+ }
376+ function requireNumericDate ( name ) {
377+ if ( payload [ name ] === undefined ) fail ( name , 'Missing required claim (RFC 9068 §2.2)' ) ;
378+ else if ( typeof payload [ name ] !== 'number' || ! Number . isInteger ( payload [ name ] ) ) fail ( name , 'Must be an integer NumericDate' ) ;
379+ else pass ( name , new Date ( payload [ name ] * 1000 ) . toISOString ( ) ) ;
380+ }
381+
382+ requireString ( 'iss' ) ;
383+ requireNumericDate ( 'exp' ) ;
384+
385+ // aud is REQUIRED: a StringOrURI or a non-empty array of them.
386+ if ( payload . aud === undefined ) {
387+ fail ( 'aud' , 'Missing required claim (RFC 9068 §2.2)' ) ;
388+ } else if ( typeof payload . aud === 'string' ) {
389+ pass ( 'aud' , '"' + payload . aud + '"' ) ;
390+ } else if ( Array . isArray ( payload . aud ) && payload . aud . length > 0 && payload . aud . every ( function ( a ) { return typeof a === 'string' ; } ) ) {
391+ pass ( 'aud' , payload . aud . length + ' value(s)' ) ;
392+ } else {
393+ fail ( 'aud' , 'Must be a string or non-empty array of strings' ) ;
394+ }
395+
396+ requireString ( 'sub' ) ;
397+ requireString ( 'client_id' ) ;
398+ requireNumericDate ( 'iat' ) ;
399+ requireString ( 'jti' ) ;
400+
401+ // ---- Conditional / optional claims ----
402+ // scope SHOULD be present when a scope was requested (RFC 9068 §2.2.3).
403+ if ( payload . scope === undefined ) {
404+ skip ( 'scope' , 'Not present (SHOULD be present if a scope was requested — RFC 9068 §2.2.3)' ) ;
405+ } else if ( typeof payload . scope !== 'string' ) {
406+ fail ( 'scope' , 'Must be a space-delimited string (RFC 9068 §2.2.3)' ) ;
407+ } else {
408+ pass ( 'scope' , '"' + payload . scope + '"' ) ;
409+ }
410+
411+ // Authentication information claims are optional (RFC 9068 §2.2.1).
412+ if ( payload . auth_time !== undefined ) {
413+ if ( typeof payload . auth_time !== 'number' || ! Number . isInteger ( payload . auth_time ) ) fail ( 'auth_time' , 'Must be an integer NumericDate' ) ;
414+ else pass ( 'auth_time' , new Date ( payload . auth_time * 1000 ) . toISOString ( ) ) ;
415+ }
416+ if ( payload . acr !== undefined ) {
417+ if ( typeof payload . acr !== 'string' ) fail ( 'acr' , 'Must be a string' ) ;
418+ else pass ( 'acr' , '"' + payload . acr + '"' ) ;
419+ }
420+ if ( payload . amr !== undefined ) {
421+ if ( Array . isArray ( payload . amr ) && payload . amr . every ( function ( a ) { return typeof a === 'string' ; } ) ) pass ( 'amr' , payload . amr . length + ' value(s)' ) ;
422+ else fail ( 'amr' , 'Must be an array of strings' ) ;
423+ }
424+
425+ setVal ( 'compliance_output' , results . join ( '\n' ) ) ;
426+ return false ;
427+ }
428+
429+ // Populate Header, Payload, and the Encoded JWT with a sample RFC 9068 access
430+ // token: header carries alg + typ "at+jwt"; payload carries the required claims
431+ // (iss, exp, aud, sub, client_id, iat, jti) plus a scope. Produced unsigned
432+ // (header.payload.) — sign it in the Sign pane to complete it.
433+ function generateRfc9068Token ( ) {
434+ var now = Math . floor ( Date . now ( ) / 1000 ) ;
435+ var header = { alg : 'RS256' , typ : 'at+jwt' } ;
436+ var payload = {
437+ iss : 'https://as.example.com' ,
438+ sub : 'user-1234' ,
439+ aud : 'https://api.example.com' ,
440+ client_id : 'example-client' ,
441+ iat : now ,
442+ exp : now + 3600 ,
443+ jti : bytesToB64u ( crypto . getRandomValues ( new Uint8Array ( 12 ) ) ) ,
444+ scope : 'read write'
445+ } ;
446+ setVal ( 'jwt_tools_header' , JSON . stringify ( header , null , 2 ) ) ;
447+ setVal ( 'jwt_tools_payload' , JSON . stringify ( payload , null , 2 ) ) ;
448+ updateEncoded ( ) ; // fills the Encoded JWT field (header.payload.) from the above
449+ setVal ( 'jwt_tools_sync_status' , 'Generated a sample RFC 9068 access token (unsigned). Sign it in the Sign (JWS) pane to complete it.' ) ;
450+ return false ;
451+ }
452+
319453// ---------------------------------------------------------------------------
320454// Digital signatures (JWS)
321455// ---------------------------------------------------------------------------
@@ -1042,6 +1176,8 @@ module.exports = {
10421176 onEncodedInput,
10431177 addClaim,
10441178 checkCompliance,
1179+ checkRfc9068Compliance,
1180+ generateRfc9068Token,
10451181 generateSigningKeys,
10461182 signJWT,
10471183 verifyJWT,
0 commit comments