Authentification OIDC : package internal/auth et menu utilisateur
Suite du commit précédent : flux OIDC (découverte, PKCE, vérification de l'ID Token, groupes), sessions signées et bouton de déconnexion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018GBsBpHJW7GxFDBV7z1zmo
This commit is contained in:
1 parent
80fd60d006
commit
7397f0f789
3 files changed
+396
No files matched your search
@@ -0,0 +1,118 @@
|
||||
// Package auth fournit l'authentification OpenID Connect (flux Authorization
|
||||
// Code + PKCE) et les sessions signées associées, sans dépendance externe.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User est l'utilisateur authentifié, tel que décrit par le fournisseur.
|
||||
type User struct {
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
// session est le contenu du cookie de session (signé, non chiffré : il ne
|
||||
// contient que l'identité, pas de jeton).
|
||||
type session struct {
|
||||
User
|
||||
Exp int64 `json:"exp"`
|
||||
}
|
||||
|
||||
// loginState accompagne l'aller-retour vers le fournisseur (cookie de 10 min).
|
||||
type loginState struct {
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
Verifier string `json:"verifier"`
|
||||
Next string `json:"next"`
|
||||
Exp int64 `json:"exp"`
|
||||
}
|
||||
|
||||
var errBadCookie = errors.New("cookie invalide")
|
||||
|
||||
// signer signe et vérifie des valeurs JSON (HMAC-SHA256).
|
||||
type signer struct{ key []byte }
|
||||
|
||||
var b64 = base64.RawURLEncoding
|
||||
|
||||
func (s signer) mac(payload string) string {
|
||||
m := hmac.New(sha256.New, s.key)
|
||||
m.Write([]byte(payload))
|
||||
return b64.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
func (s signer) encode(v any) (string, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p := b64.EncodeToString(raw)
|
||||
return p + "." + s.mac(p), nil
|
||||
}
|
||||
|
||||
// decode vérifie la signature puis décode ; le contrôle d'expiration revient
|
||||
// à l'appelant (champ Exp).
|
||||
func (s signer) decode(value string, v any) error {
|
||||
p, sig, ok := strings.Cut(value, ".")
|
||||
if !ok || !hmac.Equal([]byte(sig), []byte(s.mac(p))) {
|
||||
return errBadCookie
|
||||
}
|
||||
raw, err := b64.DecodeString(p)
|
||||
if err != nil {
|
||||
return errBadCookie
|
||||
}
|
||||
if json.Unmarshal(raw, v) != nil {
|
||||
return errBadCookie
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err) // crypto/rand ne échoue pas sur les systèmes supportés
|
||||
}
|
||||
return b64.EncodeToString(b)
|
||||
}
|
||||
|
||||
// LoadSecret renvoie la clé de signature des sessions : secret si fourni
|
||||
// (16 caractères minimum), sinon le contenu de dir/.session-secret, créé
|
||||
// aléatoirement au premier démarrage (0600).
|
||||
func LoadSecret(secret, dir string) ([]byte, error) {
|
||||
if secret != "" {
|
||||
if len(secret) < 16 {
|
||||
return nil, errors.New("SESSION_SECRET trop court (16 caractères minimum)")
|
||||
}
|
||||
return []byte(secret), nil
|
||||
}
|
||||
file := filepath.Join(dir, ".session-secret")
|
||||
if b, err := os.ReadFile(file); err == nil {
|
||||
if k, err := hex.DecodeString(strings.TrimSpace(string(b))); err == nil && len(k) >= 32 {
|
||||
return k, nil
|
||||
}
|
||||
return nil, errors.New(file + " invalide (supprimer le fichier pour en générer un nouveau)")
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
k := make([]byte, 32)
|
||||
if _, err := rand.Read(k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(file, []byte(hex.EncodeToString(k)+"\n"), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func now() time.Time { return time.Now() }
|
||||
Reference in new issue
Block a user