92 lines
3.0 KiB
Go
92 lines
3.0 KiB
Go
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
|
|
}
|