@@ -310,8 +310,9 @@ func TestJWTBearer(t *testing.T) {
310
310
audience := reg .Config ().OAuth2TokenURL (ctx ).String ()
311
311
grantType := "urn:ietf:params:oauth:grant-type:jwt-bearer"
312
312
313
+ jti := uuid .NewString ()
313
314
token , _ , err := signer .Generate (ctx , jwt.MapClaims {
314
- "jti" : uuid . NewString () ,
315
+ "jti" : jti ,
315
316
"iss" : trustGrant .Issuer ,
316
317
"sub" : trustGrant .Subject ,
317
318
"aud" : audience ,
@@ -339,6 +340,7 @@ func TestJWTBearer(t *testing.T) {
339
340
require .ElementsMatch (t , hookReq .Request .GrantedScopes , expectedGrantedScopes )
340
341
require .ElementsMatch (t , hookReq .Request .GrantedAudience , expectedGrantedAudience )
341
342
require .Equal (t , expectedPayload , hookReq .Request .Payload )
343
+ require .Equal (t , jti , hookReq .Request .JWTClaims ["jti" ])
342
344
343
345
claims := map [string ]interface {}{
344
346
"hooked" : true ,
@@ -561,3 +563,191 @@ func TestJWTBearer(t *testing.T) {
561
563
t .Run ("strategy=jwt" , run ("jwt" ))
562
564
})
563
565
}
566
+
567
+ func TestJWTClientAssertion (t * testing.T ) {
568
+ ctx := context .Background ()
569
+
570
+ reg := testhelpers .NewMockedRegistry (t , & contextx.Default {})
571
+ reg .Config ().MustSet (ctx , config .KeyAccessTokenStrategy , "opaque" )
572
+ _ , admin := testhelpers .NewOAuth2Server (ctx , t , reg )
573
+
574
+ set , kid := uuid .NewString (), uuid .NewString ()
575
+ keys , err := jwk .GenerateJWK (ctx , jose .RS256 , kid , "sig" )
576
+ require .NoError (t , err )
577
+ signer := jwk .NewDefaultJWTSigner (reg .Config (), reg , set )
578
+ signer .GetPrivateKey = func (ctx context.Context ) (interface {}, error ) {
579
+ return keys .Keys [0 ], nil
580
+ }
581
+
582
+ client := & hc.Client {
583
+ GrantTypes : []string {"client_credentials" },
584
+ Scope : "offline_access" ,
585
+ TokenEndpointAuthMethod : "private_key_jwt" ,
586
+ JSONWebKeys : & x.JoseJSONWebKeySet {
587
+ JSONWebKeySet : & jose.JSONWebKeySet {
588
+ Keys : []jose.JSONWebKey {keys .Keys [0 ].Public ()},
589
+ },
590
+ },
591
+ }
592
+ require .NoError (t , reg .ClientManager ().CreateClient (ctx , client ))
593
+
594
+ var newConf = func (client * hc.Client ) * clientcredentials.Config {
595
+ return & clientcredentials.Config {
596
+ AuthStyle : goauth2 .AuthStyleInParams ,
597
+ TokenURL : reg .Config ().OAuth2TokenURL (ctx ).String (),
598
+ Scopes : strings .Split (client .Scope , " " ),
599
+ EndpointParams : url.Values {
600
+ "client_assertion_type" : {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
601
+ },
602
+ }
603
+ }
604
+ var getToken = func (t * testing.T , conf * clientcredentials.Config ) (* goauth2.Token , error ) {
605
+ return conf .Token (context .Background ())
606
+ }
607
+
608
+ var inspectToken = func (t * testing.T , token * goauth2.Token , cl * hc.Client , strategy string , checkExtraClaims bool ) {
609
+ introspection := testhelpers .IntrospectToken (t , & goauth2.Config {ClientID : cl .GetID (), ClientSecret : cl .Secret }, token .AccessToken , admin )
610
+
611
+ check := func (res gjson.Result ) {
612
+ assert .EqualValues (t , cl .GetID (), res .Get ("client_id" ).String (), "%s" , res .Raw )
613
+ assert .EqualValues (t , cl .GetID (), res .Get ("sub" ).String (), "%s" , res .Raw )
614
+ assert .EqualValues (t , reg .Config ().IssuerURL (ctx ).String (), res .Get ("iss" ).String (), "%s" , res .Raw )
615
+
616
+ assert .EqualValues (t , res .Get ("nbf" ).Int (), res .Get ("iat" ).Int (), "%s" , res .Raw )
617
+ assert .True (t , res .Get ("exp" ).Int () >= res .Get ("iat" ).Int ()+ int64 (reg .Config ().GetAccessTokenLifespan (ctx ).Seconds ()), "%s" , res .Raw )
618
+
619
+ if checkExtraClaims {
620
+ require .True (t , res .Get ("ext.hooked" ).Bool ())
621
+ }
622
+ }
623
+
624
+ check (introspection )
625
+ assert .True (t , introspection .Get ("active" ).Bool ())
626
+ assert .EqualValues (t , "access_token" , introspection .Get ("token_use" ).String ())
627
+ assert .EqualValues (t , "Bearer" , introspection .Get ("token_type" ).String ())
628
+ assert .EqualValues (t , "offline_access" , introspection .Get ("scope" ).String (), "%s" , introspection .Raw )
629
+
630
+ if strategy != "jwt" {
631
+ return
632
+ }
633
+
634
+ body , err := x .DecodeSegment (strings .Split (token .AccessToken , "." )[1 ])
635
+ require .NoError (t , err )
636
+ jwtClaims := gjson .ParseBytes (body )
637
+ assert .NotEmpty (t , jwtClaims .Get ("jti" ).String ())
638
+ assert .NotEmpty (t , jwtClaims .Get ("iss" ).String ())
639
+ assert .NotEmpty (t , jwtClaims .Get ("client_id" ).String ())
640
+ assert .EqualValues (t , "offline_access" , introspection .Get ("scope" ).String (), "%s" , introspection .Raw )
641
+
642
+ header , err := x .DecodeSegment (strings .Split (token .AccessToken , "." )[0 ])
643
+ require .NoError (t , err )
644
+ jwtHeader := gjson .ParseBytes (header )
645
+ assert .NotEmpty (t , jwtHeader .Get ("kid" ).String ())
646
+ assert .EqualValues (t , "offline_access" , introspection .Get ("scope" ).String (), "%s" , introspection .Raw )
647
+
648
+ check (jwtClaims )
649
+ }
650
+
651
+ var generateAssertion = func () (string , jwt.MapClaims , error ) {
652
+ claims := jwt.MapClaims {
653
+ "jti" : uuid .NewString (),
654
+ "iss" : client .GetID (),
655
+ "sub" : client .GetID (),
656
+ "aud" : reg .Config ().OAuth2TokenURL (ctx ).String (),
657
+ "exp" : time .Now ().Add (time .Hour ).Unix (),
658
+ "iat" : time .Now ().Add (- time .Minute ).Unix (),
659
+ }
660
+ headers := & jwt.Headers {Extra : map [string ]interface {}{"kid" : kid }}
661
+ token , _ , err := signer .Generate (ctx , claims , headers )
662
+ return token , claims , err
663
+ }
664
+
665
+ t .Run ("case=unable to exchange invalid jwt" , func (t * testing.T ) {
666
+ conf := newConf (client )
667
+ conf .EndpointParams .Set ("client_assertion" , "not-a-jwt" )
668
+ _ , err := getToken (t , conf )
669
+ require .Error (t , err )
670
+ assert .Contains (t , err .Error (), "Unable to verify the integrity of the 'client_assertion' value." )
671
+ })
672
+
673
+ t .Run ("case=should exchange for an access token" , func (t * testing.T ) {
674
+ run := func (strategy string ) func (t * testing.T ) {
675
+ return func (t * testing.T ) {
676
+ reg .Config ().MustSet (ctx , config .KeyAccessTokenStrategy , strategy )
677
+
678
+ token , _ , err := generateAssertion ()
679
+ require .NoError (t , err )
680
+
681
+ conf := newConf (client )
682
+ conf .EndpointParams .Set ("client_assertion" , token )
683
+
684
+ result , err := getToken (t , conf )
685
+ require .NoError (t , err )
686
+
687
+ inspectToken (t , result , client , strategy , false )
688
+ }
689
+ }
690
+
691
+ t .Run ("strategy=opaque" , run ("opaque" ))
692
+ t .Run ("strategy=jwt" , run ("jwt" ))
693
+ })
694
+
695
+ t .Run ("should call token hook if configured" , func (t * testing.T ) {
696
+ run := func (strategy string ) func (t * testing.T ) {
697
+ return func (t * testing.T ) {
698
+ token , assertionClaims , err := generateAssertion ()
699
+ require .NoError (t , err )
700
+
701
+ hs := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
702
+ assert .Equal (t , r .Header .Get ("Content-Type" ), "application/json; charset=UTF-8" )
703
+
704
+ expectedGrantedScopes := []string {client .Scope }
705
+ expectedPayload := map [string ][]string {
706
+ "grant_type" : {"client_credentials" },
707
+ "scope" : {"offline_access" },
708
+ }
709
+
710
+ var hookReq hydraoauth2.TokenHookRequest
711
+ require .NoError (t , json .NewDecoder (r .Body ).Decode (& hookReq ))
712
+ require .NotEmpty (t , hookReq .Session )
713
+ require .Equal (t , hookReq .Session .Extra , map [string ]interface {}{})
714
+ require .NotEmpty (t , hookReq .Request )
715
+ require .ElementsMatch (t , hookReq .Request .GrantedScopes , expectedGrantedScopes )
716
+ require .Equal (t , expectedPayload , hookReq .Request .Payload )
717
+ require .Equal (t , assertionClaims ["jti" ], hookReq .Request .JWTClaims ["jti" ])
718
+
719
+ claims := map [string ]interface {}{
720
+ "hooked" : true ,
721
+ }
722
+
723
+ hookResp := hydraoauth2.TokenHookResponse {
724
+ Session : flow.AcceptOAuth2ConsentRequestSession {
725
+ AccessToken : claims ,
726
+ IDToken : claims ,
727
+ },
728
+ }
729
+
730
+ w .WriteHeader (http .StatusOK )
731
+ require .NoError (t , json .NewEncoder (w ).Encode (& hookResp ))
732
+ }))
733
+ defer hs .Close ()
734
+
735
+ reg .Config ().MustSet (ctx , config .KeyAccessTokenStrategy , strategy )
736
+ reg .Config ().MustSet (ctx , config .KeyTokenHook , hs .URL )
737
+
738
+ defer reg .Config ().MustSet (ctx , config .KeyTokenHook , nil )
739
+
740
+ conf := newConf (client )
741
+ conf .EndpointParams .Set ("client_assertion" , token )
742
+
743
+ result , err := getToken (t , conf )
744
+ require .NoError (t , err )
745
+
746
+ inspectToken (t , result , client , strategy , true )
747
+ }
748
+ }
749
+
750
+ t .Run ("strategy=opaque" , run ("opaque" ))
751
+ t .Run ("strategy=jwt" , run ("jwt" ))
752
+ })
753
+ }
0 commit comments