Encore Go provides a built-in authentication system using the //encore:authhandler annotation.
The examples below are reference fragments. Some omit standard library imports or return statements, so adapt them to your project and review and test them before relying on them.
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
}
In real use, check that the header starts with the Bearer prefix and that the remaining token is not empty, and return errs.Unauthenticated otherwise. Give your own auth service package a different name, or alias the imported Encore auth package, so the two package names stay distinct.
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
}
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
}
When the user ID is missing, return errs.Unauthenticated right away and stop handling the request. Use a checked type assertion on auth.Data() and return an error when it fails, so a missing or unexpected value results in an error response instead of a panic.
//encore:api auth only authenticates the caller. It enforces neither roles nor ownership. Inside the handler, read the auth data with a checked type assertion, compare the Role field with the role the endpoint requires, and return a permission denied error when it does not match. Apply the same idea to individual records: confirm that a record fetched by a caller supplied ID belongs to the calling user from auth.UserID() before returning it, or limit the query to records owned by that user.
The auth handler must:
- Have the
//encore:authhandler annotation
- Accept
context.Context and a params struct pointer
- Return
(auth.UID, *YourAuthData, error)
//encore:authhandler
func MyAuthHandler(ctx context.Context, params *Params) (auth.UID, *AuthData, error)
| 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 |
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
}
In real use, restrict the accepted signing method to the single HMAC method you issue and confirm the token uses that method before returning the key. Require an expiry claim, and check issuer and audience using the parser options of the JWT library. The key function should return an error when the secret is empty, and the secret should be long, strong and random.
//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
}
In real use, parse a defined scheme prefix from the header and reject an empty key with errs.Unauthenticated. Store only a hash of each API key, look users up by the hash of the presented key, and rotate keys periodically. Return errs.Unauthenticated only when no matching row exists, and return an internal error for any other database failure.
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
}
Set session cookies with http only, secure, and same site settings of lax or strict. Protect state changing endpoints that use cookie authentication with cross site request forgery defenses such as tokens or origin checks. Treat session lookup failures separately from expiry, and return an internal error when the lookup itself fails.
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
}
//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",
}
}
Keep tokens and secrets in headers or cookies, because values in the query string leak through logs, referrers and browser history. When both a cookie and an Authorization header are present, consider preferring the header, and apply the cookie guidance above to the cookie path.
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
}
This example leaves out an ownership check. Before returning an order fetched by a caller supplied ID, confirm that the order belongs to the caller from auth.UserID(), and return a not found or permission denied error when it does not. Each service must enforce its own authorization on the propagated identity and perform privileged actions only after explicit checks.
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)
}
}
- 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
- Each service still enforces its own role and ownership checks on the propagated identity
- Authentication alone does not authorize; check roles and record ownership inside handlers
- Use
auth.WithContext() to override auth in tests
- Keep auth handlers fast - they run on every authenticated request