44 "bytes"
55 "compress/flate"
66 "context"
7+ "crypto"
8+ "crypto/rand"
9+ "crypto/rsa"
710 "crypto/tls"
811 "crypto/x509"
912 "encoding/base64"
@@ -1268,7 +1271,14 @@ func signXMLDocument(t *testing.T, doc *etree.Document) []byte {
12681271 return out
12691272}
12701273
1271- func TestHandleLogoutCallbackSignatureValidation (t * testing.T ) {
1274+ func postSAMLResponse (encoded string ) * http.Request {
1275+ form := url.Values {"SAMLResponse" : {encoded }}
1276+ req := httptest .NewRequest (http .MethodPost , "/logout/callback" , strings .NewReader (form .Encode ()))
1277+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
1278+ return req
1279+ }
1280+
1281+ func TestHandleLogoutCallbackPOSTSignatureValidation (t * testing.T ) {
12721282 conn , err := (& Config {
12731283 CA : "testdata/ca.crt" ,
12741284 UsernameAttr : "Name" ,
@@ -1287,19 +1297,16 @@ func TestHandleLogoutCallbackSignatureValidation(t *testing.T) {
12871297 t .Fatal (err )
12881298 }
12891299 signedXML := signXMLDocument (t , doc )
1290-
12911300 encoded := base64 .StdEncoding .EncodeToString (signedXML )
1292- req := httptest . NewRequest ( http . MethodGet , "/logout/callback?SAMLResponse=" + url . QueryEscape ( encoded ), nil )
1293- if err := conn .HandleLogoutCallback (context .Background (), req ); err != nil {
1301+
1302+ if err := conn .HandleLogoutCallback (context .Background (), postSAMLResponse ( encoded ) ); err != nil {
12941303 t .Errorf ("expected no error for validly signed response, got: %v" , err )
12951304 }
12961305 })
12971306
12981307 t .Run ("InvalidSignature" , func (t * testing.T ) {
1299- // Use unsigned XML — should fail signature validation
13001308 encoded := base64 .StdEncoding .EncodeToString ([]byte (successLogoutResponseXML ))
1301- req := httptest .NewRequest (http .MethodGet , "/logout/callback?SAMLResponse=" + url .QueryEscape (encoded ), nil )
1302- if err := conn .HandleLogoutCallback (context .Background (), req ); err == nil {
1309+ if err := conn .HandleLogoutCallback (context .Background (), postSAMLResponse (encoded )); err == nil {
13031310 t .Error ("expected error for unsigned response when signature validation is enabled" )
13041311 }
13051312 })
@@ -1322,15 +1329,112 @@ func TestHandleLogoutCallbackSignatureValidation(t *testing.T) {
13221329 t .Fatal (err )
13231330 }
13241331 signedXML := signXMLDocument (t , doc )
1325-
13261332 encoded := base64 .StdEncoding .EncodeToString (signedXML )
1327- req := httptest . NewRequest ( http . MethodGet , "/logout/callback?SAMLResponse=" + url . QueryEscape ( encoded ), nil )
1328- if err := connBadCA .HandleLogoutCallback (context .Background (), req ); err == nil {
1333+
1334+ if err := connBadCA .HandleLogoutCallback (context .Background (), postSAMLResponse ( encoded ) ); err == nil {
13291335 t .Error ("expected error when response is signed with different CA" )
13301336 }
13311337 })
13321338}
13331339
1340+ // signRedirectBinding builds a complete URL for a GET LogoutResponse with
1341+ // SAML HTTP-Redirect binding signature. The XML is deflated, base64-encoded,
1342+ // and a query-string RSA-SHA256 signature is appended.
1343+ func signRedirectBinding (t * testing.T , xmlPayload string , keyFile , certFile string ) string {
1344+ t .Helper ()
1345+
1346+ var buf bytes.Buffer
1347+ fw , err := flate .NewWriter (& buf , flate .DefaultCompression )
1348+ if err != nil {
1349+ t .Fatalf ("deflate writer: %v" , err )
1350+ }
1351+ if _ , err := fw .Write ([]byte (xmlPayload )); err != nil {
1352+ t .Fatalf ("deflate write: %v" , err )
1353+ }
1354+ if err := fw .Close (); err != nil {
1355+ t .Fatalf ("deflate close: %v" , err )
1356+ }
1357+ samlResp := base64 .StdEncoding .EncodeToString (buf .Bytes ())
1358+
1359+ sigAlg := "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
1360+ signedContent := "SAMLResponse=" + url .QueryEscape (samlResp ) +
1361+ "&SigAlg=" + url .QueryEscape (sigAlg )
1362+
1363+ tlsCert , err := tls .LoadX509KeyPair (certFile , keyFile )
1364+ if err != nil {
1365+ t .Fatalf ("load key pair: %v" , err )
1366+ }
1367+ rsaKey , ok := tlsCert .PrivateKey .(* rsa.PrivateKey )
1368+ if ! ok {
1369+ t .Fatal ("test key is not RSA" )
1370+ }
1371+
1372+ h := crypto .SHA256 .New ()
1373+ h .Write ([]byte (signedContent ))
1374+ sig , err := rsa .SignPKCS1v15 (rand .Reader , rsaKey , crypto .SHA256 , h .Sum (nil ))
1375+ if err != nil {
1376+ t .Fatalf ("sign: %v" , err )
1377+ }
1378+ sigB64 := base64 .StdEncoding .EncodeToString (sig )
1379+
1380+ return "/logout/callback?" + signedContent +
1381+ "&Signature=" + url .QueryEscape (sigB64 )
1382+ }
1383+
1384+ func TestHandleLogoutCallbackRedirectSignatureValidation (t * testing.T ) {
1385+ conn , err := (& Config {
1386+ CA : "testdata/ca.crt" ,
1387+ UsernameAttr : "Name" ,
1388+ EmailAttr : "email" ,
1389+ RedirectURI : "http://127.0.0.1:5556/dex/callback" ,
1390+ SSOURL : "http://foo.bar/" ,
1391+ InsecureSkipSLOSignatureValidation : false ,
1392+ }).openConnector (slog .New (slog .DiscardHandler ))
1393+ if err != nil {
1394+ t .Fatal (err )
1395+ }
1396+
1397+ t .Run ("ValidSignature" , func (t * testing.T ) {
1398+ u := signRedirectBinding (t , successLogoutResponseXML , "testdata/ca.key" , "testdata/ca.crt" )
1399+ req := httptest .NewRequest (http .MethodGet , u , nil )
1400+ if err := conn .HandleLogoutCallback (context .Background (), req ); err != nil {
1401+ t .Errorf ("expected no error, got: %v" , err )
1402+ }
1403+ })
1404+
1405+ t .Run ("MissingSignature" , func (t * testing.T ) {
1406+ var buf bytes.Buffer
1407+ fw , _ := flate .NewWriter (& buf , flate .DefaultCompression )
1408+ fw .Write ([]byte (successLogoutResponseXML ))
1409+ fw .Close ()
1410+ encoded := base64 .StdEncoding .EncodeToString (buf .Bytes ())
1411+
1412+ req := httptest .NewRequest (http .MethodGet ,
1413+ "/logout/callback?SAMLResponse=" + url .QueryEscape (encoded ), nil )
1414+ if err := conn .HandleLogoutCallback (context .Background (), req ); err == nil {
1415+ t .Error ("expected error for missing Signature parameter" )
1416+ }
1417+ })
1418+
1419+ t .Run ("WrongCA" , func (t * testing.T ) {
1420+ u := signRedirectBinding (t , successLogoutResponseXML , "testdata/bad-ca.key" , "testdata/bad-ca.crt" )
1421+ req := httptest .NewRequest (http .MethodGet , u , nil )
1422+ if err := conn .HandleLogoutCallback (context .Background (), req ); err == nil {
1423+ t .Error ("expected error when signed with wrong CA" )
1424+ }
1425+ })
1426+
1427+ t .Run ("TamperedPayload" , func (t * testing.T ) {
1428+ u := signRedirectBinding (t , successLogoutResponseXML , "testdata/ca.key" , "testdata/ca.crt" )
1429+ // Replace part of the SAMLResponse value to simulate tampering.
1430+ u = strings .Replace (u , "SAMLResponse=" , "SAMLResponse=AAAA" , 1 )
1431+ req := httptest .NewRequest (http .MethodGet , u , nil )
1432+ if err := conn .HandleLogoutCallback (context .Background (), req ); err == nil {
1433+ t .Error ("expected error for tampered payload" )
1434+ }
1435+ })
1436+ }
1437+
13341438func TestSLOEndToEnd (t * testing.T ) {
13351439 c := Config {
13361440 CA : "testdata/ca.crt" ,
0 commit comments