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.Identityin 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.RequestContextwith 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 accessRoleManager- Organization managementRoleMember- Standard user access
Permissions
Format: "{resource}:{action}"
Common permissions:
resource:view- Read accessresource:create- Create new itemsresource:update- Modify existing itemsresource:delete- Delete itemsorg: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
- Database operations: See Database Guide
- Building APIs: See API Development Guide
- Stytch documentation: https://stytch.com/docs/b2b