fix: harden auth flows and align api contracts

This commit is contained in:
Your Name
2026-05-30 21:29:24 +08:00
parent 7ad65a0138
commit a332917142
50 changed files with 23594 additions and 723 deletions

View File

@@ -1,347 +1,327 @@
package handler_test
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// =============================================================================
// SSOHandler Tests - Single Sign-On
// =============================================================================
type ssoWrappedResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
// TestSSOHandler_Authorize_CodeFlow 验证授权码流程
func TestSSOHandler_Authorize_CodeFlow(t *testing.T) {
type ssoTokenPayload struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
}
type ssoIntrospectPayload struct {
Active bool `json:"active"`
UserID int64 `json:"user_id"`
Username string `json:"username"`
Scope string `json:"scope"`
}
type ssoUserInfoPayload struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
}
func doSSOAuthorizeRequest(t *testing.T, rawURL, bearer string) *http.Response {
t.Helper()
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
if err != nil {
t.Fatalf("build authorize request: %v", err)
}
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("execute authorize request: %v", err)
}
return resp
}
func doSSOFormPost(t *testing.T, rawURL string, form url.Values, bearer string) (*http.Response, []byte) {
t.Helper()
req, err := http.NewRequest(http.MethodPost, rawURL, strings.NewReader(form.Encode()))
if err != nil {
t.Fatalf("build form request: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("execute form request: %v", err)
}
defer resp.Body.Close()
body := new(bytes.Buffer)
if _, err := body.ReadFrom(resp.Body); err != nil {
t.Fatalf("read form response: %v", err)
}
return resp, body.Bytes()
}
func decodeSSOWrappedResponse(t *testing.T, body []byte) ssoWrappedResponse {
t.Helper()
var wrapped ssoWrappedResponse
if err := json.Unmarshal(body, &wrapped); err != nil {
t.Fatalf("decode wrapped response failed: %v body=%s", err, string(body))
}
return wrapped
}
func extractAuthorizationCode(t *testing.T, location string) string {
t.Helper()
parsed, err := url.Parse(location)
if err != nil {
t.Fatalf("parse redirect location failed: %v", err)
}
code := parsed.Query().Get("code")
if code == "" {
t.Fatalf("redirect location missing code: %s", location)
}
return code
}
func issueSSOAuthCode(t *testing.T, serverURL, bearer string) string {
t.Helper()
resp := doSSOAuthorizeRequest(t, serverURL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&scope=profile&state=abc", bearer)
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
t.Fatalf("authorize expected 302, got %d", resp.StatusCode)
}
location := resp.Header.Get("Location")
if location == "" {
t.Fatal("authorize redirect missing Location header")
}
return extractAuthorizationCode(t, location)
}
func exchangeSSOToken(t *testing.T, serverURL, code, redirectURI string) ssoTokenPayload {
t.Helper()
resp, body := doSSOFormPost(t, serverURL+"/api/v1/sso/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"client_id": {"test-client"},
"client_secret": {"test-secret"},
"redirect_uri": {redirectURI},
}, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("token exchange expected 200, got %d body=%s", resp.StatusCode, string(body))
}
wrapped := decodeSSOWrappedResponse(t, body)
if wrapped.Code != 0 {
t.Fatalf("token exchange expected code=0, got %d body=%s", wrapped.Code, string(body))
}
var payload ssoTokenPayload
if err := json.Unmarshal(wrapped.Data, &payload); err != nil {
t.Fatalf("decode token payload failed: %v body=%s", err, string(body))
}
if payload.AccessToken == "" {
t.Fatalf("token exchange returned empty access token: %s", string(body))
}
return payload
}
func TestSSOHandler_Authorize_CodeFlowRedirectsWithCodeAndState(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Register and login user
registerUser(server.URL, "ssouser", "sso@test.com", "Pass123!")
token := getToken(server.URL, "ssouser", "Pass123!")
assert.NotEmpty(t, token)
platformToken := getToken(server.URL, "ssouser", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for authorize flow")
}
// Request authorization with code flow
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&state=xyz", token)
resp := doSSOAuthorizeRequest(t, server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&scope=profile&state=xyz", platformToken)
defer resp.Body.Close()
// SSO may return various status codes based on configuration
assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest ||
resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized,
"should handle authorize request, got %d", resp.StatusCode)
if resp.StatusCode != http.StatusFound {
t.Fatalf("authorize expected 302, got %d", resp.StatusCode)
}
location := resp.Header.Get("Location")
if location == "" {
t.Fatal("authorize redirect missing Location header")
}
if !strings.Contains(location, "code=") {
t.Fatalf("authorize redirect missing code: %s", location)
}
if !strings.Contains(location, "state=xyz") {
t.Fatalf("authorize redirect missing state: %s", location)
}
}
// TestSSOHandler_Authorize_TokenFlow 验证隐式授权流程
func TestSSOHandler_Authorize_TokenFlow(t *testing.T) {
func TestSSOHandler_Authorize_ImplicitFlowRejected(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "ssouser2", "sso2@test.com", "Pass123!")
token := getToken(server.URL, "ssouser2", "Pass123!")
assert.NotEmpty(t, token)
platformToken := getToken(server.URL, "ssouser2", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for implicit rejection test")
}
// Request authorization with token flow
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=token&state=abc", token)
resp := doSSOAuthorizeRequest(t, server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=token", platformToken)
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest ||
resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized,
"should handle token flow, got %d", resp.StatusCode)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("implicit flow expected 400, got %d", resp.StatusCode)
}
}
// TestSSOHandler_Authorize_MissingParams 验证缺少参数
func TestSSOHandler_Authorize_MissingParams(t *testing.T) {
func TestSSOHandler_Token_ExchangesWithoutPlatformBearerAuth(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "ssouser3", "sso3@test.com", "Pass123!")
token := getToken(server.URL, "ssouser3", "Pass123!")
// Missing params - handler may enforce or not based on config
resp1, _ := doGet(server.URL+"/api/v1/sso/authorize?redirect_uri=http://localhost&response_type=code", token)
defer resp1.Body.Close()
assert.True(t, resp1.StatusCode >= http.StatusBadRequest || resp1.StatusCode == http.StatusOK,
"should handle missing client_id, got %d", resp1.StatusCode)
// Missing redirect_uri
resp2, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&response_type=code", token)
defer resp2.Body.Close()
assert.True(t, resp2.StatusCode >= http.StatusBadRequest || resp2.StatusCode == http.StatusOK,
"should handle missing redirect_uri, got %d", resp2.StatusCode)
// Missing response_type
resp3, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost", token)
defer resp3.Body.Close()
assert.True(t, resp3.StatusCode >= http.StatusBadRequest || resp3.StatusCode == http.StatusOK,
"should handle missing response_type, got %d", resp3.StatusCode)
}
// TestSSOHandler_Authorize_InvalidResponseType 验证无效响应类型
func TestSSOHandler_Authorize_InvalidResponseType(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "ssouser4", "sso4@test.com", "Pass123!")
token := getToken(server.URL, "ssouser4", "Pass123!")
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=invalid", token)
defer resp.Body.Close()
assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle invalid response_type, got %d", resp.StatusCode)
}
// TestSSOHandler_Authorize_Unauthorized 验证未认证用户
func TestSSOHandler_Authorize_Unauthorized(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// No authentication token
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code", "")
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest,
"should handle unauthorized request, got %d", resp.StatusCode)
}
// TestSSOHandler_Token_Success 验证获取 Token
func TestSSOHandler_Token_Success(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Try to exchange code for token using doPost helper
resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"grant_type": "authorization_code",
"code": "invalid-code",
"client_id": "test",
"client_secret": "secret",
"redirect_uri": "http://localhost",
})
defer resp.Body.Close()
// Handler may accept or reject based on SSO config
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle token request, got %d", resp.StatusCode)
}
// TestSSOHandler_Token_MissingParams 验证缺少 Token 参数
func TestSSOHandler_Token_MissingParams(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Missing client_id
resp1, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"grant_type": "authorization_code",
"code": "test",
"client_secret": "secret",
})
defer resp1.Body.Close()
assert.True(t, resp1.StatusCode >= http.StatusBadRequest || resp1.StatusCode == http.StatusOK,
"should handle missing client_id, got %d", resp1.StatusCode)
// Missing client_secret
resp2, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"grant_type": "authorization_code",
"code": "test",
"client_id": "test",
})
defer resp2.Body.Close()
assert.True(t, resp2.StatusCode >= http.StatusBadRequest || resp2.StatusCode == http.StatusOK,
"should handle missing client_secret, got %d", resp2.StatusCode)
// Missing grant_type
resp3, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"client_id": "test",
"client_secret": "secret",
"code": "test",
})
defer resp3.Body.Close()
assert.True(t, resp3.StatusCode >= http.StatusBadRequest || resp3.StatusCode == http.StatusOK,
"should handle missing grant_type, got %d", resp3.StatusCode)
}
// TestSSOHandler_Token_InvalidGrantType 验证无效授权类型
func TestSSOHandler_Token_InvalidGrantType(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"grant_type": "invalid_grant",
"client_id": "test",
"client_secret": "secret",
"code": "test",
})
defer resp.Body.Close()
assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle invalid grant_type, got %d", resp.StatusCode)
}
// TestSSOHandler_Introspect_Success 验证 Token 验证
func TestSSOHandler_Introspect_Success(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Introspect invalid token
resp, body := doPost(server.URL+"/api/v1/sso/introspect", "", map[string]interface{}{
"token": "invalid-token",
})
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest,
"should return introspect response, got %d: %s", resp.StatusCode, body)
}
// TestSSOHandler_Introspect_MissingToken 验证缺少 Token
func TestSSOHandler_Introspect_MissingToken(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
resp, _ := doPost(server.URL+"/api/v1/sso/introspect", "", map[string]interface{}{
"token": "",
})
defer resp.Body.Close()
assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle missing token, got %d", resp.StatusCode)
}
// TestSSOHandler_Revoke_Success 验证 Token 撤销
func TestSSOHandler_Revoke_Success(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
resp, body := doPost(server.URL+"/api/v1/sso/revoke", "", map[string]interface{}{
"token": "some-token",
})
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest,
"should handle revoke request, got %d: %s", resp.StatusCode, body)
}
// TestSSOHandler_Revoke_MissingToken 验证缺少 Token
func TestSSOHandler_Revoke_MissingToken(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
resp, _ := doPost(server.URL+"/api/v1/sso/revoke", "", map[string]interface{}{
"token": "",
})
defer resp.Body.Close()
assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle missing token, got %d", resp.StatusCode)
}
// TestSSOHandler_UserInfo_Success 验证获取用户信息
func TestSSOHandler_UserInfo_Success(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "ssouser5", "sso5@test.com", "Pass123!")
token := getToken(server.URL, "ssouser5", "Pass123!")
assert.NotEmpty(t, token)
resp, body := doGet(server.URL+"/api/v1/sso/userinfo", token)
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden,
"should handle userinfo request, got %d: %s", resp.StatusCode, body)
}
// TestSSOHandler_UserInfo_Unauthorized 验证未认证访问
func TestSSOHandler_UserInfo_Unauthorized(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
resp, _ := doGet(server.URL+"/api/v1/sso/userinfo", "")
defer resp.Body.Close()
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest,
"should handle unauthorized request, got %d", resp.StatusCode)
}
// TestSSOHandler_FullFlow_Authorization 验证完整授权流程
func TestSSOHandler_FullFlow_Authorization(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Register and login
registerUser(server.URL, "flowuser", "flow@test.com", "Pass123!")
token := getToken(server.URL, "flowuser", "Pass123!")
assert.NotEmpty(t, token)
platformToken := getToken(server.URL, "flowuser", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for authorization")
}
// Step 1: Authorize (get code or redirect)
authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&scope=profile", token)
defer authResp.Body.Close()
code := issueSSOAuthCode(t, server.URL, platformToken)
payload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback")
// Step 2: Check response - SSO may return redirect or direct response based on config
assert.True(t, authResp.StatusCode == http.StatusFound || authResp.StatusCode == http.StatusOK ||
authResp.StatusCode == http.StatusBadRequest || authResp.StatusCode == http.StatusInternalServerError,
"should handle authorization, got %d", authResp.StatusCode)
if authResp.StatusCode == http.StatusFound {
location := authResp.Header.Get("Location")
assert.Contains(t, location, "localhost")
t.Logf("Redirected to: %s", location)
if payload.TokenType != "Bearer" {
t.Fatalf("unexpected token type: %q", payload.TokenType)
}
if payload.ExpiresIn <= 0 {
t.Fatalf("unexpected expires_in: %d", payload.ExpiresIn)
}
}
// TestSSOHandler_ClientCredentials_Validation 验证客户端凭证验证
func TestSSOHandler_ClientCredentials_Validation(t *testing.T) {
func TestSSOHandler_Token_RedirectURIMismatchRejected(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
// Try with invalid client credentials
resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{
"grant_type": "authorization_code",
"code": "test-code",
"client_id": "invalid-client",
"client_secret": "wrong-secret",
"redirect_uri": "http://localhost",
})
defer resp.Body.Close()
registerUser(server.URL, "mismatchuser", "mismatch@test.com", "Pass123!")
platformToken := getToken(server.URL, "mismatchuser", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for authorization")
}
// May accept or reject based on SSO configuration
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK,
"should handle client credentials, got %d", resp.StatusCode)
}
// TestSSOHandler_Scope_Handling 验证 Scope 处理
func TestSSOHandler_Scope_Handling(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "scopeuser", "scope@test.com", "Pass123!")
token := getToken(server.URL, "scopeuser", "Pass123!")
// Request with scope
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&scope=profile+email", token)
defer resp.Body.Close()
// Should handle scope parameter
assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest ||
resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized,
"should handle scope parameter, got %d", resp.StatusCode)
}
// TestSSOHandler_State_Preservation 验证 State 参数保持
func TestSSOHandler_State_Preservation(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "stateuser", "state@test.com", "Pass123!")
token := getToken(server.URL, "stateuser", "Pass123!")
// Request with state parameter
resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&state=my-state-value", token)
defer resp.Body.Close()
// If redirected, state should be preserved in callback
if resp.StatusCode == http.StatusFound {
location := resp.Header.Get("Location")
// State should be included in redirect URL
t.Logf("Redirect location: %s", location)
code := issueSSOAuthCode(t, server.URL, platformToken)
resp, body := doSSOFormPost(t, server.URL+"/api/v1/sso/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"client_id": {"test-client"},
"client_secret": {"test-secret"},
"redirect_uri": {"http://localhost/other"},
}, "")
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("redirect mismatch expected 400, got %d body=%s", resp.StatusCode, string(body))
}
}
func TestSSOHandler_IntrospectAndRevokeUseClientCredentialsNotPlatformBearer(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "introspectuser", "introspect@test.com", "Pass123!")
platformToken := getToken(server.URL, "introspectuser", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for authorization")
}
code := issueSSOAuthCode(t, server.URL, platformToken)
tokenPayload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback")
resp1, body1 := doSSOFormPost(t, server.URL+"/api/v1/sso/introspect", url.Values{
"token": {tokenPayload.AccessToken},
"client_id": {"test-client"},
"client_secret": {"test-secret"},
}, "")
if resp1.StatusCode != http.StatusOK {
t.Fatalf("introspect expected 200, got %d body=%s", resp1.StatusCode, string(body1))
}
wrapped1 := decodeSSOWrappedResponse(t, body1)
var introspect ssoIntrospectPayload
if err := json.Unmarshal(wrapped1.Data, &introspect); err != nil {
t.Fatalf("decode introspect payload failed: %v body=%s", err, string(body1))
}
if !introspect.Active {
t.Fatalf("expected active token in introspect response: %s", string(body1))
}
if introspect.Username != "introspectuser" {
t.Fatalf("unexpected introspect username: %q", introspect.Username)
}
resp2, body2 := doSSOFormPost(t, server.URL+"/api/v1/sso/revoke", url.Values{
"token": {tokenPayload.AccessToken},
"client_id": {"test-client"},
"client_secret": {"test-secret"},
}, "")
if resp2.StatusCode != http.StatusOK {
t.Fatalf("revoke expected 200, got %d body=%s", resp2.StatusCode, string(body2))
}
resp3, body3 := doSSOFormPost(t, server.URL+"/api/v1/sso/introspect", url.Values{
"token": {tokenPayload.AccessToken},
"client_id": {"test-client"},
"client_secret": {"test-secret"},
}, "")
if resp3.StatusCode != http.StatusOK {
t.Fatalf("post-revoke introspect expected 200, got %d body=%s", resp3.StatusCode, string(body3))
}
wrapped3 := decodeSSOWrappedResponse(t, body3)
var revoked ssoIntrospectPayload
if err := json.Unmarshal(wrapped3.Data, &revoked); err != nil {
t.Fatalf("decode revoked introspect payload failed: %v body=%s", err, string(body3))
}
if revoked.Active {
t.Fatalf("expected revoked token to be inactive: %s", string(body3))
}
}
func TestSSOHandler_UserInfoUsesSSOAccessTokenSubject(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "userinfo-user", "userinfo@test.com", "Pass123!")
platformToken := getToken(server.URL, "userinfo-user", "Pass123!")
if platformToken == "" {
t.Fatal("expected login token for authorization")
}
code := issueSSOAuthCode(t, server.URL, platformToken)
tokenPayload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback")
resp, body := doGet(server.URL+"/api/v1/sso/userinfo", tokenPayload.AccessToken)
if resp.StatusCode != http.StatusOK {
t.Fatalf("userinfo expected 200, got %d body=%s", resp.StatusCode, body)
}
wrapped := decodeSSOWrappedResponse(t, []byte(body))
var payload ssoUserInfoPayload
if err := json.Unmarshal(wrapped.Data, &payload); err != nil {
t.Fatalf("decode userinfo payload failed: %v body=%s", err, body)
}
if payload.Username != "userinfo-user" {
t.Fatalf("unexpected userinfo username: %q body=%s", payload.Username, body)
}
if payload.UserID == 0 {
t.Fatalf("userinfo user_id should be non-zero: %s", body)
}
}