@@ -2,9 +2,12 @@ package authprovider_test
22
33import (
44 "context"
5+ "crypto"
56 "crypto/ecdsa"
67 "crypto/elliptic"
78 "crypto/rand"
9+ "crypto/rsa"
10+ "crypto/sha256"
811 "crypto/x509"
912 "crypto/x509/pkix"
1013 "encoding/base64"
@@ -25,6 +28,37 @@ import (
2528 "bosh-nats-sync/pkg/config"
2629)
2730
31+ // generateRSAKeyPair creates a 2048-bit RSA key pair for JWT signing tests.
32+ func generateRSAKeyPair () (* rsa.PrivateKey , []byte ) {
33+ priv , err := rsa .GenerateKey (rand .Reader , 2048 )
34+ if err != nil {
35+ panic (err )
36+ }
37+ pubBytes , err := x509 .MarshalPKIXPublicKey (& priv .PublicKey )
38+ if err != nil {
39+ panic (err )
40+ }
41+ pubPEM := pem .EncodeToMemory (& pem.Block {Type : "PUBLIC KEY" , Bytes : pubBytes })
42+ return priv , pubPEM
43+ }
44+
45+ // createRS256JWT builds a minimal RS256-signed JWT for use in tests.
46+ func createRS256JWT (privKey * rsa.PrivateKey , expiresAt time.Time ) string {
47+ header := base64 .RawURLEncoding .EncodeToString ([]byte (`{"alg":"RS256","typ":"JWT"}` ))
48+ claimsJSON , _ := json .Marshal (map [string ]interface {}{
49+ "sub" : "test-client" ,
50+ "exp" : expiresAt .Unix (),
51+ })
52+ payload := base64 .RawURLEncoding .EncodeToString (claimsJSON )
53+ sigInput := header + "." + payload
54+ digest := sha256 .Sum256 ([]byte (sigInput ))
55+ sig , err := rsa .SignPKCS1v15 (rand .Reader , privKey , crypto .SHA256 , digest [:])
56+ if err != nil {
57+ panic (err )
58+ }
59+ return sigInput + "." + base64 .RawURLEncoding .EncodeToString (sig )
60+ }
61+
2862// generateTestCACertPEM creates a minimal self-signed CA certificate PEM for use in tests.
2963func generateTestCACertPEM () []byte {
3064 priv , err := ecdsa .GenerateKey (elliptic .P256 (), rand .Reader )
@@ -443,6 +477,101 @@ var _ = Describe("AuthProvider", func() {
443477 })
444478 })
445479
480+ Context ("when uaa_public_key is configured (JWT RS256 verification)" , func () {
481+ // Mirrors Ruby UAAToken decode_options: when uaa_public_key is non-empty,
482+ // the fetched token's RS256 signature is verified against the public key.
483+ var (
484+ rsaPrivKey * rsa.PrivateKey
485+ pubKeyPEM []byte
486+ )
487+
488+ BeforeEach (func () {
489+ rsaPrivKey , pubKeyPEM = generateRSAKeyPair ()
490+ })
491+
492+ It ("accepts a token whose RS256 signature matches uaa_public_key" , func () {
493+ validJWT := createRS256JWT (rsaPrivKey , time .Now ().Add (time .Hour ))
494+ uaaServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
495+ w .Header ().Set ("Content-Type" , "application/json" )
496+ fmt .Fprintf (w , `{"access_token":%q,"token_type":"bearer","expires_in":3600}` , validJWT )
497+ }))
498+ defer uaaServer .Close ()
499+
500+ info := authprovider.InfoResponse {
501+ UserAuthentication : & authprovider.UserAuthentication {
502+ Type : "uaa" ,
503+ Options : authprovider.UAAOptions {URL : uaaServer .URL },
504+ },
505+ }
506+ cfg := config.DirectorConfig {
507+ ClientID : "fake-client" ,
508+ ClientSecret : "fake-secret" ,
509+ UAAPublicKey : string (pubKeyPEM ),
510+ }
511+ provider := authprovider .New (info , cfg , logger )
512+
513+ header , err := provider .AuthHeader (context .Background ())
514+ Expect (err ).NotTo (HaveOccurred ())
515+ Expect (header ).To (Equal ("Bearer " + validJWT ))
516+ })
517+
518+ It ("rejects a token signed with a different RSA key" , func () {
519+ otherKey , _ := generateRSAKeyPair ()
520+ wrongJWT := createRS256JWT (otherKey , time .Now ().Add (time .Hour ))
521+
522+ uaaServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
523+ w .Header ().Set ("Content-Type" , "application/json" )
524+ fmt .Fprintf (w , `{"access_token":%q,"token_type":"bearer","expires_in":3600}` , wrongJWT )
525+ }))
526+ defer uaaServer .Close ()
527+
528+ info := authprovider.InfoResponse {
529+ UserAuthentication : & authprovider.UserAuthentication {
530+ Type : "uaa" ,
531+ Options : authprovider.UAAOptions {URL : uaaServer .URL },
532+ },
533+ }
534+ cfg := config.DirectorConfig {
535+ ClientID : "fake-client" ,
536+ ClientSecret : "fake-secret" ,
537+ UAAPublicKey : string (pubKeyPEM ),
538+ }
539+ provider := authprovider .New (info , cfg , logger )
540+
541+ _ , err := provider .AuthHeader (context .Background ())
542+ Expect (err ).To (HaveOccurred ())
543+ Expect (err .Error ()).To (ContainSubstring ("UAA JWT signature verification failed" ))
544+ })
545+
546+ It ("skips JWT verification when uaa_public_key is empty (matching Ruby verify:false path)" , func () {
547+ // With an empty uaa_public_key the token is used as-is regardless of
548+ // whether it is a valid JWT, matching Ruby's { verify: false } behaviour.
549+ uaaServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
550+ w .Header ().Set ("Content-Type" , "application/json" )
551+ // Return a plain opaque string, not a JWT — verification must be skipped.
552+ fmt .Fprint (w , `{"access_token":"opaque-token","token_type":"bearer","expires_in":3600}` )
553+ }))
554+ defer uaaServer .Close ()
555+
556+ info := authprovider.InfoResponse {
557+ UserAuthentication : & authprovider.UserAuthentication {
558+ Type : "uaa" ,
559+ Options : authprovider.UAAOptions {URL : uaaServer .URL },
560+ },
561+ }
562+ cfg := config.DirectorConfig {
563+ ClientID : "fake-client" ,
564+ ClientSecret : "fake-secret" ,
565+ UAAPublicKey : "" ,
566+ }
567+ provider := authprovider .New (info , cfg , logger )
568+
569+ header , err := provider .AuthHeader (context .Background ())
570+ Expect (err ).NotTo (HaveOccurred ())
571+ Expect (header ).To (Equal ("Bearer opaque-token" ))
572+ })
573+ })
574+
446575 Context ("when director is in non-UAA mode" , func () {
447576 It ("returns Basic authentication string with username and password" , func () {
448577 info := authprovider.InfoResponse {}
0 commit comments