使用Golang的Encore.Dev框架实现Auth0与VueJS的AuthHandler

使用Golang的Encore.Dev框架实现Auth0与VueJS的AuthHandler TL;DR:文档表明,在 Encore 中,JWT 验证对于身份验证来说已经足够,但在实现 RBAC 时似乎存在冲突,因为 Auth0 要求返回一个处理程序,而 Encore 似乎无法与之配合。

我卡在这个问题上大约两天了。根据文档,从技术上讲,所有需要做的就是 JWT 验证,这包括解码;如果能够解码,则验证通过,否则验证失败:为 API 添加身份验证以验证用户 — Encore 文档。然而,对我来说,问题在于文档指出:

Encore applications can designate a special function to handle authentication, by defining a function and annotating it with //encore:authhandler. This annotation tells Encore to run the function whenever an incoming API call contains an authentication token.

The auth handler is responsible for validating the incoming authentication token and returning an auth.UID (a string type representing a user id). The auth.UID can be whatever you wish, but in practice it usually maps directly to the primary key stored in a user table (either defined in the Encore service or in an external service like Firebase or Okta).

这意味着我不能返回一个处理程序。而根据 Auth0 的文档,在实现 RBAC 时,它必须返回一个处理程序。有人有什么想法吗?以下是根据 Auth0 文档编写的有效代码:

package middleware
 
import (
    "context"
    "log"
    "net/http"
    "net/url"
    "time"
 
    "encore.app/auth/helpers"
    jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
    "github.com/auth0/go-jwt-middleware/v2/jwks"
    "github.com/auth0/go-jwt-middleware/v2/validator"
    "github.com/pkg/errors"
)
 
// Declare constants for error messages
const (
    missingJWTErrorMessage       = "Requires authentication"
    invalidJWTErrorMessage       = "Bad credentials"
    permissionDeniedErrorMessage = "Permission denied"
)
 
// CustomClaims is a struct representing additional permissions in the JWT
type CustomClaims struct {
    Permissions []string `json:"permissions"`
}
 
// Validate is a method for validating custom claims (currently a no-op)
func (c CustomClaims) Validate(ctx context.Context) error {
    return nil
}
 
// HasPermissions is a method for checking if the custom claims contain all expected permissions
func (c CustomClaims) HasPermissions(expectedClaims []string) bool {
    if len(expectedClaims) == 0 {
        return false
    }
    for _, scope := range expectedClaims {
        if !helpers.Contains(c.Permissions, scope) {
            return false
        }
    }
    return true
}
 
// ValidatePermissions is a middleware function for checking if the JWT has the expected permissions
func ValidatePermissions(expectedClaims []string, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
        claims := token.CustomClaims.(*CustomClaims)
        if !claims.HasPermissions(expectedClaims) {
            errorMessage := ErrorMessage{Message: permissionDeniedErrorMessage}
            helpers.WriteJSON(w, http.StatusForbidden, errorMessage)
            return
        }
        next.ServeHTTP(w, r)
    })
}
 
// ValidateJWT is a factory function that returns a middleware for validating JWTs with a given audience and domain
func ValidateJWT(audience, domain string) func(next http.Handler) http.Handler {
    issuerURL, err := url.Parse("https://" + domain + "/")
    if err != nil {
        log.Fatalf("Failed to parse the issuer url: %v", err)
    }
 
    provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
 
    //Setting up validator
    jwtValidator, err := validator.New(
        provider.KeyFunc,
        validator.RS256,
        issuerURL.String(),
        []string{audience},
        validator.WithCustomClaims(
            func() validator.CustomClaims {
                return &CustomClaims{}
            },
        ),
    )
    if err != nil {
        log.Fatalf("Failed to set up the jwt validator")
    }
 
    errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
        log.Printf("Encountered error while validating JWT: %v", err)
        if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
            errorMessage := ErrorMessage{Message: missingJWTErrorMessage}
            helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage)
            return
        }
        if errors.Is(err, jwtmiddleware.ErrJWTInvalid) {
            errorMessage := ErrorMessage{Message: invalidJWTErrorMessage}
            helpers.WriteJSON(w, http.StatusUnauthorized, errorMessage)
            return
        }
        ServerError(w, err)
    }
 
    middleware := jwtmiddleware.New(
        jwtValidator.ValidateToken,
        jwtmiddleware.WithErrorHandler(errorHandler),
    )
 
    return func(next http.Handler) http.Handler {
        return middleware.CheckJWT(next)
    }
}

更多关于使用Golang的Encore.Dev框架实现Auth0与VueJS的AuthHandler的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

嘿 Colton,提供的示例代码使用了 net/http 中间件来进行 JWT 验证,这与 Encore 的认证处理程序并非一一对应。幸运的是,该中间件只是普通 net/http 服务的一个便捷方法;使用底层的验证函数同样简单(甚至可以说更简单),它应该能与 Encore 很好地配合。

从你提供的代码片段来看,核心验证逻辑发生在 jwtValidator.ValidateToken 中,这是传递给 jwtmiddleware.New 的验证函数。对于 Encore 的情况,你可以直接从 Encore 认证处理程序中调用该函数,大致如下(粗略示例):

import (
    "encore.dev/beta/auth"
    "github.com/auth0/go-jwt-middleware/v2/validator"
)

//encore:authhandler
func AuthHandler(ctx context.Context, token string) (auth.UID, error) {
    validatedToken, err := jwtValidator.ValidateToken(ctx, token)
    if err != nil {
        return "", err
    }
    claims := validatedToken.(*validator.ValidatedClaims)

    // ... 检查你关心的权限声明 ....

    // 最后将主题作为 Encore 用户 ID 返回
    return auth.UID(claims.RegisteredClaims.Subject), nil
}

var jwtValidator *validator.Validator

func init() {
    // 按照提供的示例代码中的方式设置 jwtValidator,调用 validator.New
}

希望这能帮到你。如果无法使其正常工作,请告诉我,我很乐意进行一次视频通话,一起探讨!

更多关于使用Golang的Encore.Dev框架实现Auth0与VueJS的AuthHandler的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Encore中实现Auth0集成时,确实需要理解其authhandler模式与标准HTTP中间件的区别。Encore的认证处理程序返回的是auth.UID,而不是HTTP处理程序。以下是针对你问题的解决方案:

核心解决方案

Encore的authhandler应该专注于JWT验证并返回用户标识,而RBAC权限检查应该在API端点内部处理。

1. 创建Encore认证处理器

package auth

import (
    "context"
    "log"
    "net/url"
    "time"

    "encore.dev/beta/auth"
    "github.com/auth0/go-jwt-middleware/v2/jwks"
    "github.com/auth0/go-jwt-middleware/v2/validator"
)

type CustomClaims struct {
    Permissions []string `json:"permissions"`
    Subject     string   `json:"sub"`
}

func (c CustomClaims) Validate(ctx context.Context) error {
    return nil
}

//encore:authhandler
func AuthHandler(ctx context.Context, token string) (auth.UID, *CustomClaims, error) {
    // Auth0配置
    audience := "your-api-audience"
    domain := "your-auth0-domain"
    
    issuerURL, err := url.Parse("https://" + domain + "/")
    if err != nil {
        return "", nil, err
    }

    provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)

    jwtValidator, err := validator.New(
        provider.KeyFunc,
        validator.RS256,
        issuerURL.String(),
        []string{audience},
        validator.WithCustomClaims(
            func() validator.CustomClaims {
                return &CustomClaims{}
            },
        ),
    )
    if err != nil {
        return "", nil, err
    }

    // 验证JWT令牌
    validatedClaims, err := jwtValidator.ValidateToken(ctx, token)
    if err != nil {
        return "", nil, err
    }

    claims := validatedClaims.(*validator.ValidatedClaims)
    customClaims := claims.CustomClaims.(*CustomClaims)
    
    // 使用Auth0的sub作为UID
    return auth.UID(customClaims.Subject), customClaims, nil
}

2. 创建权限检查工具函数

package auth

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

// HasPermission 检查用户是否拥有指定权限
func HasPermission(ctx context.Context, requiredPermission string) error {
    claims, ok := auth.Data().(*CustomClaims)
    if !ok || claims == nil {
        return &errs.Error{
            Code:    errs.PermissionDenied,
            Message: "No permission claims found",
        }
    }

    for _, perm := range claims.Permissions {
        if perm == requiredPermission {
            return nil
        }
    }

    return &errs.Error{
        Code:    errs.PermissionDenied,
        Message: "Insufficient permissions",
    }
}

// HasAllPermissions 检查用户是否拥有所有指定权限
func HasAllPermissions(ctx context.Context, requiredPermissions []string) error {
    claims, ok := auth.Data().(*CustomClaims)
    if !ok || claims == nil {
        return &errs.Error{
            Code:    errs.PermissionDenied,
            Message: "No permission claims found",
        }
    }

    permSet := make(map[string]bool)
    for _, perm := range claims.Permissions {
        permSet[perm] = true
    }

    for _, required := range requiredPermissions {
        if !permSet[required] {
            return &errs.Error{
                Code:    errs.PermissionDenied,
                Message: "Missing required permission: " + required,
            }
        }
    }

    return nil
}

3. 在API端点中使用RBAC

package api

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

//encore:api auth method=GET path=/admin
func GetAdminData(ctx context.Context) (*Response, error) {
    // 检查管理员权限
    if err := auth.HasPermission(ctx, "admin:read"); err != nil {
        return nil, err
    }
    
    // 获取当前用户ID
    userID := auth.UserID()
    
    // 业务逻辑
    return &Response{Data: "Admin data for " + string(userID)}, nil
}

//encore:api auth method=POST path=/users
func CreateUser(ctx context.Context, req *CreateUserRequest) (*Response, error) {
    // 检查多个权限
    requiredPerms := []string{"users:write", "users:create"}
    if err := auth.HasAllPermissions(ctx, requiredPerms); err != nil {
        return nil, err
    }
    
    // 业务逻辑
    return &Response{Data: "User created"}, nil
}

4. VueJS前端集成示例

// auth.js - VueJS前端
import { Auth0Client } from '@auth0/auth0-spa-js';

const auth0 = new Auth0Client({
  domain: 'your-auth0-domain',
  clientId: 'your-client-id',
  authorizationParams: {
    audience: 'your-api-audience',
    redirect_uri: window.location.origin
  }
});

// 获取访问令牌
async function getAccessToken() {
  const token = await auth0.getTokenSilently();
  return token;
}

// API调用示例
async function callEncoreAPI(endpoint, method = 'GET', data = null) {
  const token = await getAccessToken();
  
  const response = await fetch(`https://your-encore-app.encoreapi.com${endpoint}`, {
    method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: data ? JSON.stringify(data) : null
  });
  
  return response.json();
}

// 使用示例
async function fetchAdminData() {
  try {
    const data = await callEncoreAPI('/admin');
    console.log('Admin data:', data);
  } catch (error) {
    console.error('Permission denied:', error);
  }
}

5. 配置Encore认证中间件

// encore.app配置
package config

import "encore.dev/config"

var Auth0 = struct {
    Domain   string
    Audience string
}{
    Domain:   config.String("AUTH0_DOMAIN"),
    Audience: config.String("AUTH0_AUDIENCE"),
}

关键点:

  1. Encore的authhandler只负责验证JWT并返回auth.UID
  2. RBAC权限检查在API端点内部通过工具函数实现
  3. auth.Data()用于获取认证时返回的自定义声明
  4. 前端使用标准Bearer令牌发送请求

这种模式保持了Encore的声明式API设计,同时实现了Auth0的RBAC功能。

回到顶部