使用 AES-256-GCM 保存认证中心令牌,并以随机 Cookie 哈希关联 PostgreSQL 会话。加入闲置与绝对期限、Refresh Token 轮换以及多实例刷新锁。
126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestOIDCPublicClientUsesAuthorizationCodePKCES256WithoutSecret(t *testing.T) {
|
|
var issuer string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/.well-known/openid-configuration":
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
|
|
"token_endpoint": issuer + "/token", "revocation_endpoint": issuer + "/revoke",
|
|
"end_session_endpoint": issuer + "/logout",
|
|
})
|
|
case "/token":
|
|
body, _ := io.ReadAll(r.Body)
|
|
values, _ := url.ParseQuery(string(body))
|
|
if values.Get("client_secret") != "" || strings.Contains(r.Header.Get("Authorization"), "Basic") {
|
|
t.Fatal("public client token request must not contain client credentials")
|
|
}
|
|
if values.Get("grant_type") != "authorization_code" || values.Get("client_id") != "gateway-public" || values.Get("code_verifier") != "verifier" {
|
|
t.Fatalf("unexpected token request: %v", values)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"access_token": "access-token", "refresh_token": "refresh-token",
|
|
"id_token": "id-token", "expires_in": 300, "token_type": "Bearer",
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
issuer = server.URL
|
|
|
|
client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
|
|
Issuer: issuer, ClientID: "gateway-public", RedirectURI: "https://gateway.example.com/gateway-api/api/v1/auth/oidc/callback",
|
|
PostLogoutRedirectURI: "https://gateway.example.com/", Scopes: []string{"openid", "profile", "gateway.access"}, HTTPClient: server.Client(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authorizationURL, err := client.AuthorizationURL(context.Background(), "state", "nonce", "challenge")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsed, _ := url.Parse(authorizationURL)
|
|
query := parsed.Query()
|
|
if query.Get("response_type") != "code" || query.Get("code_challenge_method") != "S256" || query.Get("code_challenge") != "challenge" {
|
|
t.Fatalf("authorization request is not PKCE S256: %v", query)
|
|
}
|
|
if _, err := client.ExchangeCode(context.Background(), "authorization-code", "verifier"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestOIDCPublicClientRejectsOfflineAccess(t *testing.T) {
|
|
_, err := NewOIDCPublicClient(OIDCPublicClientConfig{
|
|
Issuer: "https://auth.example.com", ClientID: "gateway-public",
|
|
RedirectURI: "https://gateway.example.com/api/v1/auth/oidc/callback",
|
|
PostLogoutRedirectURI: "https://gateway.example.com/",
|
|
Scopes: []string{"openid", "gateway.access", "offline_access"},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "offline_access") {
|
|
t.Fatalf("NewOIDCPublicClient() error = %v, want offline_access rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestOIDCPublicClientRefreshAndRevokeNeverSendSecret(t *testing.T) {
|
|
var issuer string
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/.well-known/openid-configuration":
|
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
|
"issuer": issuer, "authorization_endpoint": issuer + "/authorize",
|
|
"token_endpoint": issuer + "/token", "revocation_endpoint": issuer + "/revoke",
|
|
"end_session_endpoint": issuer + "/logout",
|
|
})
|
|
case "/token", "/revoke":
|
|
requests++
|
|
body, _ := io.ReadAll(r.Body)
|
|
values, _ := url.ParseQuery(string(body))
|
|
if values.Get("client_secret") != "" || r.Header.Get("Authorization") != "" {
|
|
t.Fatal("public client request contained client authentication")
|
|
}
|
|
if r.URL.Path == "/token" {
|
|
if values.Get("grant_type") != "refresh_token" || values.Get("refresh_token") != "old-refresh" {
|
|
t.Fatalf("unexpected refresh request: %v", values)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"access_token": "new-access", "refresh_token": "new-refresh", "expires_in": 300})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
issuer = server.URL
|
|
client, err := NewOIDCPublicClient(OIDCPublicClientConfig{
|
|
Issuer: issuer, ClientID: "gateway-public", RedirectURI: "https://gateway.example.com/callback",
|
|
PostLogoutRedirectURI: "https://gateway.example.com/", HTTPClient: server.Client(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := client.Refresh(context.Background(), "old-refresh"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := client.RevokeRefreshToken(context.Background(), "new-refresh"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requests != 2 {
|
|
t.Fatalf("requests = %d, want 2", requests)
|
|
}
|
|
}
|