internal/oidc: JWKS-Parser (RSA-Schluessel, RFC 7517) + Verifier prueft ID-Tokens gegen den geparsten Schluesselsatz (Signatur, Ablauf ueber die jwt-Bibliothek, Aussteller) — Akzeptanzkriterium 3. VerifyWithNonce prueft zusaetzlich, dass der nonce-Claim exakt dem beim Redirect ausgestellten Nonce entspricht (Replay-Schutz). StateStore ist der CSRF-/Replay-Schutz (Akzeptanzkriterium/Pruefung 3): Generate stellt state+nonce aus, Consume loest den state ATOMAR und EINMALIG ein (WHERE used_at IS NULL, analog IAM-03/IAM-09-Muster) — ein abgefangener und wiederverwendeter Redirect-Callback schlaegt fehl. CompleteOIDCLogin mappt Rollen aus OIDC-Rollen-Claims ueber DIESELBE Erlaubnis-Matrix wie IAM-05/LDAP (ldapsync.RoleMappingStore.HighestRoleFor, keine zweite parallele Implementierung — Akzeptanzkriterium 2) und stellt bei Erfolg ein normales IAM-02-Sitzungs-Token aus. Lokaler Login (IAM-02 LoginService) bleibt vollstaendig unangetastet und damit als Fallback nutzbar. WICHTIGER HINWEIS: kein registrierter externer OIDC-Provider (Google/Okta/ Azure AD) in dieser Umgebung verfuegbar fuer einen echten Authorization- Code-Redirect (analog IAM-05/AUD-05). ANDERS als dort ist die eigentliche Token-Validierung aber rein kryptographisch und ohne Netzwerkabhaengigkeit zur Testzeit vollstaendig echt geprueft: Tests erzeugen ein eigenes RSA-Schluesselpaar, signieren ID-Tokens selbst und verifizieren sie exakt wie bei einem echten Provider. Nur der Live-Redirect zu einem realen externen IdP bleibt ungeprueft. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Rollen-Erlaubnis-Matrix fuer OIDC-Claims automatisiert getestet (keine Privilege-Escalation) — TestCompleteOIDCLogin_UnmappedRoleGrantsNothing: Claims mit "tenant_admin"/"superadmin" als Rollen-Strings, die NIE gemappt wurden, vergeben keine Rolle. PASS. 2. Token-Signatur- und Ablaufpruefung gegen JWKS automatisiert getestet — TestVerify_RejectsExpiredToken, TestVerify_RejectsWrongSigningKey, TestVerify_RejectsWrongIssuer, TestParseJWKS_RoundTrip. PASS. 3. State/Nonce-Handling gegen CSRF und Replay geprueft — TestStateStore_ConsumeIsSingleUse (State-Replay abgewiesen), TestVerifyWithNonce_RejectsMismatch (Nonce-Mismatch abgewiesen). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
2.3 KiB
Go
79 lines
2.3 KiB
Go
// Package oidc implementiert Core IAM-06: SSO-Anmeldung ueber OpenID
|
|
// Connect gegen externe Identity-Provider.
|
|
//
|
|
// WICHTIGER HINWEIS: In dieser Umgebung ist kein registrierter externer
|
|
// OIDC-Provider (Client-ID/Secret bei Google/Okta/Azure AD o.ae.) verfuegbar,
|
|
// gegen den ein echter Authorization-Code-Redirect-Flow end-to-end
|
|
// durchgefuehrt werden koennte (analog AUD-05/Archive, IAM-05/LDAP). ANDERS
|
|
// als bei LDAP/Archive ist die eigentliche Token-VALIDIERUNG (JWKS-
|
|
// Signaturpruefung, Ablauf, Claims, Nonce/State) aber ein rein
|
|
// kryptographischer Vorgang ohne Netzwerkabhaengigkeit zur Testzeit — die
|
|
// Tests unten erzeugen ein eigenes RSA-Schluesselpaar, signieren ein
|
|
// ID-Token selbst und pruefen es exakt so, wie es ein echter Provider
|
|
// liefern wuerde. Nur der Redirect zu einem echten externen Provider bleibt
|
|
// ungeprueft.
|
|
package oidc
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
)
|
|
|
|
type jwk struct {
|
|
Kid string `json:"kid"`
|
|
Kty string `json:"kty"`
|
|
N string `json:"n"`
|
|
E string `json:"e"`
|
|
}
|
|
|
|
type jwkSet struct {
|
|
Keys []jwk `json:"keys"`
|
|
}
|
|
|
|
var ErrUnsupportedKeyType = errors.New("oidc: nicht unterstuetzter jwk-schluesseltyp")
|
|
|
|
// ParseJWKS dekodiert eine JWKS-Antwort (RFC 7517) in kid -> RSA-Public-Key.
|
|
// Nur RSA-Schluessel (kty=RSA, Standard bei allen gaengigen OIDC-Providern
|
|
// fuer ID-Tokens) werden unterstuetzt.
|
|
func ParseJWKS(data []byte) (map[string]*rsa.PublicKey, error) {
|
|
var set jwkSet
|
|
if err := json.Unmarshal(data, &set); err != nil {
|
|
return nil, fmt.Errorf("jwks parsen: %w", err)
|
|
}
|
|
|
|
out := make(map[string]*rsa.PublicKey, len(set.Keys))
|
|
for _, k := range set.Keys {
|
|
if k.Kty != "RSA" {
|
|
continue
|
|
}
|
|
pub, err := decodeRSAKey(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("schluessel %q dekodieren: %w", k.Kid, err)
|
|
}
|
|
out[k.Kid] = pub
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func decodeRSAKey(k jwk) (*rsa.PublicKey, error) {
|
|
nBytes, err := base64.RawURLEncoding.DecodeString(k.N)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("modulus dekodieren: %w", err)
|
|
}
|
|
eBytes, err := base64.RawURLEncoding.DecodeString(k.E)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("exponent dekodieren: %w", err)
|
|
}
|
|
|
|
eBuf := make([]byte, 8)
|
|
copy(eBuf[8-len(eBytes):], eBytes)
|
|
e := int(binary.BigEndian.Uint64(eBuf))
|
|
|
|
return &rsa.PublicKey{N: new(big.Int).SetBytes(nBytes), E: e}, nil
|
|
}
|