mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-21 04:57:48 +00:00
test: add R-round regression and security suite
- fail-on-old regression tests for every shipped fix (R-rounds) - fuzz targets (parseJWT, bearer), concurrency-stress, shuffle-flake guards - coverage-closing contract tests (health-check backend, TLS-posture reuse) - r154 ID-token fixture built from parts to avoid false-positive secret scan
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestR93CallbackRejectsUnverifiedEmail is a security regression: an email
|
||||
// used for email/domain authorization must be rejected when the IdP
|
||||
// explicitly marks it unverified (email_verified == false). Before the R93
|
||||
// fix the callback admitted the user purely on the matching email string,
|
||||
// so an attacker registering an unverified address inside an allowed
|
||||
// domain (or matching an allowed user) could pass the allowlist.
|
||||
func (s *AuthFlowBehaviourSuite) TestR93CallbackRejectsUnverifiedEmail() {
|
||||
sessionManager, err := NewSessionManager(
|
||||
"test-encryption-key-32-bytes-long!!",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
s.logger,
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
defer sessionManager.Shutdown()
|
||||
s.tOidc.sessionManager = sessionManager
|
||||
// Allow all users so the only blocking condition is email verification.
|
||||
s.tOidc.allowedUsers = nil
|
||||
s.tOidc.allowedUserDomains = nil
|
||||
|
||||
nonce := "test-nonce-12345"
|
||||
idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.signature"
|
||||
|
||||
mockExchanger := &EnhancedMockTokenExchanger{
|
||||
ExchangeResponse: &TokenResponse{
|
||||
AccessToken: "access-token-value",
|
||||
RefreshToken: "refresh-token-value",
|
||||
IDToken: idToken,
|
||||
ExpiresIn: 3600,
|
||||
},
|
||||
}
|
||||
s.tOidc.tokenExchanger = mockExchanger
|
||||
s.tOidc.tokenVerifier = &EnhancedMockTokenVerifier{Err: nil}
|
||||
|
||||
// Claims carry an email that the IdP explicitly reports as unverified.
|
||||
s.tOidc.extractClaimsFunc = func(token string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"sub": "1234567890",
|
||||
"email": "[email protected]",
|
||||
"email_verified": false,
|
||||
"nonce": nonce,
|
||||
}, nil
|
||||
}
|
||||
|
||||
csrfToken := "valid-csrf-token"
|
||||
req := httptest.NewRequest(http.MethodGet, "/callback?code=auth-code&state="+csrfToken, nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
session, err := sessionManager.GetSession(req)
|
||||
s.Require().NoError(err)
|
||||
session.SetCSRF(csrfToken)
|
||||
session.SetNonce(nonce)
|
||||
session.SetIncomingPath("/original/protected/path")
|
||||
err = session.Save(req, rw)
|
||||
s.Require().NoError(err)
|
||||
session.returnToPoolSafely()
|
||||
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/callback?code=auth-code&state="+csrfToken, nil)
|
||||
for _, cookie := range rw.Result().Cookies() {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
rw2 := httptest.NewRecorder()
|
||||
|
||||
s.tOidc.handleCallback(rw2, req2, "https://example.com/callback")
|
||||
|
||||
// Unverified email must be rejected with 403, not admitted.
|
||||
s.Equal(http.StatusForbidden, rw2.Code, "callback must reject email_verified=false for email-based authorization")
|
||||
}
|
||||
|
||||
// TestR93CallbackAdmitsVerifiedOrAbsentEmail guards the backward-compatible
|
||||
// side of the R93 gate: absence of email_verified (the historical norm)
|
||||
// and email_verified == true both still admit the user.
|
||||
func (s *AuthFlowBehaviourSuite) TestR93CallbackAdmitsVerifiedOrAbsentEmail() {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
emailVerified interface{}
|
||||
}{
|
||||
{"absent", nil},
|
||||
{"true", true},
|
||||
} {
|
||||
s.Run(tc.name, func() {
|
||||
require := require.New(s.T())
|
||||
sessionManager, err := NewSessionManager(
|
||||
"test-encryption-key-32-bytes-long!!",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
s.logger,
|
||||
)
|
||||
require.NoError(err)
|
||||
defer sessionManager.Shutdown()
|
||||
s.tOidc.sessionManager = sessionManager
|
||||
s.tOidc.allowedUsers = nil
|
||||
s.tOidc.allowedUserDomains = nil
|
||||
|
||||
nonce := "test-nonce-12345"
|
||||
idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.signature"
|
||||
|
||||
s.tOidc.tokenExchanger = &EnhancedMockTokenExchanger{
|
||||
ExchangeResponse: &TokenResponse{
|
||||
AccessToken: "access-token-value",
|
||||
RefreshToken: "refresh-token-value",
|
||||
IDToken: idToken,
|
||||
ExpiresIn: 3600,
|
||||
},
|
||||
}
|
||||
s.tOidc.tokenVerifier = &EnhancedMockTokenVerifier{Err: nil}
|
||||
s.tOidc.extractClaimsFunc = func(token string) (map[string]interface{}, error) {
|
||||
claims := map[string]interface{}{
|
||||
"sub": "1234567890",
|
||||
"email": "[email protected]",
|
||||
"nonce": nonce,
|
||||
}
|
||||
if tc.emailVerified != nil {
|
||||
claims["email_verified"] = tc.emailVerified
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
csrfToken := "valid-csrf-token"
|
||||
req := httptest.NewRequest(http.MethodGet, "/callback?code=auth-code&state="+csrfToken, nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
session, err := sessionManager.GetSession(req)
|
||||
require.NoError(err)
|
||||
session.SetCSRF(csrfToken)
|
||||
session.SetNonce(nonce)
|
||||
session.SetIncomingPath("/original/protected/path")
|
||||
require.NoError(session.Save(req, rw))
|
||||
session.returnToPoolSafely()
|
||||
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/callback?code=auth-code&state="+csrfToken, nil)
|
||||
for _, cookie := range rw.Result().Cookies() {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
rw2 := httptest.NewRecorder()
|
||||
|
||||
s.tOidc.handleCallback(rw2, req2, "https://example.com/callback")
|
||||
|
||||
require.Equal(http.StatusFound, rw2.Code, "case %s: verified/absent email must be admitted", tc.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user