mail: moduleauth aus common v0.2.1
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Successful in 1m1s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 6s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Successful in 5s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 25s
CI / Web (dms-browser) (push) Failing after 1m20s
CI / Web (pdf-viewer) (push) Failing after 59s
CI / Web (shl) (push) Failing after 11s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 56s
Mail-Schnittstellen-Vertragstests / contract-tests (push) Successful in 17m7s
CI / Core-Modul (Build/Vet/Test) (pull_request) Successful in 1m0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Successful in 4s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Successful in 4s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Failing after 22s
CI / Web (dms-browser) (pull_request) Failing after 1m14s
CI / Web (pdf-viewer) (pull_request) Failing after 57s
CI / Web (shl) (pull_request) Failing after 12s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 17m22s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Failing after 56s
Mail-Schnittstellen-Vertragstests / contract-tests (pull_request) Successful in 16m53s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 16m43s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Failing after 1m43s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Successful in 5m49s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Successful in 17m20s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Failing after 2s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5.5 committed 2026-10-28 11:37:35 +01:00
1 parent a9e2cfc29a
commit 8b9d0f8316
19 files changed
+17 -479

No files matched your search

+1 -1
View File
@@ -17,8 +17,8 @@ import (
"os/signal"
"syscall"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailapi"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
+1 -1
View File
@@ -18,8 +18,8 @@ import (
"strings"
"syscall"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailapi"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.25.0
toolchain go1.25.13
require (
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
gitea.perlbach24.de/scripte/nexarch/common v0.2.1
github.com/aws/aws-sdk-go-v2 v1.45.1
github.com/aws/aws-sdk-go-v2/config v1.33.1
github.com/aws/aws-sdk-go-v2/credentials v1.20.1
+2 -2
View File
@@ -1,5 +1,5 @@
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc=
gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks=
github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"github.com/getkin/kin-openapi/routers"
"github.com/getkin/kin-openapi/routers/gorillamux"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"testing"
"time"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
)
+2 -2
View File
@@ -17,7 +17,7 @@
// sich Core dort ein, ohne dass dieses Paket geändert werden muss.
//
// INT-18: Jede Route verlangt ein gueltiges Core-Modul-Token
// (mail/internal/moduleauth, aud=mail seit API-18); der Mandant kommt
// (common/moduleauth, aud=mail seit API-18); der Mandant kommt
// ausschliesslich aus dessen Claims. Ein evtl. mitgesendeter
// "tenant"-Query-Parameter wird nicht gelesen (Akzeptanzkriterium 2:
// ignoriert, nicht nur zusaetzlich geprueft) — es gibt technisch keinen
@@ -31,8 +31,8 @@ import (
"net/http"
"strconv"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
+1 -1
View File
@@ -6,8 +6,8 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
"testing"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
+1 -1
View File
@@ -13,8 +13,8 @@ import (
"net/http"
"strconv"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailexport"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
)
// ExportService ist die für diese API benötigte Teilmenge von
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
)
type fakeExportService struct {
@@ -13,8 +13,8 @@ import (
"strconv"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailrules"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/testdb"
)
+1 -1
View File
@@ -24,8 +24,8 @@ import (
"net/http"
"strconv"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailrules"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
)
// RulesStore ist die für diese API benötigte Teilmenge von
@@ -11,8 +11,8 @@ import (
"sync"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailrules"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
)
// fakeRulesStore ist ein In-Memory-Fake für RulesStore — reine
@@ -8,8 +8,8 @@ import (
"net/http/httptest"
"testing"
"gitea.perlbach24.de/scripte/nexarch/common/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/mailapi"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
)
-76
View File
@@ -1,76 +0,0 @@
package moduleauth
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
)
// Identity ist die aus einem gueltigen Modul-Token gewonnene Identitaet.
// Der Mandant kommt ausschliesslich hieraus, nie aus einem Anfrageparameter
// (INT-18, Befund F-20: Mail-API las den Mandanten bisher aus "?tenant=").
type Identity struct {
UserID string
Tenant string
Role string
}
type identityKey struct{}
func WithIdentity(ctx context.Context, id Identity) context.Context {
return context.WithValue(ctx, identityKey{}, id)
}
func IdentityFrom(ctx context.Context) (Identity, bool) {
id, ok := ctx.Value(identityKey{}).(Identity)
return id, ok
}
type errorResponse struct {
Error string `json:"error"`
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(errorResponse{Error: message})
}
// RequireAuth verlangt ein gueltiges Modul-Token (Bearer) fuer next. Es gibt
// hier bewusst keinen "caller"-Uebergangsmodus wie im DMS: die Mail-API war
// bislang nirgends verdrahtet (kein cmd/-Aufrufer), es gibt also keinen
// bestehenden Client, dessen Kompatibilitaet zu wahren waere.
func RequireAuth(v *Verifier, next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token, ok := bearer(r)
if !ok {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
return
}
if v == nil {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
return
}
claims, err := v.Verify(r.Context(), token)
if err != nil {
if errors.Is(err, ErrInvalidToken) {
writeError(w, http.StatusUnauthorized, "nicht angemeldet")
} else {
writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar")
}
return
}
id := Identity{UserID: claims.Subject, Tenant: claims.Tenant, Role: claims.Role}
next.ServeHTTP(w, r.WithContext(WithIdentity(r.Context(), id)))
}
}
func bearer(r *http.Request) (string, bool) {
h := r.Header.Get("Authorization")
if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") {
return strings.TrimSpace(h[7:]), true
}
return "", false
}
-66
View File
@@ -1,66 +0,0 @@
package moduleauth
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"time"
)
// TestIssuer signiert Modul-Tokens mit einem einmalig erzeugten Testschluessel
// (nur fuer Tests anderer Pakete gedacht, siehe INT-18: mailapi/mailrulesapi
// koennen ErrInvalidToken/echte Signaturpruefung nicht ohne einen Aussteller
// testen, ein echter Kontodienst ist in Unit-Tests nicht verfuegbar).
type TestIssuer struct {
kid string
pub ed25519.PublicKey
prv ed25519.PrivateKey
}
// NewTestVerifierAndIssuer erzeugt ein Ed25519-Testschluesselpaar, einen
// Verifier, der ausschliesslich diesen Schluessel kennt, und den passenden
// Aussteller. audience/issuer leer = keine Bindung geprueft (Verhalten wie
// im Uebergang vor API-18/API-15 verdrahtet sind).
func NewTestVerifierAndIssuer() (*Verifier, *TestIssuer) {
pub, prv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
iss := &TestIssuer{kid: "test-key", pub: pub, prv: prv}
v := NewVerifier(time.Minute, "", func(context.Context) (map[string]ed25519.PublicKey, error) {
return map[string]ed25519.PublicKey{iss.kid: iss.pub}, nil
})
return v, iss
}
func b64json(v any) string {
raw, _ := json.Marshal(v)
return base64.RawURLEncoding.EncodeToString(raw)
}
// Token stellt ein gueltiges Modul-Token fuer subject/tenant/role aus,
// optional mit audience/issuer (leer = Claim wird weggelassen).
func (t *TestIssuer) Token(subject, tenant, role, audience, issuer string) string {
return t.TokenExpiringAt(subject, tenant, role, audience, issuer, time.Now().Add(time.Hour))
}
// TokenExpiringAt wie Token, aber mit fest vorgegebenem Ablaufzeitpunkt
// (fuer Tests, die ein bereits abgelaufenes Token brauchen).
func (t *TestIssuer) TokenExpiringAt(subject, tenant, role, audience, issuer string, exp time.Time) string {
now := time.Now()
claims := map[string]any{
"sub": subject, "tenant": tenant, "role": role,
"iat": now.Unix(), "exp": exp.Unix(),
}
if audience != "" {
claims["aud"] = audience
}
if issuer != "" {
claims["iss"] = issuer
}
h := b64json(map[string]string{"alg": "EdDSA", "kid": t.kid, "typ": "JWT"})
signing := h + "." + b64json(claims)
return signing + "." + base64.RawURLEncoding.EncodeToString(ed25519.Sign(t.prv, []byte(signing)))
}
-258
View File
@@ -1,258 +0,0 @@
// Package moduleauth verifiziert Core-Modul-JWTs (API-05: EdDSA, kid, JWKS) lokal.
package moduleauth
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
var ErrInvalidToken = errors.New("moduleauth: ungueltiges token")
const (
leeway = 30 * time.Second
minRefreshInterval = 30 * time.Second
maxJWKSBytes = 1 << 20
)
type Claims struct {
Subject string
Tenant string
Role string
}
type KeyFetchFunc func(ctx context.Context) (map[string]ed25519.PublicKey, error)
type Verifier struct {
fetch KeyFetchFunc
ttl time.Duration
tenant string
now func() time.Time
audience string
issuer string
enforceAudience bool
mu sync.Mutex
keys map[string]ed25519.PublicKey
fetchedAt time.Time
lastAttempt time.Time
}
func NewVerifier(ttl time.Duration, tenant string, fetch KeyFetchFunc) *Verifier {
return &Verifier{fetch: fetch, ttl: ttl, tenant: tenant, now: time.Now}
}
// Expect bindet das Modul an ein Zielmodul ("aud") und einen Aussteller ("iss")
// (QA-15 F-07). Ist enforce gesetzt, weist der Verifier Tokens ohne passende
// "aud" ab; ohne enforce (Uebergang beim Ausrollen) werden Tokens ohne "aud"
// noch angenommen, ein Token mit fremder "aud" wird aber immer abgewiesen.
// "iss" wird geprueft, sobald ein Aussteller konfiguriert ist und das Token
// einen traegt bzw. enforce gilt.
func (v *Verifier) Expect(audience, issuer string, enforce bool) *Verifier {
v.audience, v.issuer, v.enforceAudience = audience, issuer, enforce
return v
}
// keySet liefert die Schluessel: frisch aus dem Cache, sonst per Abruf; schlaegt
// der Abruf fehl, gilt der letzte bekannte Satz weiter (Core-Ausfall), ohne
// Satz ist die Verifikation nicht moeglich. force erzwingt einen Abruf (unbekannte
// kid nach Rotation), hoechstens einmal je minRefreshInterval.
func (v *Verifier) keySet(ctx context.Context, force bool) (map[string]ed25519.PublicKey, error) {
v.mu.Lock()
defer v.mu.Unlock()
now := v.now()
fresh := v.keys != nil && now.Sub(v.fetchedAt) < v.ttl
if fresh && !force {
return v.keys, nil
}
if force && now.Sub(v.lastAttempt) < minRefreshInterval {
if v.keys != nil {
return v.keys, nil
}
}
v.lastAttempt = now
keys, err := v.fetch(ctx)
if err == nil {
v.keys, v.fetchedAt = keys, now
return keys, nil
}
if v.keys != nil {
return v.keys, nil
}
return nil, fmt.Errorf("moduleauth: schluesselsatz nicht verfuegbar: %w", err)
}
type header struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
type payload struct {
Sub string `json:"sub"`
Tenant string `json:"tenant"`
Role string `json:"role"`
Exp float64 `json:"exp"`
Iat float64 `json:"iat"`
Iss string `json:"iss"`
Aud json.RawMessage `json:"aud"`
}
// audiences liest "aud" als String oder Liste (RFC 7519 erlaubt beides).
func (p payload) audiences() ([]string, bool) {
if len(p.Aud) == 0 || string(p.Aud) == "null" {
return nil, true
}
var one string
if err := json.Unmarshal(p.Aud, &one); err == nil {
return []string{one}, true
}
var many []string
if err := json.Unmarshal(p.Aud, &many); err == nil {
return many, true
}
return nil, false
}
func (v *Verifier) checkScope(p payload) bool {
auds, ok := p.audiences()
if !ok {
return false
}
if v.audience != "" {
if len(auds) == 0 {
if v.enforceAudience {
return false
}
} else {
match := false
for _, a := range auds {
if a == v.audience {
match = true
break
}
}
if !match {
return false
}
}
}
if v.issuer != "" && (p.Iss != "" || v.enforceAudience) && p.Iss != v.issuer {
return false
}
return true
}
func (v *Verifier) Verify(ctx context.Context, token string) (Claims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return Claims{}, ErrInvalidToken
}
var h header
if err := decodePart(parts[0], &h); err != nil || h.Alg != "EdDSA" || h.Kid == "" {
return Claims{}, ErrInvalidToken
}
sig, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil || len(sig) != ed25519.SignatureSize {
return Claims{}, ErrInvalidToken
}
keys, err := v.keySet(ctx, false)
if err != nil {
return Claims{}, err
}
pub, ok := keys[h.Kid]
if !ok {
if keys, err = v.keySet(ctx, true); err != nil {
return Claims{}, err
}
if pub, ok = keys[h.Kid]; !ok {
return Claims{}, ErrInvalidToken
}
}
if !ed25519.Verify(pub, []byte(parts[0]+"."+parts[1]), sig) {
return Claims{}, ErrInvalidToken
}
var p payload
if err := decodePart(parts[1], &p); err != nil || p.Sub == "" || p.Exp == 0 {
return Claims{}, ErrInvalidToken
}
now := v.now()
if now.After(time.Unix(int64(p.Exp), 0).Add(leeway)) {
return Claims{}, ErrInvalidToken
}
if p.Iat != 0 && time.Unix(int64(p.Iat), 0).After(now.Add(leeway)) {
return Claims{}, ErrInvalidToken
}
if v.tenant != "" && p.Tenant != v.tenant {
return Claims{}, ErrInvalidToken
}
if !v.checkScope(p) {
return Claims{}, ErrInvalidToken
}
return Claims{Subject: p.Sub, Tenant: p.Tenant, Role: p.Role}, nil
}
func decodePart(part string, into any) error {
raw, err := base64.RawURLEncoding.DecodeString(part)
if err != nil {
return err
}
return json.Unmarshal(raw, into)
}
type jwksResponse struct {
Keys []struct {
Kid string `json:"kid"`
PublicKey string `json:"public_key"`
} `json:"keys"`
}
func HTTPKeyFetcher(url string, client *http.Client) KeyFetchFunc {
if client == nil {
client = &http.Client{Timeout: 5 * time.Second}
}
return func(ctx context.Context) (map[string]ed25519.PublicKey, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("jwks-abruf: status %d", resp.StatusCode)
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxJWKSBytes))
if err != nil {
return nil, err
}
var r jwksResponse
if err := json.Unmarshal(raw, &r); err != nil {
return nil, fmt.Errorf("jwks parsen: %w", err)
}
out := make(map[string]ed25519.PublicKey, len(r.Keys))
for _, k := range r.Keys {
b, err := base64.StdEncoding.DecodeString(k.PublicKey)
if err != nil || len(b) != ed25519.PublicKeySize || k.Kid == "" {
return nil, fmt.Errorf("jwks: schluessel %q ungueltig", k.Kid)
}
out[k.Kid] = ed25519.PublicKey(b)
}
if len(out) == 0 {
return nil, errors.New("jwks: leerer schluesselsatz")
}
return out, nil
}
}
-62
View File
@@ -1,62 +0,0 @@
package moduleauth
import (
"context"
"testing"
"time"
)
// TestVerify_AcceptsValidToken belegt den Grundfall: ein vom Testaussteller
// signiertes, gueltiges Token liefert die erwarteten Claims.
func TestVerify_AcceptsValidToken(t *testing.T) {
v, iss := NewTestVerifierAndIssuer()
tok := iss.Token("u1", "acme", "user", "", "")
c, err := v.Verify(context.Background(), tok)
if err != nil || c.Subject != "u1" || c.Tenant != "acme" || c.Role != "user" {
t.Fatalf("claims=%+v err=%v", c, err)
}
}
// TestVerify_RejectsForeignSignature: ein Token eines anderen Schluessels
// (der Verifier kennt dessen kid nicht) wird abgewiesen.
func TestVerify_RejectsForeignSignature(t *testing.T) {
v, _ := NewTestVerifierAndIssuer()
_, other := NewTestVerifierAndIssuer()
if _, err := v.Verify(context.Background(), other.Token("u1", "acme", "user", "", "")); err == nil {
t.Fatal("erwartete fehler bei fremder signatur")
}
}
// TestScope_AudienceMismatchRejected ist der Kern von API-18/F-07: mit
// Expect(audience="mail", ..., enforce=true) gebundene Verifier weisen ein
// Token mit aud="dms" ab, akzeptieren aber aud="mail".
func TestScope_AudienceMismatchRejected(t *testing.T) {
v, iss := NewTestVerifierAndIssuer()
v.Expect("mail", "", true)
if _, err := v.Verify(context.Background(), iss.Token("u1", "acme", "user", "dms", "")); err == nil {
t.Fatal("erwartete fehler bei aud=dms gegen einen mail-gebundenen verifier")
}
if _, err := v.Verify(context.Background(), iss.Token("u1", "acme", "user", "mail", "")); err != nil {
t.Fatalf("erwartete erfolg bei aud=mail, habe: %v", err)
}
}
// TestScope_EnforceRejectsTokenWithoutAudience: mit enforce=true wird ein
// Token OHNE aud-Claim ebenfalls abgewiesen (kein stiller Rueckfall).
func TestScope_EnforceRejectsTokenWithoutAudience(t *testing.T) {
v, iss := NewTestVerifierAndIssuer()
v.Expect("mail", "", true)
if _, err := v.Verify(context.Background(), iss.Token("u1", "acme", "user", "", "")); err == nil {
t.Fatal("erwartete fehler bei fehlendem aud unter enforce=true")
}
}
// TestVerify_RejectsExpiredToken.
func TestVerify_RejectsExpiredToken(t *testing.T) {
v, iss := NewTestVerifierAndIssuer()
expired := iss.TokenExpiringAt("u1", "acme", "user", "", "", time.Now().Add(-2*time.Hour))
if _, err := v.Verify(context.Background(), expired); err == nil {
t.Fatal("erwartete fehler bei abgelaufenem token")
}
}