@@ -2,6 +2,10 @@ package samlsp
22
33import (
44 "bytes"
5+ "crypto"
6+ "crypto/ecdsa"
7+ "crypto/elliptic"
8+ "crypto/rand"
59 "crypto/rsa"
610 "crypto/sha256"
711 "crypto/x509"
@@ -17,6 +21,7 @@ import (
1721 "testing"
1822 "time"
1923
24+ "github.com/golang-jwt/jwt/v5"
2025 dsig "github.com/russellhaering/goxmldsig"
2126 "gotest.tools/assert"
2227 is "gotest.tools/assert/cmp"
@@ -520,3 +525,158 @@ func TestMiddlewareHandlesInvalidResponse(t *testing.T) {
520525 assert .Check (t , is .Equal ("" , resp .Header ().Get ("Location" )))
521526 assert .Check (t , is .Equal ("" , resp .Header ().Get ("Set-Cookie" )))
522527}
528+
529+ type mockSigner struct {
530+ signer crypto.Signer
531+ }
532+
533+ func (m * mockSigner ) Public () crypto.PublicKey {
534+ return m .signer .Public ()
535+ }
536+
537+ func (m * mockSigner ) Sign (rand io.Reader , digest []byte , opts crypto.SignerOpts ) ([]byte , error ) {
538+ return m .signer .Sign (rand , digest , opts )
539+ }
540+
541+ func newMockRSASigner (t * testing.T ) crypto.Signer {
542+ key := mustParsePrivateKey (golden .Get (t , "key.pem" ))
543+ return & mockSigner {signer : key .(crypto.Signer )}
544+ }
545+
546+ func TestMiddleware_WithCryptoSignerE2E (t * testing.T ) {
547+ saml .TimeNow = func () time.Time {
548+ rv , _ := time .Parse ("Mon Jan 2 15:04:05.999999999 MST 2006" , "Mon Dec 1 01:57:09.123456789 UTC 2015" )
549+ return rv
550+ }
551+ saml .Clock = dsig .NewFakeClockAt (saml .TimeNow ())
552+ saml .RandReader = & testRandomReader {}
553+
554+ cert := mustParseCertificate (golden .Get (t , "cert.pem" ))
555+ idpMetadata := golden .Get (t , "idp_metadata.xml" )
556+
557+ var metadata saml.EntityDescriptor
558+ if err := xml .Unmarshal (idpMetadata , & metadata ); err != nil {
559+ panic (err )
560+ }
561+
562+ mockSigner := newMockRSASigner (t )
563+
564+ opts := Options {
565+ URL : mustParseURL ("https://15661444.ngrok.io/" ),
566+ Key : mockSigner ,
567+ Certificate : cert ,
568+ IDPMetadata : & metadata ,
569+ }
570+
571+ middleware , err := New (opts )
572+ assert .Check (t , err )
573+
574+ sessionProvider := DefaultSessionProvider (opts )
575+ sessionProvider .Name = "ttt"
576+ sessionProvider .MaxAge = 7200 * time .Second
577+
578+ sessionCodec := sessionProvider .Codec .(JWTSessionCodec )
579+ sessionCodec .MaxAge = 7200 * time .Second
580+ sessionProvider .Codec = sessionCodec
581+
582+ middleware .Session = sessionProvider
583+ middleware .ServiceProvider .MetadataURL .Path = "/saml2/metadata"
584+ middleware .ServiceProvider .AcsURL .Path = "/saml2/acs"
585+ middleware .ServiceProvider .SloURL .Path = "/saml2/slo"
586+
587+ t .Run ("SessionEncodeDecode" , func (t * testing.T ) {
588+ var tc JWTSessionClaims
589+ if err := json .Unmarshal (golden .Get (t , "token.json" ), & tc ); err != nil {
590+ t .Fatal (err )
591+ }
592+
593+ encoded , err := sessionProvider .Codec .Encode (tc )
594+ assert .Check (t , err )
595+ assert .Assert (t , encoded != "" )
596+
597+ decoded , err := sessionProvider .Codec .Decode (encoded )
598+ assert .Check (t , err )
599+ decodedClaims := decoded .(JWTSessionClaims )
600+ assert .Equal (t , tc .Subject , decodedClaims .Subject )
601+ })
602+
603+ t .Run ("TrackedRequestEncodeDecode" , func (t * testing.T ) {
604+ codec := middleware .RequestTracker .(CookieRequestTracker ).Codec
605+ trackedReq := TrackedRequest {
606+ Index : "test-index" ,
607+ SAMLRequestID : "test-request-id" ,
608+ URI : "/test-uri" ,
609+ }
610+
611+ encoded , err := codec .Encode (trackedReq )
612+ assert .Check (t , err )
613+ assert .Assert (t , encoded != "" )
614+
615+ decoded , err := codec .Decode (encoded )
616+ assert .Check (t , err )
617+ assert .Equal (t , trackedReq .Index , decoded .Index )
618+ assert .Equal (t , trackedReq .SAMLRequestID , decoded .SAMLRequestID )
619+ })
620+
621+ t .Run ("RequireAccountFlow" , func (t * testing.T ) {
622+ handler := middleware .RequireAccount (
623+ http .HandlerFunc (func (_ http.ResponseWriter , _ * http.Request ) {
624+ panic ("not reached" )
625+ }))
626+
627+ req , _ := http .NewRequest ("GET" , "/protected" , nil )
628+ resp := httptest .NewRecorder ()
629+ handler .ServeHTTP (resp , req )
630+
631+ assert .Check (t , is .Equal (http .StatusFound , resp .Code ))
632+ assert .Assert (t , resp .Header ().Get ("Location" ) != "" )
633+ assert .Assert (t , resp .Header ().Get ("Set-Cookie" ) != "" )
634+ })
635+
636+ t .Run ("Metadata" , func (t * testing.T ) {
637+ req , _ := http .NewRequest ("GET" , "/saml2/metadata" , nil )
638+ resp := httptest .NewRecorder ()
639+ middleware .ServeHTTP (resp , req )
640+
641+ assert .Check (t , is .Equal (http .StatusOK , resp .Code ))
642+ assert .Check (t , is .Equal ("application/samlmetadata+xml" ,
643+ resp .Header ().Get ("Content-type" )))
644+ golden .Assert (t , resp .Body .String (), "expected_middleware_metadata.xml" )
645+ })
646+ }
647+
648+ func TestJWTSessionCodec_NonRSACryptoSignerReturnsError (t * testing.T ) {
649+ now := time .Now ()
650+ saml .TimeNow = func () time.Time {
651+ return now
652+ }
653+
654+ ecKey , err := ecdsa .GenerateKey (elliptic .P256 (), rand .Reader )
655+ assert .Check (t , err )
656+
657+ signer := & mockSigner {signer : ecKey }
658+
659+ audience := "https://example.com/"
660+ codec := JWTSessionCodec {
661+ SigningMethod : jwt .SigningMethodES256 ,
662+ Audience : audience ,
663+ Issuer : audience ,
664+ MaxAge : time .Hour ,
665+ Key : signer ,
666+ }
667+
668+ tc := JWTSessionClaims {
669+ RegisteredClaims : jwt.RegisteredClaims {
670+ Audience : jwt.ClaimStrings {audience },
671+ Issuer : audience ,
672+ Subject : "test" ,
673+ IssuedAt : jwt .NewNumericDate (now ),
674+ ExpiresAt : jwt .NewNumericDate (now .Add (time .Hour )),
675+ NotBefore : jwt .NewNumericDate (now ),
676+ },
677+ SAMLSession : true ,
678+ }
679+
680+ _ , err = codec .Encode (tc )
681+ assert .Check (t , is .ErrorContains (err , "crypto.Signer must hold an RSA key" ))
682+ }
0 commit comments