ultisuite-backend/internal/integrationtest/oidc.go
R3D347HR4Y 1ffd0817d8
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(migration): enhance migration API with roster and audit export features
- Added endpoints for listing and importing migration rosters.
- Introduced audit export functionality for migration jobs in CSV and NDJSON formats.
- Implemented tenant mismatch validation for Microsoft migration claims.
- Enhanced error handling for email claiming and migration processes.
- Added integration tests for roster import and claim workflows.
2026-06-13 13:11:30 +02:00

113 lines
2.8 KiB
Go

//go:build integration
package integrationtest
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/ultisuite/ulti-backend/internal/auth"
)
const testOIDCClientID = "ulti-backend-test"
// OIDCServer is a minimal OIDC issuer for integration tests.
type OIDCServer struct {
URL string
Issuer string
ClientID string
server *httptest.Server
key *rsa.PrivateKey
jwk jose.JSONWebKey
}
func NewOIDCServer() (*OIDCServer, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("generate rsa key: %w", err)
}
jwk := jose.JSONWebKey{Key: key.Public(), KeyID: "test-key", Use: "sig", Algorithm: string(jose.RS256)}
s := &OIDCServer{
ClientID: testOIDCClientID,
key: key,
jwk: jwk,
}
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{
"issuer": s.Issuer,
"jwks_uri": s.Issuer + "/jwks/",
})
})
mux.HandleFunc("/jwks/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"keys": []jose.JSONWebKey{s.jwk}})
})
s.server = httptest.NewServer(mux)
s.URL = strings.TrimSuffix(s.server.URL, "/")
s.Issuer = s.URL
return s, nil
}
func (s *OIDCServer) Verifier(ctx context.Context) (*auth.Verifier, error) {
return auth.NewVerifier(ctx, s.URL, s.ClientID, "localhost")
}
func (s *OIDCServer) Holder(ctx context.Context) (*auth.Holder, error) {
v, err := s.Verifier(ctx)
if err != nil {
return nil, err
}
return auth.NewHolder(v), nil
}
func (s *OIDCServer) IssueToken(claims *auth.Claims) (string, error) {
if claims == nil {
return "", fmt.Errorf("claims is nil")
}
now := time.Now()
opts := (&jose.SignerOptions{}).WithHeader(jose.HeaderKey("kid"), s.jwk.KeyID)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: s.key}, opts)
if err != nil {
return "", err
}
builder := jwt.Signed(sig).Claims(jwt.Claims{
Issuer: s.Issuer,
Subject: claims.Sub,
Audience: jwt.Audience{s.ClientID},
Expiry: jwt.NewNumericDate(now.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(now),
}).Claims(map[string]any{
"email": claims.Email,
"preferred_username": claims.PreferredUsername,
"upn": claims.UPN,
"name": claims.Name,
"groups": claims.Groups,
})
if tid := strings.TrimSpace(claims.TID); tid != "" {
builder = builder.Claims(map[string]any{"tid": tid})
}
return builder.Serialize()
}
func (s *OIDCServer) Close() {
if s != nil && s.server != nil {
s.server.Close()
}
}