177 lines
4.5 KiB
Go
177 lines
4.5 KiB
Go
package auth_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// mockIdP is a test OIDC identity provider that serves discovery,
|
|
// JWKS, and token endpoints. It signs JWTs with a known RSA key
|
|
// so the OIDCProvider can validate them in tests.
|
|
type mockIdP struct {
|
|
server *httptest.Server
|
|
privateKey *rsa.PrivateKey
|
|
kid string
|
|
}
|
|
|
|
func newMockIdP() *mockIdP {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
panic("generate RSA key: " + err.Error())
|
|
}
|
|
|
|
m := &mockIdP{
|
|
privateKey: key,
|
|
kid: "test-key-1",
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/.well-known/openid-configuration", m.handleDiscovery)
|
|
mux.HandleFunc("/protocol/openid-connect/certs", m.handleJWKS)
|
|
mux.HandleFunc("/protocol/openid-connect/token", m.handleToken)
|
|
|
|
m.server = httptest.NewServer(mux)
|
|
return m
|
|
}
|
|
|
|
func (m *mockIdP) Close() {
|
|
m.server.Close()
|
|
}
|
|
|
|
func (m *mockIdP) IssuerURL() string {
|
|
return m.server.URL
|
|
}
|
|
|
|
func (m *mockIdP) handleDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
base := m.server.URL
|
|
disc := map[string]string{
|
|
"issuer": base,
|
|
"authorization_endpoint": base + "/protocol/openid-connect/auth",
|
|
"token_endpoint": base + "/protocol/openid-connect/token",
|
|
"jwks_uri": base + "/protocol/openid-connect/certs",
|
|
"userinfo_endpoint": base + "/protocol/openid-connect/userinfo",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(disc)
|
|
}
|
|
|
|
func (m *mockIdP) handleJWKS(w http.ResponseWriter, r *http.Request) {
|
|
pub := m.privateKey.PublicKey
|
|
jwks := map[string]interface{}{
|
|
"keys": []map[string]string{
|
|
{
|
|
"kid": m.kid,
|
|
"kty": "RSA",
|
|
"alg": "RS256",
|
|
"use": "sig",
|
|
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
|
|
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()),
|
|
},
|
|
},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(jwks)
|
|
}
|
|
|
|
func (m *mockIdP) handleToken(w http.ResponseWriter, r *http.Request) {
|
|
// Simple password grant for testing
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "bad form", 400)
|
|
return
|
|
}
|
|
|
|
username := r.FormValue("username")
|
|
if username == "" {
|
|
username = "testuser"
|
|
}
|
|
|
|
token := m.issueToken(username, username+"@test.local", username, nil, nil)
|
|
resp := map[string]interface{}{
|
|
"access_token": token,
|
|
"id_token": token,
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// issueToken creates a signed JWT with the given claims.
|
|
func (m *mockIdP) issueToken(sub, email, name string, groups []string, roles []string) string {
|
|
claims := jwt.MapClaims{
|
|
"iss": m.server.URL,
|
|
"sub": sub,
|
|
"aud": "switchboard",
|
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"email": email,
|
|
"preferred_username": name,
|
|
"name": name,
|
|
}
|
|
if groups != nil {
|
|
claims["groups"] = groups
|
|
}
|
|
if roles != nil {
|
|
claims["realm_access"] = map[string]interface{}{
|
|
"roles": roles,
|
|
}
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
token.Header["kid"] = m.kid
|
|
|
|
signed, err := token.SignedString(m.privateKey)
|
|
if err != nil {
|
|
panic("sign token: " + err.Error())
|
|
}
|
|
return signed
|
|
}
|
|
|
|
// issueExpiredToken creates a token that's already expired.
|
|
func (m *mockIdP) issueExpiredToken(sub string) string {
|
|
claims := jwt.MapClaims{
|
|
"iss": m.server.URL,
|
|
"sub": sub,
|
|
"aud": "switchboard",
|
|
"exp": time.Now().Add(-1 * time.Hour).Unix(),
|
|
"iat": time.Now().Add(-2 * time.Hour).Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
token.Header["kid"] = m.kid
|
|
|
|
signed, err := token.SignedString(m.privateKey)
|
|
if err != nil {
|
|
panic("sign expired token: " + err.Error())
|
|
}
|
|
return signed
|
|
}
|
|
|
|
// issueTokenBadIssuer creates a token with a mismatched issuer.
|
|
func (m *mockIdP) issueTokenBadIssuer(sub string) string {
|
|
claims := jwt.MapClaims{
|
|
"iss": "https://evil.example.com",
|
|
"sub": sub,
|
|
"aud": "switchboard",
|
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
token.Header["kid"] = m.kid
|
|
|
|
signed, err := token.SignedString(m.privateKey)
|
|
if err != nil {
|
|
panic("sign bad issuer token: " + err.Error())
|
|
}
|
|
return signed
|
|
}
|