Files
2026-10-03 10:44:29 +08:00

113 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 外部測試套件:見 jwks_test.go 開頭說明。
package oidc_test
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
"alterminal/internal/oidc"
)
// IssueAccessToken 簽出的 token 應可由 VerifyAccessToken 驗回,且 claims
// 正確(iss/sub/aud/scope)。
func TestAccessTokenRoundTrip(t *testing.T) {
e := newTestEnv(t)
token, err := oidc.IssueAccessToken(e.db, testIssuer, e.user.ID, e.app, "openid profile")
if err != nil {
t.Fatal("IssueAccessToken: ", err)
}
claims, err := oidc.VerifyAccessToken(e.db, testIssuer, token)
if err != nil {
t.Fatal("VerifyAccessToken: ", err)
}
if claims.Iss != testIssuer {
t.Errorf("iss = %q, want %q", claims.Iss, testIssuer)
}
if claims.Sub != subjectOf(e.user.ID) {
t.Errorf("sub = %q, want %q", claims.Sub, subjectOf(e.user.ID))
}
if claims.Aud != e.app.ClientID || claims.ClientID != e.app.ClientID {
t.Errorf("aud/client_id = %q/%q, want %q", claims.Aud, claims.ClientID, e.app.ClientID)
}
if claims.Scope != "openid profile" {
t.Errorf("scope = %q", claims.Scope)
}
// header 應含正確的 alg 與 kid(RP 以 kid 對應 JWKS)。
header, _ := jwtParts(t, token)
var h struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
Typ string `json:"typ"`
}
if err := json.Unmarshal(header, &h); err != nil {
t.Fatal("解析 header: ", err)
}
if h.Alg != "RS256" || h.Kid != e.key.Kid || h.Typ != "JWT" {
t.Errorf("header = %+v", h)
}
}
// 各種無效 token 都應回 ErrInvalidToken,不洩漏細節。
func TestVerifyAccessTokenRejectsInvalid(t *testing.T) {
e := newTestEnv(t)
good, err := oidc.IssueAccessToken(e.db, testIssuer, e.user.ID, e.app, "openid")
if err != nil {
t.Fatal(err)
}
rs256Header := func(kid string) map[string]string {
return map[string]string{"alg": "RS256", "kid": kid, "typ": "JWT"}
}
// 竄改 payload:改動第一個字元後以原簽章送出,驗章應失敗。
parts := strings.Split(good, ".")
payloadBytes := []byte(parts[1])
if payloadBytes[0] == 'e' {
payloadBytes[0] = 'e' + 1
} else {
payloadBytes[0] = 'e'
}
parts[1] = string(payloadBytes)
tampered := strings.Join(parts, ".")
tests := []struct {
name string
token string
}{
{"非 JWT 結構", "not-a-jwt"},
{"alg=none(演算法混淆)", forgeJWT(t, e.key, map[string]string{"alg": "none", "kid": e.key.Kid}, map[string]any{"iss": testIssuer, "exp": time.Now().Add(time.Hour).Unix()})},
{"alg=HS256", forgeJWT(t, e.key, map[string]string{"alg": "HS256", "kid": e.key.Kid}, map[string]any{"iss": testIssuer, "exp": time.Now().Add(time.Hour).Unix()})},
{"kid 不存在", forgeJWT(t, e.key, rs256Header("unknown-kid"), map[string]any{"iss": testIssuer, "exp": time.Now().Add(time.Hour).Unix()})},
{"issuer 不符", forgeJWT(t, e.key, rs256Header(e.key.Kid), map[string]any{"iss": "https://other.example", "exp": time.Now().Add(time.Hour).Unix()})},
{"已過期", forgeJWT(t, e.key, rs256Header(e.key.Kid), map[string]any{"iss": testIssuer, "exp": time.Now().Add(-time.Minute).Unix()})},
{"竄改 payload", tampered},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := oidc.VerifyAccessToken(e.db, testIssuer, tt.token)
if !errors.Is(err, oidc.ErrInvalidToken) {
t.Fatalf("err = %v, want ErrInvalidToken", err)
}
})
}
}
// 已退休金鑰簽發的 token 在效期內仍應驗證成功(輪替過渡期,JWKS 同步
// 發佈新舊金鑰的模型)。
func TestVerifyAccessTokenRetiredKey(t *testing.T) {
e := newTestEnv(t)
token, err := oidc.IssueAccessToken(e.db, testIssuer, e.user.ID, e.app, "openid")
if err != nil {
t.Fatal(err)
}
now := time.Now()
if err := e.db.Model(e.key).Update("retired_at", now).Error; err != nil {
t.Fatal("退休金鑰: ", err)
}
if _, err := oidc.VerifyAccessToken(e.db, testIssuer, token); err != nil {
t.Fatalf("退休金鑰在效期內仍應可驗證: %v", err)
}
}