package oidc import ( "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" ) // 隨機值長度常數(位元組數)。 // // RFC 7636 §4.1 規定 code_verifier 是 43-128 個字元([A-Z a-z 0-9 - . _ ~])。 // 32 bytes 經 base64url(無 padding)編碼後 = ceil(32 * 4 / 3) = 43 字元, // 剛好等於最小邊界 43,落在合規範圍內,且提供 256 bits 熵。 // // state 與 nonce 沒有 RFC 規範長度,採同樣 32 bytes(256 bits)足以抵抗暴力猜測。 const ( // pkceVerifierBytes = 32 bytes → base64url 後 43 字元,符合 RFC 7636 範圍下界。 pkceVerifierBytes = 32 // stateNonceBytes = 32 bytes → 256 bits 隨機性。 stateNonceBytes = 32 ) // GenerateCodeVerifier 產生 RFC 7636 PKCE code_verifier。 // // 回傳值是 base64url(無 padding)編碼的 32 byte 隨機值,等於 43 個字元, // 落在 RFC 7636 §4.1 規定的 43-128 字元範圍內(剛好踩在下界)。 // // 字元集為 [A-Z a-z 0-9 - _](base64url 規範),符合 RFC 7636 §4.1 的 // unreserved characters 子集(少了 `.` 和 `~`,但仍合規)。 func GenerateCodeVerifier() (string, error) { return randomBase64URL(pkceVerifierBytes) } // CodeChallenge 由 code_verifier 算出 RFC 7636 §4.2 規定的 code_challenge: // // BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) // // challenge_method 固定 S256(不支援 plain,因 OAuth 2.1 已標 plain 為 deprecated)。 // // caller 應將回傳值放在 authorization request 的 code_challenge 參數, // 並把 GenerateCodeVerifier() 的原值(verifier)安全地存在 server-side pending session, // 後續 ExchangeCode 時帶 verifier 給 token endpoint 完成 proof。 func CodeChallenge(verifier string) string { sum := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(sum[:]) } // GenerateState 產生 OAuth 2.0 state 值,用於 CSRF 防護。 // // caller 應將其存在 server-side pending session,並在 callback 收到後比對。 func GenerateState() (string, error) { return randomBase64URL(stateNonceBytes) } // GenerateNonce 產生 OIDC nonce 值,用於 id_token replay 防護。 // // caller 應將其存在 server-side pending session,並在 VerifyIDToken 時比對 claims.Nonce。 func GenerateNonce() (string, error) { return randomBase64URL(stateNonceBytes) } // randomBase64URL 是內部 helper:產生 n bytes 隨機值並做 base64url(無 padding)編碼。 // // 使用 crypto/rand 為密碼學安全亂數源;任何讀取錯誤都向上傳遞給 caller 處理 // (通常代表系統 entropy 出問題,應該讓請求 fail 而非回退到不安全的預設值)。 func randomBase64URL(n int) (string, error) { if n <= 0 { return "", fmt.Errorf("oidc: random length must be positive, got %d", n) } b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("oidc: read random bytes: %w", err) } return base64.RawURLEncoding.EncodeToString(b), nil }