Skip to content
Back to skills

Setup Auth Magic Link

ASecurity

Met en place un système d'authentification Magic Link (passwordless) complet pour FastAPI + React

  • 4 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 12, 2026
developmenttypescriptpythongosqlreactfastapiapifrontendbackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill setup-auth-magic-link --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Setup Auth Magic Link?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Setup Auth Magic Link
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-setup-auth-magic-link/badge)](https://www.skillsdirectory.com/skills/aibot88-setup-auth-magic-link)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: Met en place un système d'authentification Magic Link (passwordless) complet pour FastAPI + React
argument-hint: [description du projet ou contexte]
model: opus
---

# Setup Auth Magic Link — Authentification sans mot de passe

Implémente un système d'authentification **Magic Link** (passwordless) complet et sécurisé pour une application FastAPI + React. Le principe : l'utilisateur saisit son email, reçoit un lien unique par email, clique dessus et est authentifié. Aucun mot de passe n'est stocké ni transmis.

## Variables

CONTEXTE_PROJET: $ARGUMENTS

## Instructions

- Lis le `CONTEXTE_PROJET` pour adapter l'implémentation au projet cible.
- **Utilise le mode team** : invoque le skill `plan-with-team` pour planifier, puis `build` pour exécuter avec des agents spécialisés (backend, frontend, email, validation).
- Suis la checklist ci-dessous comme référence. Chaque point est essentiel.

## Architecture Magic Link - Checklist

### 1. Flow d'authentification

```
1. L'utilisateur saisit son email sur /login
2. POST /auth/magic-link { email }
   → Génère un token unique, single-use
   → Envoie un email avec le lien : https://app.example.com/auth/verify?token=xxx
   → Retourne toujours HTTP 200 (même si email inconnu — anti-énumération)
3. L'utilisateur clique sur le lien dans son email
4. GET /auth/verify?token=xxx (ou POST /auth/verify { token })
   → Valide le token (existe, non expiré, non utilisé)
   → Marque le token comme utilisé (single-use)
   → Génère access_token + refresh_token en cookies HttpOnly
   → Redirige vers l'app
5. Les requêtes suivantes utilisent les cookies JWT (identique à l'auth classique)
```

### 2. Génération du token Magic Link

- Utiliser `secrets.token_urlsafe(32)` — 256 bits d'entropie, URL-safe
- **Ne jamais utiliser UUID** comme token — prévisible, seulement 122 bits d'entropie
- Stocker le **hash** du token en BD (SHA-256), pas le token en clair — si la BD est compromise, les tokens ne sont pas exploitables
- Stocker avec : `email`, `token_hash`, `created_at`, `expires_at`, `used_at` (null si non utilisé)

```python
import secrets
import hashlib

token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
# Stocker token_hash en BD, envoyer token par email
```

### 3. Modèle BD — MagicLinkToken

```python
class MagicLinkToken(SQLModel, table=True):
    __tablename__ = "magic_link_tokens"

    id: UUID = Field(default_factory=uuid4, primary_key=True)
    email: str = Field(max_length=255, index=True)
    token_hash: str = Field(max_length=64, unique=True, index=True)  # SHA-256 hex
    created_at: datetime = Field(default_factory=datetime.utcnow)
    expires_at: datetime  # created_at + 15 min
    used_at: datetime | None = Field(default=None)  # null = non utilisé
    ip_address: str | None = Field(default=None, max_length=45)  # IP du demandeur
```

### 4. Endpoints Backend (FastAPI)

```
POST /auth/magic-link      → demande un magic link (email) — rate limited
POST /auth/verify-magic    → vérifie le token, crée la session JWT
POST /auth/refresh         → refresh token (identique à l'auth classique)
POST /auth/logout          → supprime les cookies
GET  /auth/me              → retourne l'utilisateur courant
```

### 5. Sécurité du token

- **Expiration courte** : 15 minutes maximum (10 min recommandé)
- **Single-use** : marquer `used_at` dès la première vérification (dans la même transaction SQL)
- **Anti-énumération** : toujours retourner le même message ("Si cet email existe, un lien a été envoyé") — même timing de réponse
- **Anti-brute-force** : rate limiting strict sur `/auth/magic-link` (3 req/min par email, 10 req/min par IP)
- **Nettoyage** : supprimer les tokens expirés périodiquement (tâche cron ou à chaque requête)
- **IP logging** : stocker l'IP du demandeur pour audit

### 6. Anti-énumération — Timing constant

```python
@router.post("/magic-link")
@limiter.limit("10/minute")
async def request_magic_link(data: MagicLinkRequest, request: Request, session: DbSession):
    # Toujours faire le même travail, que l'email existe ou non
    user = await get_user_by_email(session, data.email)

    if user and user.is_active:
        token = secrets.token_urlsafe(32)
        token_hash = hashlib.sha256(token.encode()).hexdigest()
        # Stocker en BD + envoyer email
        await store_magic_token(session, data.email, token_hash, request.client.host)
        await send_magic_link_email(data.email, token)
    else:
        # Simuler le même délai pour empêcher le timing attack
        await asyncio.sleep(random.uniform(0.1, 0.3))

    # Toujours la même réponse
    return {"message": "Si cet email est associé à un compte, un lien de connexion a été envoyé."}
```

### 7. Vérification du token

```python
@router.post("/verify-magic")
async def verify_magic_link(data: VerifyMagicRequest, session: DbSession, response: Response):
    token_hash = hashlib.sha256(data.token.encode()).hexdigest()

    # Requête atomique : sélectionner ET marquer comme utilisé
    result = await session.execute(
        select(MagicLinkToken)
        .where(
            MagicLinkToken.token_hash == token_hash,
            MagicLinkToken.used_at.is_(None),
            MagicLinkToken.expires_at > datetime.utcnow(),
        )
        .with_for_update()  # Verrou pessimiste — empêche la double utilisation
    )
    magic_token = result.scalar_one_or_none()

    if not magic_token:
        raise HTTPException(status_code=401, detail="Lien invalide ou expiré")

    magic_token.used_at = datetime.utcnow()

    user = await get_user_by_email(session, magic_token.email)
    if not user or not user.is_active:
        raise HTTPException(status_code=401, detail="Compte inactif")

    # Créer la session JWT (identique à l'auth classique)
    access_token = create_access_token(data={"sub": str(user.id)})
    refresh_token = create_refresh_token(data={"sub": str(user.id)})
    _set_auth_cookies(response, access_token, refresh_token)

    return {"access_token": access_token, "refresh_token": refresh_token}
```

### 8. Session JWT après vérification

- Après vérification du magic link, le système bascule sur des **cookies HttpOnly JWT** identiques à l'auth classique
- Appliquer toutes les mêmes règles que le skill `setup-auth` :
  - Cookies HttpOnly, Secure, SameSite=Lax
  - Scope `/api` pour access, `/api/auth` pour refresh
  - Rotation du refresh token
  - Révocation (token family ID ou blacklist)
  - Rate limiting sur `/auth/refresh`

### 9. Envoi d'email

- Utiliser un service SMTP ou un provider transactionnel (Resend, SendGrid, Postmark)
- **HTML + texte brut** : toujours envoyer les deux versions
- Le lien doit pointer vers le **frontend** (pas directement l'API) : `https://app.example.com/auth/verify?token=xxx`
- Le frontend récupère le token depuis l'URL et appelle `POST /auth/verify-magic`
- **Ne jamais inclure le token dans le sujet** de l'email (visible dans les aperçus)
- Configurer SPF, DKIM, DMARC pour éviter que les emails atterrissent en spam

### 10. Frontend React

- **Page /login** : formulaire email uniquement, message de confirmation après soumission
- **Page /auth/verify** : lit le token depuis `searchParams`, appelle l'API, redirige vers `/`
- Afficher un état de chargement pendant la vérification
- Gérer les erreurs : token expiré, déjà utilisé, invalide — avec messages clairs
- **Pas de stockage du token** côté client (il vient de l'URL, est envoyé une fois, puis c'est les cookies JWT)
- Route guard identique à l'auth classique (`__root.tsx`)
- `credentials: "include"` sur tous les fetch

```typescript
// /auth/verify page
function VerifyMagicLink() {
  const { token } = useSearch({ from: "/auth/verify" })
  const navigate = useNavigate()

  useEffect(() => {
    if (!token) { navigate({ to: "/login" }); return }

    fetch(`${API_BASE}/auth/verify-magic`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include",
      body: JSON.stringify({ token }),
    })
      .then(res => {
        if (res.ok) navigate({ to: "/" })
        else navigate({ to: "/login", search: { error: "expired" } })
      })
      .catch(() => navigate({ to: "/login", search: { error: "network" } }))
  }, [token])

  return <Loader2 className="animate-spin" />
}
```

### 11. Rate Limiting spécifique

| Endpoint | Limite | Raison |
|----------|--------|--------|
| `POST /auth/magic-link` | 3/min par email | Anti-spam email |
| `POST /auth/magic-link` | 10/min par IP | Anti-abus global |
| `POST /auth/verify-magic` | 10/min par IP | Anti-brute-force token |
| `POST /auth/refresh` | 5/min par IP | Standard |

Le rate limiting par email nécessite un limiter custom (pas juste l'IP) :
```python
def get_email_key(request: Request) -> str:
    """Rate limit key based on email in request body."""
    # Attention : nécessite de parser le body, utiliser un middleware ou cache
    return f"magic_link:{request.state.email}"
```

### 12. Headers de sécurité HTTP

Identique au skill `setup-auth` : HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Permissions-Policy.

### 13. CORS

Identique au skill `setup-auth` : whitelist explicite, credentials=True, méthodes/headers explicites.

## Points souvent oubliés

- **Hasher le token en BD** (SHA-256) — ne jamais stocker le token brut
- **Verrou pessimiste** (`with_for_update()`) lors de la vérification pour empêcher la race condition double-utilisation
- **Timing constant** sur la demande de magic link — ne pas révéler si l'email existe
- **Réponse identique** que l'email existe ou non
- **Expiration des tokens** : nettoyer régulièrement les tokens expirés (cron job ou cleanup lazy)
- **Le lien pointe vers le frontend**, pas vers l'API directement
- **Ne pas logger le token** dans les logs applicatifs (logger le hash uniquement)
- **Email en HTML + texte brut** — certains clients email n'affichent pas le HTML
- **Créer l'utilisateur automatiquement ?** — décision métier. Si oui, seulement pour les domaines autorisés
- **Throttle par email** en plus de l'IP — sinon un attaquant peut spammer la boîte de réception d'un utilisateur

## Paquets Python nécessaires

```
pyjwt[cryptography]
argon2-cffi          # pour les API keys si besoin (pas les magic tokens)
slowapi
python-multipart
aiosmtplib           # ou httpx pour les API email (Resend, SendGrid)
```

## Paquets Frontend nécessaires

```
@tanstack/react-query
```

## Comparaison Magic Link vs Mot de passe

| Aspect | Magic Link | Mot de passe |
|--------|-----------|--------------|
| UX | Plus simple (pas de MDP à retenir) | Familier |
| Sécurité stockage | Rien à stocker côté serveur (pas de hash MDP) | Hash bcrypt/Argon2 |
| Dépendance | Email fiable requis | Aucune |
| Vitesse login | Plus lent (attente email) | Instantané |
| Phishing | Résistant (le token est unique) | Vulnérable |
| Offline | Impossible | Possible |
| 2FA | Le magic link EST le 2FA (possession email) | Nécessite TOTP/SMS en plus |

Files in this skill

  • SKILL.md10.9 KB
  • source.json631 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.