package oidc import ( "crypto/sha256" "encoding/base64" "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // base64url(無 padding)允許的字元集,對齊 RFC 4648 §5。 // 順帶涵蓋 RFC 7636 §4.1 對 code_verifier 的字元集要求子集。 var base64URLPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) func TestGenerateCodeVerifier_LengthAndCharset(t *testing.T) { v, err := GenerateCodeVerifier() require.NoError(t, err) // 32 bytes → base64url 後 43 字元(無 padding)。 // RFC 7636 規定範圍 43-128 字元;43 字元剛好符合最小邊界。 assert.Len(t, v, 43, "verifier 應為 43 字元(32 bytes base64url)") assert.GreaterOrEqual(t, len(v), 43, "RFC 7636 最小 43 字元") assert.LessOrEqual(t, len(v), 128, "RFC 7636 最大 128 字元") assert.Regexp(t, base64URLPattern, v, "verifier 應只含 base64url 字元") } func TestGenerateCodeVerifier_Randomness(t *testing.T) { const n = 50 seen := make(map[string]struct{}, n) for i := 0; i < n; i++ { v, err := GenerateCodeVerifier() require.NoError(t, err) _, dup := seen[v] assert.Falsef(t, dup, "第 %d 次產生與先前重複,亂數源異常", i) seen[v] = struct{}{} } } func TestCodeChallenge_KnownVector(t *testing.T) { // RFC 7636 Appendix B 提供的 known answer test: // verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" // challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" const want = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" got := CodeChallenge(verifier) assert.Equal(t, want, got, "challenge 與 RFC 7636 Appendix B test vector 不符") } func TestCodeChallenge_MatchesSHA256(t *testing.T) { v, err := GenerateCodeVerifier() require.NoError(t, err) want := base64.RawURLEncoding.EncodeToString(sha256Sum([]byte(v))) got := CodeChallenge(v) assert.Equal(t, want, got, "challenge 應為 base64url(SHA256(verifier))") } func TestGenerateState_Format(t *testing.T) { s, err := GenerateState() require.NoError(t, err) // 32 bytes → 43 字元 base64url(無 padding) assert.Len(t, s, 43) assert.Regexp(t, base64URLPattern, s) } func TestGenerateNonce_Format(t *testing.T) { n, err := GenerateNonce() require.NoError(t, err) assert.Len(t, n, 43) assert.Regexp(t, base64URLPattern, n) } func TestStateAndNonce_Independent(t *testing.T) { // state 和 nonce 雖然產生方式相同,但兩次連續呼叫不應產生相同值。 s1, err := GenerateState() require.NoError(t, err) s2, err := GenerateState() require.NoError(t, err) assert.NotEqual(t, s1, s2, "兩次 GenerateState 不應重複") n1, err := GenerateNonce() require.NoError(t, err) n2, err := GenerateNonce() require.NoError(t, err) assert.NotEqual(t, n1, n2, "兩次 GenerateNonce 不應重複") assert.NotEqual(t, s1, n1, "state 與 nonce 應為獨立隨機值") } func TestRandomBase64URL_RejectNonPositive(t *testing.T) { _, err := randomBase64URL(0) assert.Error(t, err, "n=0 應拒絕") _, err = randomBase64URL(-1) assert.Error(t, err, "n<0 應拒絕") } // sha256Sum 是 test helper,避免在測試中每次都寫 [:]。 func sha256Sum(b []byte) []byte { s := sha256.Sum256(b) return s[:] }