Files
alterminal/internal/oidc/userinfo_test.go
T
2026-10-03 10:44:29 +08:00

129 lines
4.1 KiB
Go
Raw 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"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"alterminal/internal/oidc"
)
// getUserinfo 以 Bearer token 呼叫 /userinfo。
func getUserinfo(h http.HandlerFunc, token string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/userinfo", nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
// obtainAccessToken 走完授權碼流程(PKCE)並回傳 access token。
func obtainAccessToken(t *testing.T, e *testEnv, scope string) string {
t.Helper()
_, code := consentAllow(t, e, authorizeQuery(e.app, scope, "", "", testChallenge))
form := url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"redirect_uri": {e.app.RedirectURIs[0]},
"code_verifier": {testVerifier},
}
rec := postToken(oidc.TokenHandler(e.db, testIssuer), form, e.app.ClientID, e.secret)
if rec.Code != http.StatusOK {
t.Fatalf("兌換失敗: %s", rec.Body.String())
}
var body tokenBody
json.Unmarshal(rec.Body.Bytes(), &body)
return body.AccessToken
}
// 有效 token 回依 scope 的 claims(OIDC Core §5.4)。
func TestUserInfoClaims(t *testing.T) {
e := newTestEnv(t)
h := oidc.UserInfoHandler(e.db, testIssuer)
t.Run("profile 與 email scope", func(t *testing.T) {
token := obtainAccessToken(t, e, "openid profile email")
rec := getUserinfo(h, token)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
var u userInfoBody
if err := json.Unmarshal(rec.Body.Bytes(), &u); err != nil {
t.Fatal("解析回應: ", err)
}
if u.Sub != subjectOf(e.user.ID) {
t.Errorf("sub = %q, want %q", u.Sub, subjectOf(e.user.ID))
}
if u.Name != e.user.Name || u.PreferredUsername != e.user.Username {
t.Errorf("profile claims = %q/%q", u.Name, u.PreferredUsername)
}
if u.Email != e.user.Email || u.EmailVerified == nil || !*u.EmailVerified {
t.Errorf("email claims = %q/%v", u.Email, u.EmailVerified)
}
})
t.Run("僅 openid 不含個人資料 claims", func(t *testing.T) {
token := obtainAccessToken(t, e, "openid")
rec := getUserinfo(h, token)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
body := rec.Body.String()
if strings.Contains(body, `"email"`) || strings.Contains(body, `"name"`) {
t.Errorf("未授權的 scope 不應輸出對應 claim: %s", body)
}
})
}
// token 缺失、無效或授權不足時的錯誤回應(RFC 6750 §3)。
func TestUserInfoErrors(t *testing.T) {
e := newTestEnv(t)
h := oidc.UserInfoHandler(e.db, testIssuer)
t.Run("缺少 token 回 401 與 WWW-Authenticate", func(t *testing.T) {
rec := getUserinfo(h, "")
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", rec.Code)
}
if wa := rec.Header().Get("WWW-Authenticate"); !strings.HasPrefix(wa, "Bearer") {
t.Errorf("WWW-Authenticate = %q", wa)
}
})
t.Run("無效 token 回 401 invalid_token", func(t *testing.T) {
rec := getUserinfo(h, "not-a-token")
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", rec.Code)
}
if wa := rec.Header().Get("WWW-Authenticate"); !strings.Contains(wa, `error="invalid_token"`) {
t.Errorf("WWW-Authenticate = %q", wa)
}
})
t.Run("scope 無 openid 回 403", func(t *testing.T) {
// 手造僅 profile scope 的 token(正式流程必含 openid,此處模擬
// 其他來源的 token)。
token := forgeJWT(t, e.key,
map[string]string{"alg": "RS256", "kid": e.key.Kid, "typ": "JWT"},
oidc.AccessTokenClaims{
Iss: testIssuer, Sub: subjectOf(e.user.ID), Aud: e.app.ClientID,
Exp: time.Now().Add(time.Hour).Unix(), Iat: time.Now().Unix(),
Scope: "profile", ClientID: e.app.ClientID,
})
rec := getUserinfo(h, token)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403, body = %s", rec.Code, rec.Body.String())
}
if wa := rec.Header().Get("WWW-Authenticate"); !strings.Contains(wa, "insufficient_scope") {
t.Errorf("WWW-Authenticate = %q", wa)
}
})
}