feat: support OIDC login.
This commit is contained in:
106
backend/internal/handler/auth_oidc_oauth_test.go
Normal file
106
backend/internal/handler/auth_oidc_oauth_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCSyntheticEmailStableAndDistinct(t *testing.T) {
|
||||
k1 := oidcIdentityKey("https://issuer.example.com", "subject-a")
|
||||
k2 := oidcIdentityKey("https://issuer.example.com", "subject-b")
|
||||
|
||||
e1 := oidcSyntheticEmailFromIdentityKey(k1)
|
||||
e1Again := oidcSyntheticEmailFromIdentityKey(k1)
|
||||
e2 := oidcSyntheticEmailFromIdentityKey(k2)
|
||||
|
||||
require.Equal(t, e1, e1Again)
|
||||
require.NotEqual(t, e1, e2)
|
||||
require.Contains(t, e1, "@oidc-connect.invalid")
|
||||
}
|
||||
|
||||
func TestBuildOIDCAuthorizeURLIncludesNonceAndPKCE(t *testing.T) {
|
||||
cfg := config.OIDCConnectConfig{
|
||||
AuthorizeURL: "https://issuer.example.com/auth",
|
||||
ClientID: "cid",
|
||||
Scopes: "openid email profile",
|
||||
UsePKCE: true,
|
||||
}
|
||||
|
||||
u, err := buildOIDCAuthorizeURL(cfg, "state123", "nonce123", "challenge123", "https://app.example.com/callback")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, u, "nonce=nonce123")
|
||||
require.Contains(t, u, "code_challenge=challenge123")
|
||||
require.Contains(t, u, "code_challenge_method=S256")
|
||||
require.Contains(t, u, "scope=openid+email+profile")
|
||||
}
|
||||
|
||||
func TestOIDCParseAndValidateIDToken(t *testing.T) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
kid := "kid-1"
|
||||
jwks := oidcJWKSet{Keys: []oidcJWK{buildRSAJWK(kid, &priv.PublicKey)}}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, json.NewEncoder(w).Encode(jwks))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
now := time.Now()
|
||||
claims := oidcIDTokenClaims{
|
||||
Nonce: "nonce-ok",
|
||||
Azp: "client-1",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://issuer.example.com",
|
||||
Subject: "subject-1",
|
||||
Audience: jwt.ClaimStrings{"client-1", "another-aud"},
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now.Add(-30 * time.Second)),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
},
|
||||
}
|
||||
tok := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
tok.Header["kid"] = kid
|
||||
signed, err := tok.SignedString(priv)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := config.OIDCConnectConfig{
|
||||
ClientID: "client-1",
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
JWKSURL: srv.URL,
|
||||
AllowedSigningAlgs: "RS256",
|
||||
ClockSkewSeconds: 120,
|
||||
}
|
||||
|
||||
parsed, err := oidcParseAndValidateIDToken(context.Background(), cfg, signed, "nonce-ok")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "subject-1", parsed.Subject)
|
||||
require.Equal(t, "https://issuer.example.com", parsed.Issuer)
|
||||
|
||||
_, err = oidcParseAndValidateIDToken(context.Background(), cfg, signed, "bad-nonce")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func buildRSAJWK(kid string, pub *rsa.PublicKey) oidcJWK {
|
||||
n := base64.RawURLEncoding.EncodeToString(pub.N.Bytes())
|
||||
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes())
|
||||
return oidcJWK{
|
||||
Kty: "RSA",
|
||||
Kid: kid,
|
||||
Use: "sig",
|
||||
Alg: "RS256",
|
||||
N: n,
|
||||
E: e,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user