451da3d26d
Support for service accounts with static credentials * feature complete * tests and docs * lint fix
624 lines
20 KiB
Go
624 lines
20 KiB
Go
package cognitoauth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// mockConfigProvider implements the ConfigProvider interface for testing purposes.
|
|
// It provides predefined values for configuration methods and no-op implementations
|
|
// for setters, allowing tests to run without requiring actual AWS Cognito credentials.
|
|
type mockConfigProvider struct {
|
|
region string
|
|
userPoolID string
|
|
noJWTValidation bool
|
|
svcUserPoolID string
|
|
svcAppClientID string
|
|
svcJwksURL string
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetAuthRegion() string { return m.region }
|
|
func (m *mockConfigProvider) SetAuthRegion(s string) { m.region = s }
|
|
func (m *mockConfigProvider) GetAuthUserPoolID() string { return m.userPoolID }
|
|
func (m *mockConfigProvider) SetAuthUserPoolID(s string) { m.userPoolID = s }
|
|
func (m *mockConfigProvider) GetAuthClientSecret() string { return "mock-secret" }
|
|
func (m *mockConfigProvider) SetAuthClientSecret(s string) {}
|
|
func (m *mockConfigProvider) GetAuthDomain() string { return "mock-domain" }
|
|
func (m *mockConfigProvider) SetAuthDomain(s string) {}
|
|
func (m *mockConfigProvider) GetAuthClientID() string { return "mock-client-id" }
|
|
func (m *mockConfigProvider) SetAuthClientID(s string) {}
|
|
func (m *mockConfigProvider) GetAuthRedirectURI() string { return "http://localhost/callback" }
|
|
func (m *mockConfigProvider) SetAuthRedirectURI(s string) {}
|
|
func (m *mockConfigProvider) GetAuthTokenURL() string { return "http://localhost/token" }
|
|
func (m *mockConfigProvider) SetAuthTokenURL(s string) {}
|
|
func (m *mockConfigProvider) GetAuthJwksURL() string { return "http://localhost/jwks" }
|
|
func (m *mockConfigProvider) SetAuthJwksURL(s string) {}
|
|
func (m *mockConfigProvider) GetAuthURL() string { return "http://localhost/auth" }
|
|
func (m *mockConfigProvider) SetAuthURL(s string) {}
|
|
func (m *mockConfigProvider) GetAuthLogoutURL() string { return "http://localhost/logout" }
|
|
func (m *mockConfigProvider) SetAuthLogoutURL(s string) {}
|
|
func (m *mockConfigProvider) GetAuthLoginPath() string { return "/login" }
|
|
func (m *mockConfigProvider) SetAuthLoginPath(s string) {}
|
|
func (m *mockConfigProvider) GetAuthCallbackPath() string { return "/callback" }
|
|
func (m *mockConfigProvider) SetAuthCallbackPath(s string) {}
|
|
func (m *mockConfigProvider) GetAuthHomePath() string { return "/home" }
|
|
func (m *mockConfigProvider) SetAuthHomePath(s string) {}
|
|
func (m *mockConfigProvider) GetAuthLogoutPath() string { return "/logout" }
|
|
func (m *mockConfigProvider) SetAuthLogoutPath(s string) {}
|
|
func (m *mockConfigProvider) GetHealthPath() string { return "/health" }
|
|
func (m *mockConfigProvider) SetHealthPath(s string) {}
|
|
func (m *mockConfigProvider) GetAuthLogger() *slog.Logger { return slog.Default() }
|
|
func (m *mockConfigProvider) SetAuthLogger(*slog.Logger) {}
|
|
func (m *mockConfigProvider) GetAuthRoutePermissions() map[string][]string {
|
|
return map[string][]string{"/test": {"admin"}}
|
|
}
|
|
func (m *mockConfigProvider) SetAuthRoutePermissions(map[string][]string) {}
|
|
func (m *mockConfigProvider) GetPermitIOAPIKey() string {
|
|
return "permit_key_test_mock_key_for_testing_purposes_only"
|
|
}
|
|
func (m *mockConfigProvider) SetPermitIOAPIKey(s string) {}
|
|
func (m *mockConfigProvider) GetPermitIOPDPURL() string { return "http://localhost:7766" }
|
|
func (m *mockConfigProvider) SetPermitIOPDPURL(s string) {}
|
|
func (m *mockConfigProvider) GetCognitoEnvironmentID() string { return "" }
|
|
func (m *mockConfigProvider) SetCognitoEnvironmentID(string) {}
|
|
func (m *mockConfigProvider) InitializeAuthConfig(string, *slog.Logger) error {
|
|
return nil
|
|
}
|
|
func (m *mockConfigProvider) GetNoJWTValidation() bool { return m.noJWTValidation }
|
|
func (m *mockConfigProvider) SetNoJWTValidation(val bool) { m.noJWTValidation = val }
|
|
|
|
// Service-accounts pool fields default to empty (feature disabled) for the
|
|
// mock. Tests that exercise multi-issuer flows set them explicitly.
|
|
func (m *mockConfigProvider) GetAuthSvcUserPoolID() string { return m.svcUserPoolID }
|
|
func (m *mockConfigProvider) SetAuthSvcUserPoolID(s string) { m.svcUserPoolID = s }
|
|
func (m *mockConfigProvider) GetAuthSvcAppClientID() string { return m.svcAppClientID }
|
|
func (m *mockConfigProvider) SetAuthSvcAppClientID(s string) {
|
|
m.svcAppClientID = s
|
|
}
|
|
func (m *mockConfigProvider) GetAuthSvcJwksURL() string { return m.svcJwksURL }
|
|
func (m *mockConfigProvider) SetAuthSvcJwksURL(s string) { m.svcJwksURL = s }
|
|
|
|
// Helper function to generate test JWT and JWK
|
|
|
|
// TestGetUserGroups verifies the GetUserGroups function correctly extracts user group
|
|
// information from JWT claims. It tests:
|
|
// - Extraction from "cognito:groups" claim as an array
|
|
// - Extraction from "groups" claim as an array
|
|
// - Extraction from "custom:groups" claim as a string
|
|
// - Error handling when no group information is present
|
|
// - Handling of mixed data types in group arrays
|
|
func TestGetUserGroups(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
claims map[string]interface{}
|
|
expected []string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "cognito:groups as string array",
|
|
claims: map[string]interface{}{
|
|
"cognito:groups": []interface{}{"admin", "user"},
|
|
},
|
|
expected: []string{"admin", "user"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "groups as string array",
|
|
claims: map[string]interface{}{
|
|
"groups": []string{"developer", "tester"},
|
|
},
|
|
expected: []string{"developer", "tester"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "custom:groups as string",
|
|
claims: map[string]interface{}{
|
|
"custom:groups": "single-group",
|
|
},
|
|
expected: []string{"single-group"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "no groups in claims",
|
|
claims: map[string]interface{}{},
|
|
expected: nil,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "groups with mixed types",
|
|
claims: map[string]interface{}{
|
|
"cognito:groups": []interface{}{"admin", 123, true},
|
|
},
|
|
expected: []string{"admin", "123", "true"},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
groups, err := GetUserGroups(tt.claims)
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Errorf("GetUserGroups() expected error, got nil")
|
|
}
|
|
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("GetUserGroups() unexpected error: %v", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(groups, tt.expected) {
|
|
t.Errorf("GetUserGroups() got = %v, want %v", groups, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCreateCodeChallenge verifies the createCodeChallenge function correctly
|
|
// generates PKCE code challenges from code verifiers according to OAuth 2.0 PKCE
|
|
// (RFC 7636) specifications. It tests multiple input/output pairs to ensure the
|
|
// SHA-256 hashing and Base64-URL encoding are implemented correctly.
|
|
func TestCreateCodeChallenge(t *testing.T) {
|
|
tests := []struct {
|
|
verifier string
|
|
challenge string
|
|
}{
|
|
{
|
|
verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
},
|
|
{
|
|
verifier: "abcdefghijklmnopqrstuvwxyz0123456789",
|
|
challenge: "AR_CmU450lEUFUD4emkJKz8iqGdn9yg95-7ts4l77fY",
|
|
},
|
|
{
|
|
verifier: "test-verifier",
|
|
challenge: "JBbiqONGWPaAmwXk_8bT6UnlPfrn65D32eZlJS-zGG0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.verifier, func(t *testing.T) {
|
|
challenge := createCodeChallenge(tt.verifier)
|
|
if challenge != tt.challenge {
|
|
t.Errorf("createCodeChallenge(%q) = %q, want %q", tt.verifier, challenge, tt.challenge)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGenerateRandomString verifies the generateRandomString function produces
|
|
// cryptographically secure random strings of the requested length. It tests:
|
|
// - Generation of strings with various lengths
|
|
// - Correct string length
|
|
// - Base64-URL encoding validity
|
|
// - String uniqueness (randomness)
|
|
func TestGenerateRandomString(t *testing.T) {
|
|
lengths := []int{32, 43, 64, 128}
|
|
|
|
for _, length := range lengths {
|
|
t.Run(fmt.Sprintf("length=%d", length), func(t *testing.T) {
|
|
// Generate multiple strings to check for uniqueness
|
|
strings := make([]string, 5)
|
|
for i := 0; i < 5; i++ {
|
|
s, err := generateRandomString(length)
|
|
if err != nil {
|
|
t.Errorf("generateRandomString(%d) unexpected error: %v", length, err)
|
|
}
|
|
|
|
// Check length
|
|
if len(s) != length {
|
|
t.Errorf("generateRandomString(%d) = %q (length %d), want length %d",
|
|
length, s, len(s), length)
|
|
}
|
|
|
|
// Check if it's base64url valid
|
|
_, err = base64.RawURLEncoding.DecodeString(s)
|
|
if err != nil {
|
|
t.Errorf("generateRandomString(%d) = %q, not valid base64url: %v",
|
|
length, s, err)
|
|
}
|
|
|
|
strings[i] = s
|
|
}
|
|
|
|
// Check for duplicates (very unlikely but possible)
|
|
for i := 0; i < len(strings); i++ {
|
|
for j := i + 1; j < len(strings); j++ {
|
|
if strings[i] == strings[j] {
|
|
t.Errorf("generateRandomString(%d) generated duplicate strings: %q",
|
|
length, strings[i])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPKCESessionManagement verifies the PKCE session management functions work correctly.
|
|
// It tests:
|
|
// - Storing and retrieving valid code verifier sessions
|
|
// - Error handling when retrieving non-existent sessions
|
|
// - Proper expiration of sessions after their timeout period
|
|
// - Automatic cleanup of expired sessions during store operations
|
|
// - Persistence of valid sessions during cleanup
|
|
func TestPKCESessionManagement(t *testing.T) {
|
|
// Setup logger
|
|
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})
|
|
logger := slog.New(logHandler)
|
|
|
|
// Test storing and retrieving a valid session
|
|
t.Run("store and retrieve valid session", func(t *testing.T) {
|
|
state := "test-state-valid"
|
|
codeVerifier := "test-verifier-valid"
|
|
|
|
// Store the session
|
|
storePKCESession(state, codeVerifier, logger)
|
|
|
|
// Retrieve it
|
|
retrievedVerifier, err := getCodeVerifier(state, logger)
|
|
|
|
if err != nil {
|
|
t.Errorf("getCodeVerifier(%q) unexpected error: %v", state, err)
|
|
}
|
|
|
|
if retrievedVerifier != codeVerifier {
|
|
t.Errorf("getCodeVerifier(%q) = %q, want %q", state, retrievedVerifier, codeVerifier)
|
|
}
|
|
})
|
|
|
|
// Test retrieving non-existent session
|
|
t.Run("retrieve non-existent session", func(t *testing.T) {
|
|
state := "non-existent-state"
|
|
|
|
_, err := getCodeVerifier(state, logger)
|
|
|
|
if err == nil {
|
|
t.Errorf("getCodeVerifier(%q) expected error for non-existent state", state)
|
|
}
|
|
})
|
|
|
|
// Test expired session
|
|
t.Run("expired session", func(t *testing.T) {
|
|
state := "test-state-expired"
|
|
codeVerifier := "test-verifier-expired"
|
|
|
|
// Manually create an expired session
|
|
pkceSessionMap.Lock()
|
|
pkceSessionMap.sessions[state] = &PKCESession{
|
|
CodeVerifier: codeVerifier,
|
|
CreatedAt: time.Now().Add(-10 * time.Minute),
|
|
ExpiresAt: time.Now().Add(-5 * time.Minute), // Expired 5 minutes ago
|
|
}
|
|
pkceSessionMap.Unlock()
|
|
|
|
_, err := getCodeVerifier(state, logger)
|
|
|
|
if err == nil {
|
|
t.Errorf("getCodeVerifier(%q) expected error for expired session", state)
|
|
}
|
|
})
|
|
|
|
// Test cleanup of expired sessions during store
|
|
t.Run("cleanup expired sessions", func(t *testing.T) {
|
|
// Create several expired sessions
|
|
pkceSessionMap.Lock()
|
|
for i := 0; i < 5; i++ {
|
|
state := fmt.Sprintf("expired-state-%d", i)
|
|
pkceSessionMap.sessions[state] = &PKCESession{
|
|
CodeVerifier: fmt.Sprintf("expired-verifier-%d", i),
|
|
CreatedAt: time.Now().Add(-10 * time.Minute),
|
|
ExpiresAt: time.Now().Add(-1 * time.Minute), // Expired 1 minute ago
|
|
}
|
|
}
|
|
initialCount := len(pkceSessionMap.sessions)
|
|
pkceSessionMap.Unlock()
|
|
|
|
// Store a new session, which should trigger cleanup
|
|
newState := "new-test-state"
|
|
newVerifier := "new-test-verifier"
|
|
storePKCESession(newState, newVerifier, logger)
|
|
|
|
// Check if expired sessions were cleaned up
|
|
pkceSessionMap.RLock()
|
|
cleanedCount := len(pkceSessionMap.sessions)
|
|
pkceSessionMap.RUnlock()
|
|
|
|
if cleanedCount >= initialCount {
|
|
t.Errorf("storePKCESession() did not clean up expired sessions: before=%d, after=%d",
|
|
initialCount, cleanedCount)
|
|
}
|
|
|
|
// Verify the new session is still retrievable
|
|
retrievedVerifier, err := getCodeVerifier(newState, logger)
|
|
if err != nil || retrievedVerifier != newVerifier {
|
|
t.Errorf("New session not retrievable after cleanup: err=%v, verifier=%q",
|
|
err, retrievedVerifier)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestExtractUserInfo verifies the ExtractUserInfo function correctly extracts user
|
|
// information from JWT claims into a UserInfo struct. It tests:
|
|
// - Extraction of complete user information (username, email, groups)
|
|
// - Handling of missing email field
|
|
// - Handling of missing username field
|
|
// - Handling of missing groups field
|
|
// - Handling of completely empty claims
|
|
func TestExtractUserInfo(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
claims map[string]interface{}
|
|
want UserInfo
|
|
}{
|
|
{
|
|
name: "complete user info",
|
|
claims: map[string]interface{}{
|
|
"cognito:username": "testuser",
|
|
"email": "test@example.com",
|
|
"cognito:groups": []interface{}{"admin", "developer"},
|
|
},
|
|
want: UserInfo{
|
|
Username: "testuser",
|
|
Email: "test@example.com",
|
|
Groups: []string{"admin", "developer"},
|
|
},
|
|
},
|
|
{
|
|
name: "missing email",
|
|
claims: map[string]interface{}{
|
|
"cognito:username": "testuser",
|
|
"cognito:groups": []interface{}{"admin"},
|
|
},
|
|
want: UserInfo{
|
|
Username: "testuser",
|
|
Email: "",
|
|
Groups: []string{"admin"},
|
|
},
|
|
},
|
|
{
|
|
name: "missing username",
|
|
claims: map[string]interface{}{
|
|
"email": "test@example.com",
|
|
"cognito:groups": []interface{}{"admin"},
|
|
},
|
|
want: UserInfo{
|
|
Username: "",
|
|
Email: "test@example.com",
|
|
Groups: []string{"admin"},
|
|
},
|
|
},
|
|
{
|
|
name: "missing groups",
|
|
claims: map[string]interface{}{
|
|
"cognito:username": "testuser",
|
|
"email": "test@example.com",
|
|
},
|
|
want: UserInfo{
|
|
Username: "testuser",
|
|
Email: "test@example.com",
|
|
Groups: nil, // GetUserGroups will return error, so Groups will be nil
|
|
},
|
|
},
|
|
{
|
|
name: "empty claims",
|
|
claims: map[string]interface{}{},
|
|
want: UserInfo{
|
|
Username: "",
|
|
Email: "",
|
|
Groups: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "access token claims with username instead of cognito:username",
|
|
claims: map[string]interface{}{
|
|
"sub": "e12bb510-30e1-7062-a69a-ca7f3f38d80e",
|
|
"username": "accesstokenuser",
|
|
"token_use": "access",
|
|
},
|
|
want: UserInfo{
|
|
Username: "accesstokenuser",
|
|
Email: "",
|
|
Groups: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "cognito:username takes precedence over username",
|
|
claims: map[string]interface{}{
|
|
"cognito:username": "idtokenuser",
|
|
"username": "accesstokenuser",
|
|
"email": "user@example.com",
|
|
},
|
|
want: UserInfo{
|
|
Username: "idtokenuser",
|
|
Email: "user@example.com",
|
|
Groups: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ExtractUserInfo(tt.claims)
|
|
|
|
if got.Username != tt.want.Username {
|
|
t.Errorf("ExtractUserInfo() username = %v, want %v", got.Username, tt.want.Username)
|
|
}
|
|
|
|
if got.Email != tt.want.Email {
|
|
t.Errorf("ExtractUserInfo() email = %v, want %v", got.Email, tt.want.Email)
|
|
}
|
|
|
|
// We don't compare Groups directly because GetUserGroups may return an error
|
|
// for the "missing groups" test case, resulting in nil Groups
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVerifyTokenTestMode verifies that the verifyToken function correctly handles
|
|
// test mode when NO_JWT_VALIDATION is enabled. It tests:
|
|
// - Parsing of unsigned JWT tokens without signature validation
|
|
// - Extraction of user information from test tokens
|
|
// - Proper group membership validation
|
|
// - Error handling for malformed tokens
|
|
// verifyGroupMemberships is a helper function to verify group memberships in tests
|
|
func verifyGroupMemberships(t *testing.T, groups []string) {
|
|
// Verify user is in the document group
|
|
hasDocumentGroup := false
|
|
for _, group := range groups {
|
|
if group == "document" {
|
|
hasDocumentGroup = true
|
|
break
|
|
}
|
|
}
|
|
if !hasDocumentGroup {
|
|
t.Errorf("User should be in 'document' group, groups: %v", groups)
|
|
}
|
|
|
|
// Verify user is NOT in the foo group
|
|
hasFooGroup := false
|
|
for _, group := range groups {
|
|
if group == "foo" {
|
|
hasFooGroup = true
|
|
break
|
|
}
|
|
}
|
|
if hasFooGroup {
|
|
t.Errorf("User should NOT be in 'foo' group, groups: %v", groups)
|
|
}
|
|
|
|
// Verify all expected groups are present
|
|
expectedGroups := []string{"uploaders", "querybuilders", "document"}
|
|
for _, expectedGroup := range expectedGroups {
|
|
found := false
|
|
for _, group := range groups {
|
|
if group == expectedGroup {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected group '%s' not found in groups: %v", expectedGroup, groups)
|
|
}
|
|
}
|
|
}
|
|
|
|
// createTestJWTToken creates an unsigned JWT token for testing
|
|
func createTestJWTToken() string {
|
|
// Create a simple unsigned JWT token (header.payload.signature)
|
|
// Header: {"alg":"none","typ":"JWT"}
|
|
header := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" // {"alg":"none","typ":"JWT"}
|
|
|
|
// Create payload JSON and encode it
|
|
payloadJSON := `{
|
|
"sub": "e12bb510-30e1-7062-a69a-ca7f3f38d80e",
|
|
"cognito:groups": ["uploaders", "querybuilders", "document"],
|
|
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_1y6po8rR8",
|
|
"version": 2,
|
|
"client_id": "552cqkf3640t39ncehkmgpce31",
|
|
"origin_jti": "0750d18e-8588-4d12-acbd-754d7279f317",
|
|
"event_id": "89841577-8dd9-4cd4-a698-a890393305ff",
|
|
"token_use": "access",
|
|
"scope": "openid profile email",
|
|
"auth_time": 1747326253,
|
|
"exp": 1747329853,
|
|
"iat": 1747326253,
|
|
"jti": "739302f1-7334-4718-b124-b51fb958af9a",
|
|
"cognito:username": "testuser"
|
|
}`
|
|
|
|
payload := base64.RawURLEncoding.EncodeToString([]byte(payloadJSON))
|
|
|
|
// RFC 7519 Section 6: Unsecured JWTs with alg:none should not have signature part
|
|
// Create a 2-part token (header.payload.) that's RFC compliant
|
|
return fmt.Sprintf("%s.%s.", header, payload)
|
|
}
|
|
|
|
func TestVerifyTokenTestMode(t *testing.T) {
|
|
unsignedToken := createTestJWTToken()
|
|
|
|
// Print the test JWT token for manual integration testing
|
|
t.Logf("=== TEST JWT TOKEN FOR MANUAL TESTING ===")
|
|
t.Logf("Use this unsigned JWT token for integration tests:")
|
|
t.Logf("%s", unsignedToken)
|
|
t.Logf("=== END TEST JWT TOKEN ===")
|
|
|
|
// Also print it to stdout so it's visible even without -v flag
|
|
fmt.Printf("\n=== TEST JWT TOKEN FOR MANUAL TESTING ===\n")
|
|
fmt.Printf("Token: %s\n", unsignedToken)
|
|
fmt.Printf("=== END TEST JWT TOKEN ===\n\n")
|
|
|
|
t.Run("test mode with unsigned token", func(t *testing.T) {
|
|
// Create mock config with test mode enabled
|
|
mockConfig := &mockConfigProvider{
|
|
region: "us-east-2",
|
|
userPoolID: "us-east-2_1y6po8rR8",
|
|
noJWTValidation: true,
|
|
}
|
|
|
|
// Verify the token in test mode (keySet is not used in test mode)
|
|
claims, err := verifyToken(unsignedToken, nil, mockConfig)
|
|
if err != nil {
|
|
t.Errorf("verifyToken() in test mode failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify the subject was extracted correctly
|
|
if sub, ok := claims["sub"].(string); !ok || sub != "e12bb510-30e1-7062-a69a-ca7f3f38d80e" {
|
|
t.Errorf("verifyToken() sub = %v, want %v", claims["sub"], "e12bb510-30e1-7062-a69a-ca7f3f38d80e")
|
|
}
|
|
|
|
// Verify the username was extracted correctly
|
|
if username, ok := claims["cognito:username"].(string); !ok || username != "testuser" {
|
|
t.Errorf("verifyToken() cognito:username = %v, want %v", claims["cognito:username"], "testuser")
|
|
}
|
|
|
|
// Test group extraction
|
|
groups, err := GetUserGroups(claims)
|
|
if err != nil {
|
|
t.Errorf("GetUserGroups() failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Test group memberships using helper function
|
|
verifyGroupMemberships(t, groups)
|
|
})
|
|
|
|
t.Run("normal mode with unsigned token should fail", func(t *testing.T) {
|
|
// Create mock config with test mode disabled
|
|
mockConfig := &mockConfigProvider{
|
|
region: "us-east-2",
|
|
userPoolID: "us-east-2_1y6po8rR8",
|
|
noJWTValidation: false,
|
|
}
|
|
|
|
// This should fail because we're trying to verify an unsigned token in normal mode
|
|
_, err := verifyToken(unsignedToken, nil, mockConfig)
|
|
if err == nil {
|
|
t.Error("verifyToken() in normal mode should fail with unsigned token")
|
|
}
|
|
})
|
|
|
|
t.Run("test mode with malformed token", func(t *testing.T) {
|
|
// Create mock config with test mode enabled
|
|
mockConfig := &mockConfigProvider{
|
|
region: "us-east-2",
|
|
userPoolID: "us-east-2_1y6po8rR8",
|
|
noJWTValidation: true,
|
|
}
|
|
|
|
// Test with malformed token
|
|
invalidToken := "invalid.jwt.structure.here" // nolint:gosec // This is a test string, not real credentials
|
|
_, err := verifyToken(invalidToken, nil, mockConfig)
|
|
if err == nil {
|
|
t.Error("verifyToken() in test mode should fail with malformed token")
|
|
}
|
|
})
|
|
}
|