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

305 lines
6.8 KiB
Markdown

# 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
```env
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.
```go
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.
```go
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.
```go
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:
```go
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
```go
// 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:**
```go
type OrganizationResolver interface {
ResolveByProviderID(ctx context.Context, providerID string) (int32, error)
}
```
**2. Domain modules implement via adapters:**
```go
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)
```go
router.GET("/health", handler.Health)
```
### Authenticated Route
```go
apiGroup := router.Group("/api")
apiGroup.Use(authMiddleware.RequireAuth())
apiGroup.Use(authMiddleware.RequireOrganization())
{
apiGroup.GET("/profile", handler.GetProfile)
}
```
### Permission-Protected Route
```go
apiGroup.POST("/resources",
auth.RequirePermissionFunc("resource", "create"),
handler.CreateResource)
apiGroup.DELETE("/resources/:id",
auth.RequirePermissionFunc("resource", "delete"),
handler.DeleteResource)
```
### Role-Protected Route
```go
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`:
```go
const PermResourceView = Permission("resource:view")
const PermResourceCreate = Permission("resource:create")
```
**2. Assign to roles** in `src/pkg/auth/rbac.go`:
```go
{
RoleMember: {
PermResourceView,
// ... other permissions
},
RoleManager: {
PermResourceView,
PermResourceCreate,
// ... other permissions
},
}
```
**3. Protect routes**:
```go
router.POST("/resources",
auth.RequirePermissionFunc("resource", "create"),
handler.CreateResource)
```
## Common Patterns
### Check Organization Ownership
```go
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
```go
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](./database.md)
- **Building APIs**: See [API Development Guide](./api-development.md)
- **Stytch documentation**: https://stytch.com/docs/b2b