web-production-saas-starter/go-b2b-starter/docs/authentication.md
2025-12-16 18:54:41 +04:00

6.8 KiB

Authentication Guide

The authentication system uses Stytch B2B for identity management with JWT verification, RBAC, and multi-tenant organization context.

Architecture

Provider: Stytch B2B handles user authentication and sessions Middleware: Verifies JWTs and resolves organization/account context RBAC: Role-based access control with permissions Resolvers: Bridge auth provider IDs to database IDs

JWT Verification

The system uses a two-tier verification strategy:

1. Fast Path - Verify JWT locally using cached public keys 2. API Fallback - Call Stytch API if local verification fails

This approach balances security with performance.

Configuration

STYTCH_PROJECT_ID=project-test-xxx
STYTCH_SECRET=secret-test-xxx
STYTCH_ENV=test  # or "live"

Middleware

Three middleware functions protect routes:

RequireAuth

Verifies JWT and extracts identity.

router.Use(authMiddleware.RequireAuth())

What it does:

  • Verifies JWT from Authorization: Bearer {token} header
  • Extracts user identity (email, roles, permissions)
  • Stores auth.Identity in request context
  • Returns 401 if auth fails

RequireOrganization

Resolves organization and account IDs from auth provider.

router.Use(authMiddleware.RequireOrganization())

What it does:

  • Gets organization ID from Stytch → resolves to database ID
  • Gets user email → resolves to account ID
  • Stores auth.RequestContext with IDs
  • Returns 401 if resolution fails

Note: Always use after RequireAuth().

RequirePermission

Checks user has specific permission.

router.POST("/resources",
    auth.RequirePermissionFunc("resource", "create"),
    handler.CreateResource)

What it does:

  • Checks if user has permission (e.g., "resource:create")
  • Returns 403 if permission missing

Note: Use after RequireOrganization().

Using Context in Handlers

Access authentication info from request context:

func (h *Handler) MyHandler(c *gin.Context) {
    // Get full context
    reqCtx := auth.GetRequestContext(c)
    orgID := reqCtx.OrganizationID    // int32
    accountID := reqCtx.AccountID      // int32
    email := reqCtx.Identity.Email     // string

    // Or use convenience functions
    orgID := auth.GetOrganizationID(c)
    accountID := auth.GetAccountID(c)
}

RBAC System

Roles

Defined in src/pkg/auth/roles.go:

  • RoleAdmin - Full system access
  • RoleManager - Organization management
  • RoleMember - Standard user access

Permissions

Format: "{resource}:{action}"

Common permissions:

  • resource:view - Read access
  • resource:create - Create new items
  • resource:update - Modify existing items
  • resource:delete - Delete items
  • org:manage - Organization administration

Defined in src/pkg/auth/permissions.go.

Permission Checks

// In middleware (route-level)
router.POST("/resources",
    auth.RequirePermissionFunc("resource", "create"),
    handler.CreateResource)

// In code (programmatic)
if !auth.HasPermission(identity, "resource:delete") {
    return errors.New("permission denied")
}

Resolver Pattern

Resolvers convert auth provider IDs to database IDs.

Why Needed?

  • Stytch uses string UUIDs for organizations
  • Database uses int32 for primary keys
  • Auth package can't depend on domain modules (circular dependency)

How It Works

1. Auth package defines interfaces:

type OrganizationResolver interface {
    ResolveByProviderID(ctx context.Context, providerID string) (int32, error)
}

2. Domain modules implement via adapters:

type orgResolverAdapter struct {
    repo domain.OrganizationRepository
}

func (a *orgResolverAdapter) ResolveByProviderID(ctx context.Context, id string) (int32, error) {
    org, err := a.repo.GetByStytchID(ctx, id)
    if err != nil {
        return 0, err
    }
    return org.ID, nil
}

3. Wired in initialization:

Resolvers registered in src/main/cmd/init_mods.go after organization module loads.

Route Protection Patterns

Public Route (No Auth)

router.GET("/health", handler.Health)

Authenticated Route

apiGroup := router.Group("/api")
apiGroup.Use(authMiddleware.RequireAuth())
apiGroup.Use(authMiddleware.RequireOrganization())
{
    apiGroup.GET("/profile", handler.GetProfile)
}

Permission-Protected Route

apiGroup.POST("/resources",
    auth.RequirePermissionFunc("resource", "create"),
    handler.CreateResource)

apiGroup.DELETE("/resources/:id",
    auth.RequirePermissionFunc("resource", "delete"),
    handler.DeleteResource)

Role-Protected Route

adminGroup := router.Group("/admin")
adminGroup.Use(authMiddleware.RequireRole(auth.RoleAdmin))
{
    adminGroup.GET("/users", handler.ListUsers)
}

Adding New Permissions

1. Define permission constant in src/pkg/auth/permissions.go:

const PermResourceView = Permission("resource:view")
const PermResourceCreate = Permission("resource:create")

2. Assign to roles in src/pkg/auth/rbac.go:

{
    RoleMember: {
        PermResourceView,
        // ... other permissions
    },
    RoleManager: {
        PermResourceView,
        PermResourceCreate,
        // ... other permissions
    },
}

3. Protect routes:

router.POST("/resources",
    auth.RequirePermissionFunc("resource", "create"),
    handler.CreateResource)

Common Patterns

Check Organization Ownership

func (h *Handler) GetResource(c *gin.Context) {
    orgID := auth.GetOrganizationID(c)
    resourceID := parseID(c.Param("id"))

    resource, err := h.service.GetResource(c.Request.Context(), resourceID)
    if err != nil {
        c.JSON(500, gin.H{"error": "failed to get resource"})
        return
    }

    // Verify resource belongs to user's organization
    if resource.OrganizationID != orgID {
        c.JSON(403, gin.H{"error": "access denied"})
        return
    }

    c.JSON(200, resource)
}

Optional Authentication

func (h *Handler) PublicResource(c *gin.Context) {
    // Try to get org ID (may be 0 if not authenticated)
    orgID := auth.GetOrganizationID(c)

    if orgID != 0 {
        // User is authenticated, show personalized data
    } else {
        // User is not authenticated, show public data
    }
}

File Locations

Component Path
Auth provider interface src/pkg/auth/auth.go
Middleware src/pkg/auth/middleware.go
Context helpers src/pkg/auth/context.go
RBAC definitions src/pkg/auth/rbac.go
Roles src/pkg/auth/roles.go
Permissions src/pkg/auth/permissions.go
Resolvers src/pkg/auth/resolvers.go
Stytch adapter src/pkg/auth/adapters/stytch/

Next Steps