Authentication and Authorization
Overview
The Auth domain provides the central authorization and authentication layer for Comby. It implements middleware for command and query authorization, manages permission contexts, and maintains a readmodel that aggregates authentication and authorization data from multiple domains.
In the comby framework, the Facade itself does not handle any authorization logic. Instead, authorization is managed by comby's defaults, which users can register to.
TIP
Further information about Defaults can be found in the documentation: Defaults
Concept
What is the Auth Domain?
The Auth domain is a cross-cutting concern that validates whether users have permission to execute commands and queries. Unlike other domains, it doesn't manage its own aggregates but instead consumes events from Account, Tenant, Identity, Workspace, and other domains to build a comprehensive authorization context.
Key Characteristics:
- Middleware-Based: Provides authorization middleware for commands and queries
- Event Consumer: Listens to events from multiple domains to build auth context
- Multi-Layer Authorization: System, RBAC, and custom permission functions
- Workspace-Aware: Supports both tenant-level and workspace-level authorization
- Additive Permission Model: Grants access if user has EITHER tenant OR workspace permission
Auth Domain
├── Middleware (Authorization Layer)
├── Readmodel (Authorization Context)
│ ├── Accounts & Sessions
│ ├── Tenants & Identities
│ ├── Groups & Permissions
│ └── Workspaces & Members
└── Authorization Flow
├── System-Level Checks
├── Custom Permission Functions
├── RBAC (Role-Based Access Control)
└── Workspace AuthorizationArchitecture
Hierarchy
Auth Domain (Cross-Cutting)
├── Authorization Middleware
│ ├── Command Handler Middleware
│ └── Query Handler Middleware
└── Authorization Readmodel
├── Account Context (Sessions, Credentials)
├── Tenant Context (Tenants, Identities, Groups)
├── Workspace Context (Workspaces, Members, Groups)
└── Permission Context (Tenant & Workspace Permissions)Location in Codebase
Domain:
/domain/auth/middleware.go- Authorization middleware for commands and queriesreadmodel/- Authorization context readmodelhandle.account.go- Account event handlershandle.tenant.go- Tenant event handlershandle.identity.go- Identity event handlershandle.workspace.go- Workspace event handlershandle.group.go- Group event handlersmodel.go- Authorization context models
Note: Auth domain has NO commands, queries, or API endpoints - it's purely middleware
Related Entities
Auth domain consumes events from:
- Account: Session creation/logout, credential updates
- Tenant: Tenant creation, group management
- Identity: Identity creation, group assignments, token management
- Workspace: Workspace creation, member/group management
- All Aggregates: Tracks aggregate-tenant and aggregate-workspace associations
Authorization Context Models
type IdentityCtxModel struct {
TenantUuid string
IdentityUuid string
AccountUuid string
Groups []*GroupCtxModel
}
type GroupCtxModel struct {
GroupUuid string
Name string
PermissionNames []string
Permissions []*PermissionModel
}
type WorkspaceCtxModel struct {
TenantUuid string
WorkspaceUuid string
Name string
Groups []*WorkspaceGroupCtxModel
Members []*WorkspaceMemberCtxModel
MemberWorkspaces []*WorkspaceMemberWorkspaceCtxModel // Transitive membership
}
type WorkspaceMemberWorkspaceCtxModel struct {
WorkspaceUuid string // The workspace that is a member
GroupUuids []string // Groups assigned to this workspace member
}
type SessionCtxModel struct {
AccountUuid string
SessionUuid string
SessionKey string // hashed
ExpiredAt int64
}Authorization Flow
The defaults ensures proper authorization through 5 middlewares:
AuthAnonymousCtx— which sets the anonymous sender context by defaultAuthCookiesCtx— which handlesauthenticationthrough theCookieheader.AuthAuthorizationCtx— which handlesauthenticationthrough theAuthorizationheader.AuthTargetCtx- which augments request with target information.AuthCommandHandlerFunc(andAuthQueryHandlerFunc) — which validates user permissions and enforces access control within theFacade.
The Facade simply executes Commands and Queries, relying on these middleware layers to enforce security.
When a user makes an HTTP request, the request is processed in multiple stages by the above middleware components. These components work together to ensure that the request is authenticated and finally authorized based on the user's permissions.
Flow Overview
The user sends an HTTP request to the REST API, which triggers the following flow:
- Authentication:
- The request is first processed by the
apimiddleware, which includes theAuthAnonymousCtx,AuthCookiesCtx,AuthAuthorizationCtx, andAuthTargetCtxmiddleware. - The
AuthAnonymousCtxmiddleware sets the default anonymous sender context. - The
AuthCookiesCtxmiddleware handles authentication through theCookieheader. - The
AuthAuthorizationCtxmiddleware handles authentication through theAuthorizationheader. - The
AuthTargetCtxmiddleware augments the request with target information (which can be adapted to user's requirements with differentaggregateUuidfield name)
- The request is first processed by the
- Authorization:
- The request is then dispatched to the
Facade, where theAuthmiddleware enforces access control based on the user's permissions.
- The request is then dispatched to the
Stage 1: Authentication
Anonymous Context
The AuthAnonymousCtx middleware sets the default anonymous sender context in the request context. This context is used when no authentication credentials are provided in the request. The middleware begins by setting default values in the context to represent an anonymous user:
sessionUuid: Default toANONYMOUS_SESSION_UUIDaccountUuid: Default toANONYMOUS_ACCOUNT_UUIDidentityUuid: Default toANONYMOUS_IDENTITY_UUIDtenantUuid: Default toANONYMOUS_TENANT_UUID
Cookies Context
The middleware retrieves the Cookie header from the request. If the header is present and the AccountCtxReadmodel (rm) is provided, it attempts to authenticate the user. These fields are extracted from the Cookie header:
- Cookie Format: The Cookie Header is defined following:
Cookie: session=<sessionUuid|sessionKey>- HttpOnly and Secure (User Login)Cookie: session=<sessionUuid|sessionKey>, identity=<tenantUuid|identityUuid>- Secure (Default Usage after Login and Identity Selection)
The middleware sets the sessionUuid and accountUuid in the request context if a valid session is found. If an identityUuid is also present in the Cookie header and belongs to the account, the request context is further updated with the identityUuid and tenantUuid.
Authorization Header
The AuthAuthorizationCtx middleware handles the extraction and validation of authentication credentials from the HTTP request's Authorization header. The Authorization Header is defined following:
- Authorization Format:
Authorization: Bearer sa=<tokenUuid|tokenKey>- Service Account connected with specific identity
The middleware sets the sessionUuid and accountUuid in the request context if a valid session is found. If an identityUuid is also present in the Authorization header and belongs to the account, the request context is further updated with the identityUuid and tenantUuid. In addition, the middleware also handles service account token authentication, which is the primary use case for this middleware.
Note: User Account authentication should be handled by the AuthCookiesCtx middleware (using Cookies) and Service Account authentication by the AuthAuthorizationCtx middleware (using Authorization).
Final Context
After the AuthAnonymousCtx, AuthCookiesCtx, and AuthAuthorizationCtx middlewares have processed the request, the request context is updated with the following fields:
sessionUuid: The session UUID extracted from the request'sCookieorAuthorizationheader.accountUuid: The account UUID extracted from the request'sCookieorAuthorizationheader.identityUuid: The identity UUID extracted from the request'sCookieorAuthorizationheader.tenantUuid: The tenant UUID extracted from the request'sCookieorAuthorizationheader. Next, the request proceeds to theAuthTargetCtxmiddleware for context augmentation.
Stage 2: Context Augmentation with target.ctx.go
The AuthTargetCtx middleware is responsible for extracting and adding target-specific information, such as tenantUuid and aggregateUuid, from the request to the context. This information is used later in the authorization phase.
Normally targetTenantUuid is the same as the senderTenantUuid, but there are cases where a privileged user needs this distinction, for example to create new tenants. Here the acting tenant can be A but the destinated new tenant B.
The aggregatUuid has a different purpose: it is used to define which target aggregate should be finally processed. The next middleware checks whether the target aggregate exists within the target tenant and not an aggregate from another tenant. This always ensures that even manipulated requests (eg requesting with tenant A, but deleting aggregate B which lives in tenant C) are checked for accuracy.
- Field Name Customization:
By default, the values tenantUuid and aggregateUuid are extracted using the field names (tenantUuid and aggregateUuid). These field names can be customized using options such as:
AuthTargetCtxWithTenantFieldName- Overwrites the defaulttenantUuidfield name identifier.AuthTargetCtxWithAggregateFieldName- Overwrites the defaultaggregateUuidfield name identifier.
When a user defines a URL endpoint, such as: /myTenants/{myTenantUuid}/customAggregates/{customAggregateUuid} the user can explicitly specify tenant and aggregate field names using:
AuthTargetCtxWithTenantFieldName("myTenantUuid")AuthTargetCtxWithAggregateFieldName("customAggregateUuid")
This precise definition is crucial for proper authorization of the request, ensuring that cross-tenant requests are reliably blocked and that only aggregates from the user's own tenant are accessed. Alternatively user can also use RequestContext directly to set the target tenantUuid and aggregateUuid values manually.
- Extracting Tenant and Aggregate UUIDs:
The tenantUuid and aggregateUuid are extracted from the request's path based on the field names and added to the request context if valid.
- Proceed to Next Middleware:
After augmenting the context with the target-specific data, the request proceeds to the next middleware or handler.
Stage 3: Authorization with auth/middleware.go
The final stage of the authorization process is handled by the middleware defined in domain/auth/middleware.go. This middleware evaluates whether the user has the necessary permissions to execute a command or query based on various authorization checks, including custom permission functions and RBAC.
AuthCommandHandlerFunc wraps a command handler with authorization logic. Same for AuthQueryHandlerFunc for queries. The middleware ensures that commands (or queries) are only executed if the user has the required permissions based on system-level checks, RBAC (role-based access control), and custom permission functions.
Multi-Layer Authorization
The Auth domain implements a 3-layer authorization model:
1. System-Level Authorization
Skip authorization if:
- ExecuteSkipAuthorization flag is set
- Used for internal system operations2. Custom Permission Functions
Check custom permission functions for specific commands:
- Domain-specific authorization logic
- Custom validation rules
- If custom function exists and approves → Allow3. RBAC (Role-Based Access Control)
a. System Admin Check:
- Is user in SYSTEM_TENANT_GROUP_ADMIN? → Allow ALL
b. Cross-Tenant Check:
- Is SenderTenant == TargetTenant? → Continue
- Otherwise → Deny
c. Workspace Authorization (if TargetWorkspaceUuid set):
- Is user a workspace member (direct OR transitive)? → Continue
- Does aggregate belong to workspace? → Continue
- Does user have permission? (ADDITIVE)
→ Tenant-level permission OR
→ Workspace-level permission (direct) OR
→ Workspace-level permission (transitive via workspace membership)
d. Tenant Authorization (no workspace context):
- Does aggregate belong to tenant? → Continue
- Does user have tenant-level permission? → AllowAdditive Permission Model
For workspace-scoped operations:
Access Granted = (Tenant Permission) OR (Workspace Permission)Examples:
| Scenario | Tenant Permission | Workspace Member | Workspace Permission | Result |
|---|---|---|---|---|
| Tenant Admin | ✅ customer.create | ❌ No | - | ✅ Allowed |
| Workspace Member | ❌ No | ✅ Yes | ✅ customer.create | ✅ Allowed |
| Non-Member with Tenant Permission | ✅ customer.create | ❌ No | - | ❌ Denied (not member) |
| Member without Permission | ❌ No | ✅ Yes | ❌ No | ❌ Denied |
Authorization Process Details
- Request Context Extraction:
- The request context (
reqCtx) is extracted from the command usingcmd.GetReqCtx(). This context contains crucial information like the sender's identity, tenant, and target aggregates for permission validation.
- System Layer Authorization:
- Checks whether authorization should be skipped by evaluating
reqCtx.Attributes.Get(auth.ExecuteSkipAuthorization). If this is true, the command is allowed to proceed.
- RBAC Layer Authorization:
If system layer authorization doesn't allow the command, the RBAC layer is evaluated. The process involves two major checks:
Custom Permission Evaluation:
- It searches through a list of custom permissions (
runtime.RuntimePermissionList(fc)). If the command's domain and type match a permission, it runs the custom function (CmdFuncorQryFunc). If the custom function approves, the command (or query) is allowed.
- It searches through a list of custom permissions (
Identity and Group Permission Evaluation:
- The sender's identity context is fetched. The middleware checks if the sender belongs to a privileged group (like
SYSTEM_TENANT_GROUP_ADMIN_UUID), granting full access. - For other identities, RBAC ensures the identity has the necessary permissions within the target tenant and aggregate.
- If cross-tenant requests are made (excluding system-tenant), or if the target aggregate does not belong to the tenant, the request is denied.
- The sender's identity context is fetched. The middleware checks if the sender belongs to a privileged group (like
If the authorization passes, the original command handler (or query handler) is executed. Otherwise, an error
ErrPermissionDeniedis returned, indicating the command (or query) was denied by the authorization layer.
Usage
1. Register Auth Domain
import "github.com/gradientzero/comby/v2/domain/auth"
// In application initialization
auth.Register(ctx, facade)
// Add authorization middleware
facade.AddCommandHandlerMiddleware(auth.AuthCommandHandlerFunc(facade))
facade.AddQueryHandlerMiddleware(auth.AuthQueryHandlerFunc(facade))2. Skip Authorization (System Operations)
cmd, _ := comby.NewCommand("SomeDomain", &command.SomeCommand{
// command fields...
})
reqCtx := comby.NewRequestContext()
reqCtx.Attributes.Set(auth.ExecuteSkipAuthorization, true)
cmd.SetReqCtx(reqCtx)
// This command will bypass authorization checks
facade.DispatchCommand(ctx, cmd)3. Check Identity Context
// Get identity with groups and permissions
identityCtx, err := auth.GetAuthReadmodel().GetIdentity("identity-uuid")
if err != nil {
// Identity not found
}
// Check groups
for _, group := range identityCtx.Groups {
fmt.Println("Group:", group.Name)
fmt.Println("Permissions:", group.PermissionNames)
}4. Check Session
// Validate session
sessionCtx, err := auth.GetAuthReadmodel().GetSession("session-uuid", "session-key")
if err != nil {
// Session not found or invalid
}
// Check expiration
if time.Now().Unix() > sessionCtx.ExpiredAt {
// Session expired
}5. Check Workspace Membership
// Check if identity is direct workspace member
isMember := auth.GetAuthReadmodel().IsWorkspaceMember("workspace-uuid", "identity-uuid")
if !isMember {
// User is not a direct member
}
// Check if identity is member (direct OR transitive via workspace membership)
isMemberTransitive := auth.GetAuthReadmodel().IsWorkspaceMemberTransitive(
"workspace-uuid", "identity-uuid", 5) // maxDepth = 5
if !isMemberTransitive {
// User is not a member (neither direct nor transitive)
}6. Get Workspace Permissions
// Get direct permissions for identity in workspace
permissions := auth.GetAuthReadmodel().GetWorkspacePermissions("workspace-uuid", "identity-uuid")
for _, perm := range permissions {
fmt.Printf("Permission: %s.%s\n", perm.PermissionDomain, perm.PermissionType)
}
// Get all permissions including transitive (from workspace memberships)
allPermissions := auth.GetAuthReadmodel().GetWorkspacePermissionsTransitive(
"workspace-uuid", "identity-uuid", 5) // maxDepth = 5
for _, perm := range allPermissions {
fmt.Printf("Permission (incl. transitive): %s.%s\n",
perm.PermissionDomain, perm.PermissionType)
}7. Validate Aggregate Ownership
// Check if aggregate belongs to tenant
isValid := auth.GetAuthReadmodel().IsValidTenantAggregate("tenant-uuid", "aggregate-uuid")
// Check if aggregate belongs to workspace
isValid := auth.GetAuthReadmodel().IsValidWorkspaceAggregate("workspace-uuid", "aggregate-uuid")8. Register Custom Permission Function
import "github.com/gradientzero/comby/v2/domain/runtime"
// Define custom permission logic
func CustomPermissionCheck(ctx context.Context, cmd comby.Command) error {
// Custom authorization logic
domainCmd := cmd.GetDomainCmd().(*command.MyCommand)
if domainCmd.SomeField == "special-value" {
return nil // Allow
}
return runtime.ErrPermissionDenied
}
// Register custom permission
runtime.RegisterPermission(
runtime.NewPermissionCmdFunc(
"MyDomain",
"MyCommand",
CustomPermissionCheck,
),
)Authorization Readmodel Methods
Identity & Session Methods
// Get identity context
GetIdentity(identityUuid string) (*IdentityCtxModel, error)
// Get session context
GetSession(sessionUuid, sessionKey string) (*SessionCtxModel, error)
// Get service account token
GetServiceAccount(tokenUuid, tokenKey string) (*TokenCtxModel, error)Group & Permission Methods
// Get group context
GetGroup(groupUuid string) (*GroupCtxModel, error)
// Get workspace context
GetWorkspace(workspaceUuid string) (*WorkspaceCtxModel, error)
// Get workspace permissions for identity
GetWorkspacePermissions(workspaceUuid, identityUuid string) []*PermissionModelMembership Methods
// Check direct workspace membership
IsWorkspaceMember(workspaceUuid, identityUuid string) bool
// Check workspace membership including transitive (via workspace members)
IsWorkspaceMemberTransitive(workspaceUuid, identityUuid string, maxDepth int) bool
// Check if identity is member of any workspace in a list (direct or transitive)
IsWorkspaceMemberInListTransitive(workspaceUuids []string, identityUuid string, maxDepth int) bool
// Check if aggregate belongs to tenant
IsValidTenantAggregate(tenantUuid, aggregateUuid string) bool
// Check if aggregate belongs to workspace
IsValidWorkspaceAggregate(workspaceUuid, aggregateUuid string) bool
// Get all workspaces that are members of a workspace
GetMemberWorkspaces(workspaceUuid string) []*WorkspaceMemberWorkspaceCtxModel
// Check if a workspace is a member of another workspace
IsWorkspaceAMember(hostWorkspaceUuid, memberWorkspaceUuid string) boolTransitive Permission Methods
// Get workspace permissions including transitive ones from workspace memberships
GetWorkspacePermissionsTransitive(workspaceUuid, identityUuid string, maxDepth int) []*PermissionModelNote: The maxDepth parameter limits how deep the transitive lookup goes (default: 5). This prevents infinite loops in case of misconfigured circular references.
Event Handlers
The Auth readmodel listens to events from multiple domains:
Account Events
AccountRegisteredEvent- Creates account contextAccountLoggedInEvent- Creates session contextAccountLoggedOutEvent- Removes session contextAccountCredentialsUpdatedEvent- Updates credentialsAccountRemovedEvent- Removes account context
Tenant Events
TenantCreatedEvent- Creates tenant contextTenantRemovedEvent- Removes tenant contextGroupAddedEvent- Adds group to tenantGroupRemovedEvent- Removes group from tenantGroupUpdatedEvent- Updates group permissions
Identity Events
IdentityCreatedEvent- Creates identity contextIdentityRemovedEvent- Removes identity contextIdentityAddedGroupEvent- Assigns group to identityIdentityRemovedGroupEvent- Removes group from identityIdentityAddedTokenEvent- Adds service account tokenIdentityRemovedTokenEvent- Removes service account token
Workspace Events
WorkspaceCreatedEvent- Creates workspace contextWorkspaceRemovedEvent- Removes workspace contextWorkspaceMemberAddedEvent- Adds identity member to workspaceWorkspaceMemberRemovedEvent- Removes identity member from workspaceWorkspaceWorkspaceMemberAddedEvent- Adds workspace as member (transitive)WorkspaceWorkspaceMemberRemovedEvent- Removes workspace member (transitive)WorkspaceGroupAddedEvent- Adds group to workspaceWorkspaceGroupUpdatedEvent- Updates workspace group permissionsWorkspaceGroupRemovedEvent- Removes workspace group
Aggregate Association Events
OnHandleEventForTenantAggregateTuples- Tracks tenant-aggregate associationsOnHandleEventForWorkspaceAggregateTuples- Tracks workspace-aggregate associations
Custom Permission Functions
Every Command and Query can be associated with a Custom Permission Function. This function is executed during the authorization phase to determine if the command or query should be allowed based on custom logic. This allows for fine-grained control over permissions beyond standard RBAC checks. Custom permission functions can be defined and registered within the application once.
This enables the representation of use cases that cannot be covered by RBAC alone, such as:
- An Account may not delete itself.
- An Account may only change its own email/password.
- Only the own identity profile may be changed.
- and many more...
The key point is that the standard RBAC - Authorization Layer can be flexibly extended without having to modify the core code of comby. Whether a requestor is allowed to do something is always checked in the following order for a command or query:
- Skip Authorization if
ExecuteSkipAuthorizationset in the request context - Custom Permission Function (if set) -> if this function returns nil it says "Yes, the requestor is allowed to do this". Otherwise it continues with the next checks.
- RBAC Layer -> if the requestor is in the role that is allowed to do this, then they are allowed to do it or requestor is in the privileged group (
SYSTEM_TENANT_GROUP_ADMIN_UUID).
Let's illustrate this with an example:
// domain/identity/domain.go within Register function:
// ..
runtime.RegisterPermission(
runtime.NewPermissionCmdFunc(domain, &command.IdentityCommandUpdateProfile{}, "Update profile of an existing identity", func(ctx context.Context, cmd comby.Command) error {
// Following can perform this operation:
// - identity belonging to system-admin group
// - identity belonging to a group for which this permission is enabled
// - if this function returns nil
// cast into a concrete type
cmdData, ok := cmd.GetDomainCmd().(*command.IdentityCommandUpdateProfile)
if !ok {
return fmt.Errorf("%s could not cast command to concrete type", cmd)
}
// disallow patching others
var reqCtx *comby.RequestContext = cmd.GetReqCtx()
if reqCtx.SenderIdentityUuid != cmdData.IdentityUuid {
return runtime.ErrPermissionDenied
}
// allow
return nil
}),
)In this example, the command IdentityCommandUpdateProfile is registered with a custom permission function. This function checks if the SenderIdentityUuid in the request context matches the IdentityUuid in the command data. If they do not match, it returns ErrPermissionDenied, effectively preventing users from updating profiles of other identities.
This means that even if I belong to a group that does NOT have the rights to execute the command IdentityCommandUpdateProfile, I can still update my own profile because the custom permission function explicitly allows it. This in turn means: If I belong to a group that explicitly has this right as an identity, then I can change other identities as an identity profile.
Features
- ✅ Multi-layer authorization (System, Custom, RBAC)
- ✅ Command and query middleware
- ✅ Session-based authentication validation
- ✅ Service account token validation
- ✅ Tenant-level authorization (RBAC)
- ✅ Workspace-level authorization (RBAC)
- ✅ Additive permission model (Tenant OR Workspace)
- ✅ Custom permission functions
- ✅ System admin bypass
- ✅ Cross-tenant request blocking
- ✅ Aggregate ownership validation
- ✅ Real-time context updates via event sourcing
- ✅ Transitive workspace membership (workspace as member)
- ✅ Transitive permission inheritance through workspace memberships
Best Practices
Authorization Middleware
✅ Always register Auth domain first before other domains ✅ Add authorization middleware after registering domains ✅ Use ExecuteSkipAuthorization sparingly for system operations only ✅ Implement custom permission functions for complex business rules
Permission Management
✅ Use descriptive permission names (e.g., customer.create, invoice.update) ✅ Grant minimal permissions required (principle of least privilege) ✅ Use workspace-level permissions for project-specific access ✅ Use tenant-level permissions for administrative access
Session Handling
✅ Validate session expiration before processing commands ✅ Refresh sessions before they expire ✅ Invalidate sessions on logout ✅ Use secure session keys
Performance
✅ Auth readmodel is in-memory for fast lookups ✅ Avoid calling GetAuthReadmodel() in tight loops ✅ Cache identity context if needed for multiple checks ✅ Monitor readmodel restoration time on startup
Security Considerations
The comby framework incorporates robust security measures to safeguard user data and ensure secure account management:
Password & Session Key Storage
Regardless of the encryption used for the storage layer, all passwords, keys, and tokens are further encrypted/hashed with bcrypt. This ensures that even if an attacker gains access to the underlying unencrypted storage, sensitive information such as passwords, session keys, and tokens remains unreadable. This design choice necessitates that users provide these credentials, along with an associated UUID, to locate the relevant database entries for cryptographic comparison.
Example: When a user logs in, they must provide their email, and password. The password and session key are encrypted with bcrypt. The provided password is cryptographically compared to the stored value, and the encrypted session key is securely saved in the database. As a result, the application cannot retrieve a user's password or session key in plaintext, as these credentials are securely encrypted and protected against unauthorized access.
- Passwords are hashed before storage
- Session keys are hashed before storage
- Token values are hashed before storage
- Never log or expose plain passwords/keys
Cookie Security
Using Cookies in frontend applications offers distinct security advantages, particularly for mitigating attacks such as Cross-Site Request Forgery (CSRF). Here's a more detailed explanation:
Cookies serve as a secure mechanism for managing user sessions in frontend applications, providing inherent protection against CSRF attacks. When a user successfully logs in, the server generates a new session token and returns it to the client as part of the HTTP response, typically in a Set-Cookie header.
The browser automatically handles storing this cookie securely, adhering to attributes such as HttpOnly, Secure, and SameSite to bolster security:
HttpOnly: This attribute prevents JavaScript on the frontend from accessing the cookie, safeguarding it against attacks such as Cross-Site Scripting (XSS). This ensures that even if malicious scripts are injected into the page, they cannot access or steal the session token stored in the cookie.
Secure: Cookies marked with this attribute are transmitted only over secure HTTPS connections. This prevents interception of sensitive data during transmission, further reducing the risk of session hijacking through man-in-the-middle attacks.
SameSite: By setting this attribute to
StrictorLax, the browser restricts the inclusion of cookies in cross-site requests, mitigating CSRF attacks by ensuring that cookies are sent only with same-origin requests.
Unlike Authorization headers, which require explicit handling and storage (e.g., in localStorage or sessionStorage), cookies leverage the browser's built-in mechanisms for secure storage and transmission. This eliminates the need for direct interaction with cookies in frontend code, reducing the surface area for potential vulnerabilities.
Permission Validation
- Always validate permissions before executing commands
- Use custom permission functions for complex authorization logic
- Validate aggregate ownership to prevent unauthorized access
- Block cross-tenant requests except for system admins
Session Security
- Set appropriate session expiration times
- Invalidate sessions on logout
- Validate session before each request
- Use secure random session keys
Troubleshooting
"Permission Denied" error
Check:
- Is the identity in the correct tenant?
- Does the identity have the required permission?
- For workspace operations: Is identity a workspace member?
- Does the aggregate belong to the target tenant/workspace?
- Is the permission name correct (case-sensitive)?
Debug:
// Check identity context
identityCtx, _ := auth.GetAuthReadmodel().GetIdentity("identity-uuid")
fmt.Printf("Groups: %+v\n", identityCtx.Groups)
// Check workspace membership
isMember := auth.GetAuthReadmodel().IsWorkspaceMember("workspace-uuid", "identity-uuid")
fmt.Printf("Is member: %v\n", isMember)
// Check permissions
permissions := auth.GetAuthReadmodel().GetWorkspacePermissions("workspace-uuid", "identity-uuid")
fmt.Printf("Permissions: %+v\n", permissions)Session validation fails
Cause: Session expired, invalid, or not found.
Solution:
sessionCtx, err := auth.GetAuthReadmodel().GetSession(sessionUuid, sessionKey)
if err != nil {
// Session not found - user needs to login again
}
if time.Now().Unix() > sessionCtx.ExpiredAt {
// Session expired - refresh or login again
}Cross-tenant access denied
Cause: Request is trying to access resources in a different tenant.
Solution: Ensure SenderTenantUuid matches TargetTenantUuid in RequestContext, unless the user is a system admin.
Aggregate ownership validation fails
Cause: Aggregate doesn't belong to the target tenant/workspace.
Solution:
// For tenant-level
isValid := auth.GetAuthReadmodel().IsValidTenantAggregate(tenantUuid, aggregateUuid)
// For workspace-level
isValid := auth.GetAuthReadmodel().IsValidWorkspaceAggregate(workspaceUuid, aggregateUuid)Ensure aggregates are created with correct tenant/workspace context.
Disabling Authorization
For development or testing purposes, you can disable authorization:
import "github.com/gradientzero/comby/v2/domain/auth"
// Disable all authorization checks
auth.IsAuthDisabled = trueWARNING: Never disable authorization in production!