encore-go-auth

Par encoredev · skills

Implémenter l'authentification avec Encore Go.

npx skills add https://github.com/encoredev/skills --skill encore-go-auth

Authentification Encore Go

Instructions

Encore Go fournit un système d'authentification intégré utilisant l'annotation //encore:authhandler.

1. Créer un gestionnaire d'authentification

package auth

import (
    "context"
    "encore.dev/beta/auth"
    "encore.dev/beta/errs"
)

// AuthParams définit ce que reçoit le gestionnaire d'authentification
type AuthParams struct {
    Authorization string `header:"Authorization"`
}

// AuthData définit ce à quoi les requêtes authentifiées ont accès
type AuthData struct {
    UserID string
    Email  string
    Role   string
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    token := strings.TrimPrefix(params.Authorization, "Bearer ")

    payload, err := verifyToken(token)
    if err != nil {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "invalid token",
        }
    }

    return auth.UID(payload.UserID), &AuthData{
        UserID: payload.UserID,
        Email:  payload.Email,
        Role:   payload.Role,
    }, nil
}

2. Protéger les endpoints

package user

import "context"

// Endpoint protégé - requiert l'authentification
//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Seuls les utilisateurs authentifiés arrivent ici
}

// Endpoint public - pas d'authentification requise
//encore:api public method=GET path=/health
func Health(ctx context.Context) (*HealthResponse, error) {
    return &HealthResponse{Status: "ok"}, nil
}

3. Accéder aux données d'authentification dans les endpoints

package user

import (
    "context"
    "encore.dev/beta/auth"
    myauth "myapp/auth"  // Importer votre package d'authentification
)

//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Obtenir l'ID utilisateur
    userID, ok := auth.UserID()
    if !ok {
        // Ne devrait pas se produire avec un endpoint authentifié
    }

    // Obtenir les données d'authentification complètes
    data := auth.Data().(*myauth.AuthData)

    return &Profile{
        UserID: string(userID),
        Email:  data.Email,
        Role:   data.Role,
    }, nil
}

Signature du gestionnaire d'authentification

Le gestionnaire d'authentification doit :

  1. Avoir l'annotation //encore:authhandler
  2. Accepter context.Context et un pointeur sur un struct de paramètres
  3. Retourner (auth.UID, *YourAuthData, error)
//encore:authhandler
func MyAuthHandler(ctx context.Context, params *Params) (auth.UID, *AuthData, error)

Comportement du gestionnaire d'authentification

Scénario Retourne Résultat
Identifiants valides (uid, data, nil) Requête authentifiée
Identifiants invalides ("", nil, err) avec errs.Unauthenticated Réponse 401
Autre erreur ("", nil, err) Requête interrompue

Patterns d'authentification courants

Validation de token JWT

import "github.com/golang-jwt/jwt/v5"

var secrets struct {
    JWTSecret string
}

func verifyToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) {
        return []byte(secrets.JWTSecret), nil
    })
    if err != nil {
        return nil, err
    }

    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, errors.New("invalid token")
    }

    return claims, nil
}

Authentification par clé API

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    apiKey := params.Authorization

    user, err := db.QueryRow[User](ctx, `
        SELECT id, email, role FROM users WHERE api_key = $1
    `, apiKey)
    if err != nil {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "invalid API key",
        }
    }

    return auth.UID(user.ID), &AuthData{
        UserID: user.ID,
        Email:  user.Email,
        Role:   user.Role,
    }, nil
}

Authentification par cookie

type AuthParams struct {
    Cookie string `header:"Cookie"`
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    sessionID := parseCookie(params.Cookie, "session")
    if sessionID == "" {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "no session",
        }
    }

    session, err := getSession(ctx, sessionID)
    if err != nil || session.ExpiresAt.Before(time.Now()) {
        return "", nil, &errs.Error{
            Code:    errs.Unauthenticated,
            Message: "session expired",
        }
    }

    return auth.UID(session.UserID), &AuthData{
        UserID: session.UserID,
        Email:  session.Email,
        Role:   session.Role,
    }, nil
}

Authentification multi-sources (Cookie + Header + Query)

Les paramètres d'authentification peuvent extraire des données de plusieurs sources :

import "net/http"

type AuthParams struct {
    SessionCookie *http.Cookie `cookie:"session"`       // Depuis un cookie
    Authorization string       `header:"Authorization"` // Depuis un header
    ClientID      string       `query:"client_id"`      // Depuis la query string
}

//encore:authhandler
func Authenticate(ctx context.Context, params *AuthParams) (auth.UID, *AuthData, error) {
    // Essayer d'abord le cookie de session
    if params.SessionCookie != nil {
        return authenticateWithSession(ctx, params.SessionCookie.Value)
    }

    // Retomber sur le header Authorization
    if params.Authorization != "" {
        return authenticateWithToken(ctx, params.Authorization)
    }

    return "", nil, &errs.Error{
        Code:    errs.Unauthenticated,
        Message: "no credentials provided",
    }
}

Authentification de service à service

Les données d'authentification se propagent automatiquement dans les appels de service interne :

package order

import (
    "context"
    "myapp/user"  // Importer le service utilisateur
)

//encore:api auth method=GET path=/orders/:id
func GetOrderWithUser(ctx context.Context, params *GetOrderParams) (*OrderWithUser, error) {
    order, err := getOrder(ctx, params.ID)
    if err != nil {
        return nil, err
    }

    // L'authentification est automatiquement propagée à cet appel
    profile, err := user.GetProfile(ctx)
    if err != nil {
        return nil, err
    }

    return &OrderWithUser{Order: order, User: profile}, nil
}

Tests avec authentification

Surcharger les données d'authentification dans les tests avec auth.WithContext :

package user_test

import (
    "context"
    "testing"

    "encore.dev/beta/auth"
    myauth "myapp/auth"
    "myapp/user"
)

func TestGetProfile(t *testing.T) {
    // Créer un contexte avec les données d'authentification
    ctx := auth.WithContext(
        context.Background(),
        auth.UID("test-user-123"),
        &myauth.AuthData{
            UserID: "test-user-123",
            Email:  "test@example.com",
            Role:   "user",
        },
    )

    // Appeler l'endpoint avec le contexte authentifié
    profile, err := user.GetProfile(ctx)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if profile.Email != "test@example.com" {
        t.Errorf("expected test@example.com, got %s", profile.Email)
    }
}

Recommandations

  • Un seul //encore:authhandler par application
  • Retourner auth.UID comme première valeur retournée (identifiant utilisateur)
  • Retourner votre struct AuthData personnalisé comme deuxième valeur
  • Utiliser auth.UserID() pour obtenir l'ID utilisateur authentifié
  • Utiliser auth.Data() et type assert pour obtenir les données d'authentification complètes
  • L'authentification se propage automatiquement dans les appels de service à service
  • Utiliser auth.WithContext() pour surcharger l'authentification dans les tests
  • Garder les gestionnaires d'authentification rapides - ils s'exécutent à chaque requête authentifiée

Skills similaires