All skills
encoredev avatar

/go-auth

@cb69bb1 official
by Encoreencoredev/skills28 stars
5

Protect Encore Go endpoints with authentication and authorize callers. Covers `auth.AuthHandler`, `auth.UserID`, the `Authorization` header, and `//encore:api auth`.

  • 1 file
  • 8.2 KB
  • Updated 5 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/encoredev/skills/go-auth

This session only. Nothing lands on disk.

SKILL.md

≈44 tokens always: the name and description. ≈1.9k when used: this file.

Encore Go Authentication

Instructions

Encore Go provides a built-in authentication system using the //encore:authhandler annotation.

1. Create an Auth Handler

package auth

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

// AuthParams defines what the auth handler receives
type AuthParams struct {
    Authorization string `header:"Authorization"`
}

// AuthData defines what authenticated requests have access to
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. Protect Endpoints

package user

import "context"

// Protected endpoint - requires authentication
//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Only authenticated users reach here
}

// Public endpoint - no authentication required
//encore:api public method=GET path=/health
func Health(ctx context.Context) (*HealthResponse, error) {
    return &HealthResponse{Status: "ok"}, nil
}

3. Access Auth Data in Endpoints

package user

import (
    "context"
    "encore.dev/beta/auth"
    myauth "myapp/auth"  // Import your auth package
)

//encore:api auth method=GET path=/profile
func GetProfile(ctx context.Context) (*Profile, error) {
    // Get the user ID
    userID, ok := auth.UserID()
    if !ok {
        // Should not happen with auth endpoint
    }
    
    // Get full auth data
    data := auth.Data().(*myauth.AuthData)
    
    return &Profile{
        UserID: string(userID),
        Email:  data.Email,
        Role:   data.Role,
    }, nil
}

Auth Handler Signature

The auth handler must:

  1. Have the //encore:authhandler annotation
  2. Accept context.Context and a params struct pointer
  3. Return (auth.UID, *YourAuthData, error)
//encore:authhandler
func MyAuthHandler(ctx context.Context, params *Params) (auth.UID, *AuthData, error)

Auth Handler Behavior

Scenario Returns Result
Valid credentials (uid, data, nil) Request authenticated
Invalid credentials ("", nil, err) with errs.Unauthenticated 401 response
Other error ("", nil, err) Request aborted

Common Auth Patterns

JWT Token Validation

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
}

API Key Authentication

//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
}

Cookie-Based Auth

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
}

Multi-Source Auth (Cookie + Header + Query)

Auth params can extract data from multiple sources:

import "net/http"

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

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

    // Fall back to Authorization header
    if params.Authorization != "" {
        return authenticateWithToken(ctx, params.Authorization)
    }

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

Service-to-Service Auth

Auth data automatically propagates in internal service calls:

package order

import (
    "context"
    "myapp/user"  // Import the user service
)

//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
    }
    
    // Auth is automatically propagated to this call
    profile, err := user.GetProfile(ctx)
    if err != nil {
        return nil, err
    }
    
    return &OrderWithUser{Order: order, User: profile}, nil
}

Testing with Auth

Override auth data in tests using auth.WithContext:

package user_test

import (
    "context"
    "testing"

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

func TestGetProfile(t *testing.T) {
    // Create a context with auth data
    ctx := auth.WithContext(
        context.Background(),
        auth.UID("test-user-123"),
        &myauth.AuthData{
            UserID: "test-user-123",
            Email:  "test@example.com",
            Role:   "user",
        },
    )

    // Call the endpoint with the authenticated context
    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)
    }
}

Guidelines

  • Only one //encore:authhandler per application
  • Return auth.UID as the first return value (user identifier)
  • Return your custom AuthData struct as second value
  • Use auth.UserID() to get the authenticated user ID
  • Use auth.Data() and type assert to get full auth data
  • Auth propagates automatically in service-to-service calls
  • Use auth.WithContext() to override auth in tests
  • Keep auth handlers fast - they run on every authenticated request

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at cb69bb1. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 5 months ago
Other metadata
when_to_use
User wants to require login on a Go endpoint, restrict an endpoint to authenticated/signed-in users, validate a bearer token / JWT / API key from an `Authorization` header, read the current user inside a handler (`auth.UserID()` / `auth.Data()`), define an `auth.AuthHandler`, return `errs.Unauthenticated` from a handler, or use `//encore:api auth` on a handler. Trigger phrases: "protect this endpoint", "only authenticated users", "require login", "Authorization header", "bearer token", "401", "403", "who is calling", "current user".
  • Go
  • encore
  • authentication
  • authorization
  • jwt
  • api-key
  • bearer-token
  • session
  • endpoint-protection

README badge

README badge for encoredev/skills/go-auth

Adds authentication and authorization to Encore Go endpoints using auth handlers, bearer tokens, and the `//encore:api auth` annotation. Covers token validation, API key authentication, cookie-based sessions, and reading the current user with `auth.UserID()` and `auth.Data()`.

Generated from the current SKILL.md.

Does this work with JWT tokens, API keys, and cookies?
Yes. The skill covers all three patterns. The auth handler extracts credentials from the Authorization header, cookies, or query parameters, and can validate JWTs, API keys, or session tokens.
How do I access the authenticated user inside a handler?
Use `auth.UserID()` to get the user ID, or `auth.Data()` followed by a type assertion to access your custom AuthData struct (e.g., email, role).
Can auth data propagate between Encore services?
Yes. Auth data automatically propagates in internal service-to-service calls, so you do not need to manually pass credentials between services.
How do I test endpoints that require authentication?
Use `auth.WithContext()` to inject auth data into the context before calling the endpoint in tests.
Can I extract credentials from multiple sources (header, cookie, query)?
Yes. Define multiple fields in your AuthParams struct with different struct tags (header, cookie, query) and check them in priority order inside the auth handler.

Generated from the current SKILL.md. These answers refresh after source changes.