forked from alterminal/alterminal
first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// 外部測試套件:見 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user