package oidc import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // AuthCodeTTL folgt RFC 6749 Empfehlung: Authorization Codes sind extrem // kurzlebig, der Client tauscht sie sofort gegen ein Token. const AuthCodeTTL = 60 * time.Second var ErrInvalidAuthCode = errors.New("oidc: ungueltiger, abgelaufener oder bereits verwendeter authorization code") // AuthCodeData ist das Ergebnis eines eingeloesten Authorization Codes. type AuthCodeData struct { ClientID string UserID string RedirectURI string Scopes []string } // AuthCodeStore verwaltet Authorization Codes innerhalb GENAU EINER Tenant-Datenbank. type AuthCodeStore struct { pool *pgxpool.Pool } func NewAuthCodeStore(pool *pgxpool.Pool) *AuthCodeStore { return &AuthCodeStore{pool: pool} } // Issue erzeugt einen neuen Authorization Code (Akzeptanzkriterium 2, Schritt // 1 des Flows) — gespeichert wird nur der Hash, das Klartext-Code wird per // Redirect an den Client uebertragen (Standard-OAuth2-Verhalten, der Code // selbst ist einmalig und kurzlebig genug, dass die Redirect-URL kein // nennenswertes Risiko darstellt). func (s *AuthCodeStore) Issue(ctx context.Context, clientID, userID, redirectURI string, scopes []string) (code string, err error) { code, err = randomToken(32) if err != nil { return "", fmt.Errorf("code erzeugen: %w", err) } hash := hashSecret(code) expiresAt := time.Now().Add(AuthCodeTTL) _, err = s.pool.Exec(ctx, ` INSERT INTO oidc_auth_codes (code_hash, client_id, user_id, redirect_uri, scopes, expires_at) VALUES ($1, $2, $3, $4, $5, $6) `, hash, clientID, userID, redirectURI, scopes, expiresAt) if err != nil { return "", fmt.Errorf("code speichern: %w", err) } return code, nil } // Consume loest einen Authorization Code genau einmal ein (Akzeptanzkriterium // 2, Schritt 2 des Flows) — atomar ueber die WHERE-Klausel (used_at IS NULL // AND expires_at > now()), gleiches Muster wie internal/authtoken (IAM-03). func (s *AuthCodeStore) Consume(ctx context.Context, code, clientID, redirectURI string) (AuthCodeData, error) { hash := hashSecret(code) var data AuthCodeData var storedClientID, storedRedirectURI string row := s.pool.QueryRow(ctx, ` UPDATE oidc_auth_codes SET used_at = now() WHERE code_hash = $1 AND used_at IS NULL AND expires_at > now() RETURNING client_id, user_id, redirect_uri, scopes `, hash) if err := row.Scan(&storedClientID, &data.UserID, &storedRedirectURI, &data.Scopes); err != nil { if errors.Is(err, pgx.ErrNoRows) { return AuthCodeData{}, ErrInvalidAuthCode } return AuthCodeData{}, fmt.Errorf("code einloesen: %w", err) } // client_id und redirect_uri muessen exakt zu denen des urspruenglichen // Authorize-Aufrufs passen (RFC 6749 4.1.3) — sonst koennte ein Code, der // fuer Client A ausgestellt wurde, bei Client B eingeloest werden. if storedClientID != clientID || storedRedirectURI != redirectURI { return AuthCodeData{}, ErrInvalidAuthCode } data.ClientID = storedClientID data.RedirectURI = storedRedirectURI return data, nil }