Authentication & Authorization
Easy AppServer implements a comprehensive security model with dual authentication contexts (users and apps), certificate-based request signing, and fine-grained relationship-based authorization via OpenFGA.
Overview
The platform uses two distinct authentication contexts that work together to secure all operations:
- UserAuthContext: For human users accessing the platform via browser/UI
- AppAuthContext: For applications communicating with AppServer via gRPC
Both contexts integrate with OpenFGA for fine-grained authorization decisions based on relationship tuples.
Code Reference: pkg/v2/domain/auth/context.go:5
Authentication Contexts
AuthContext Interface
// AuthContext is the common interface for authentication contexts
type AuthContext interface {
// IsUser returns true if this is a user authentication context
IsUser() bool
// IsApp returns true if this is an app authentication context
IsApp() bool
// GetIdentifier returns a string identifier for logging/debugging
GetIdentifier() string
}
Both user and app contexts implement this interface, allowing middleware and services to handle both authentication types uniformly.
UserAuthContext
Represents an authenticated user session from Ory Kratos.
Code Reference: pkg/v2/domain/auth/context.go:28
type UserContext struct {
UserID uuid.UUID
SessionID string
Email string
Verified bool // Email verification status
}
Fields:
UserID: Unique identifier from Kratos identitySessionID: Kratos session identifier for session trackingEmail: User's email address from Kratos identityVerified: Email verification status (true if email confirmed)
AppAuthContext
Represents an authenticated application via signature verification.
Code Reference: pkg/v2/domain/auth/context.go:68
type AppContext struct {
AppID uuid.UUID
AppName string
Verified bool // Signature verification status
IsProvisional bool // True for bootstrap registration
}
Fields:
AppID: Application's unique identifierAppName: Application name (e.g., "de.easy-m.todos")Verified: True if signature verification succeededIsProvisional: True for bootstrap registration (app doesn't exist in DB yet)
User Authentication
User authentication is handled by Ory Kratos, an open-source identity and user management system.
Identity Management
Kratos provides comprehensive identity features:
- User registration and login
- Email/password authentication
- Social login (OAuth2 providers: Google, GitHub, etc.)
- Multi-factor authentication (MFA) via TOTP
- Account recovery flows (password reset, account verification)
- Profile management (update email, password)
Session Management
Kratos manages user sessions with secure, HTTP-only cookies:
- Secure cookie-based sessions
- Session validation on every request
- Automatic session renewal before expiration
- Logout and session revocation
- Session lifetime configuration
Session Cookie:
- Cookie name:
APPSERVER_SESSION_COOKIE_NAME(default:ory_kratos_session) - Attributes:
HTTP-only,Secure,SameSite=Lax - Validated on every HTTP request to platform APIs
User Authentication Flow
User accesses Shell:
├─ 1. Browser loads Shell application
├─ 2. Shell checks for Kratos session cookie
├─ 3. If not authenticated:
│ ├─ Redirect to Kratos login UI
│ ├─ User enters credentials
│ ├─ Kratos validates credentials
│ └─ Kratos creates session and sets cookie
├─ 4. Shell validates session with AppServer
├─ 5. AppServer extracts user ID from session
├─ 6. AppServer checks user permissions via OpenFGA
└─ 7. User granted access if authorized
Session Validation
The AppServer validates Kratos sessions using the Kratos admin API:
// Validate session cookie
session, err := kratosClient.ToSession(ctx, sessionToken)
if err != nil {
return nil, ErrInvalidSession
}
// Extract user context
userContext := NewUserContext(
session.Identity.ID,
session.ID,
session.Identity.Traits.Email,
session.Identity.VerifiableAddresses[0].Verified,
)
Sessions are checked on every request to ensure they haven't expired or been revoked.
Application Authentication
Applications authenticate to the AppServer using X.509 certificates and cryptographic request signatures.
Certificate-Based Authentication
Each application receives:
- X.509 certificate - Signed by AppServer CA, contains app name in CN
- Private key - For signing requests (kept secret by app)
- App ID - Embedded in certificate metadata
Certificates establish the app's identity and provide the public key for signature verification.
Request Signing Protocol
Every gRPC request from an application must be signed with its private key.
Code Reference: pkg/v2/infrastructure/auth/signature.go:25
Signature Generation (Client-Side)
// 1. Construct message
const method = "GRPC";
const grpcMethod = "/easy.v2.Marketplace/Subscribe";
const timestamp = new Date().toISOString(); // RFC3339
const appName = "de.easy-m.todos";
const message = `${method}\n${grpcMethod}\n${timestamp}\n${appName}`;
// 2. Sign message with private key (RSA-SHA256)
const signature = crypto.sign("RSA-SHA256", Buffer.from(message), privateKey);
const signatureBase64 = signature.toString('base64');
// 3. Add metadata headers
metadata.set("x-app-name", appName);
metadata.set("x-app-timestamp", timestamp);
metadata.set("x-app-signature", signatureBase64);
Code Reference: pkg/v2/infrastructure/auth/signature.go:25
// Message construction
func ConstructMessage(method, pathWithQuery, timestamp, appName string) string {
return fmt.Sprintf("%s\n%s\n%s\n%s", method, pathWithQuery, timestamp, appName)
}
// Signature creation
func SignMessage(message string, privateKey *rsa.PrivateKey) (string, error) {
hashed := sha256.Sum256([]byte(message))
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
Signature Verification (Server-Side)
Code Reference: pkg/v2/presentation/grpc/grpcauth/authenticator.go:85
Signature Verification Flow:
├─ 1. Extract metadata headers (x-app-name, x-app-timestamp, x-app-signature)
├─ 2. Parse and validate timestamp
│ ├─ Check timestamp format (RFC3339)
│ ├─ Verify timestamp is not too old (< replay window)
│ └─ Verify timestamp is not too far in future (< clock skew tolerance)
├─ 3. Fetch app from database by name
├─ 4. Extract public key from app's certificate
├─ 5. Reconstruct message from request metadata
├─ 6. Verify signature matches message using public key
└─ 7. Create AppContext if verification succeeds
func (a *GRPCAuthenticator) AuthenticateApp(ctx context.Context, md metadata.MD, method string) (auth.AuthContext, error) {
// Extract credentials from metadata
appName, timestampStr, signature := extractor.ExtractAppCredentials()
// Validate timestamp
timestamp, err := time.Parse(time.RFC3339, timestampStr)
age := time.Now().Sub(timestamp)
if age > a.config.SignatureReplayWindow {
return nil, &auth.ErrExpiredSignature{Age: age.String()}
}
// Fetch app and extract public key
app, err := a.appRepo.FindByName(ctx, appName)
publicKey, err := x509.ParsePKIXPublicKey(app.Certificate.PublicKey)
// Construct message and verify signature
message := infraAuth.ConstructMessage("GRPC", method, timestampStr, appName)
err = infraAuth.VerifySignature(message, signature, publicKey)
if err != nil {
return nil, auth.ErrSignatureVerificationFailed
}
return auth.NewAppContext(app.ID, appName, true), nil
}
gRPC Metadata Headers
Applications must include these headers in gRPC metadata:
Required Headers:
x-app-name: Application name (e.g., "de.easy-m.todos")x-app-timestamp: RFC3339 timestamp (e.g., "2025-01-24T10:00:00Z")x-app-signature: Base64-encoded RSA-SHA256 signature
Example:
x-app-name: de.easy-m.billing
x-app-timestamp: 2025-01-24T15:30:00Z
x-app-signature: dGVzdHNpZ25hdHVyZWhlcmU=
Security Mechanisms
Replay Protection
Prevents replay attacks by checking request timestamps.
Configuration:
APPSERVER_SIGNATURE_REPLAY_WINDOW(default:5m)
Validation:
age := time.Now().Sub(timestamp)
if age > a.config.SignatureReplayWindow {
return nil, &auth.ErrExpiredSignature{Age: age.String()}
}
Requests older than the replay window are rejected, preventing attackers from reusing captured requests.
Clock Skew Tolerance
Allows for time differences between client and server systems.
Configuration:
APPSERVER_CLOCK_SKEW_TOLERANCE(default:30s)
Validation:
if timestamp.After(now.Add(a.config.ClockSkewTolerance)) {
return nil, &auth.ErrExpiredSignature{
Age: fmt.Sprintf("timestamp in future: %v", timestamp.Sub(now)),
}
}
Requests with timestamps too far in the future are rejected (with tolerance for clock drift).
Bootstrap Registration
For development environments, apps can register without pre-issued certificates.
Configuration:
APPSERVER_AUTH_ALLOW_BOOTSTRAP_REGISTRATION(default:truein dev,falsein prod)
Flow:
if a.config.AllowBootstrapRegistration && method == "/easy.v2.Marketplace/Subscribe" {
// Return provisional context for new apps during registration
return auth.NewProvisionalAppContext(appName), nil
}
Provisional Context:
- App generates self-signed certificate
- AppServer issues CA-signed certificate during registration
- Subsequent requests use the issued certificate
Security Note: Must be disabled in production to prevent unauthorized app registration.
Certificate Rotation
Certificate rotation is not currently supported. The RotateCertificate RPC referenced below does not exist in the protobuf definitions (easy.proto/v2/protos/services.proto:161-313).
Current workaround: Applications must re-register to obtain new certificates. This section describes the planned rotation flow for future implementation.
Applications should rotate certificates periodically for security:
Planned Rotation Process:
- App generates new key pair and CSR
- App calls
RotateCertificateRPC with CSR (not yet implemented) - AppServer validates request (signed with old certificate)
- AppServer issues new certificate signed by CA
- App starts using new certificate
- Old certificate can be revoked
Best Practices (for future implementation):
- Rotate certificates every 90-365 days
- Store certificates securely (encrypted at rest)
- Automate rotation process
- Monitor certificate expiration dates
Certificate Revocation
Certificates can be revoked if compromised:
Revocation Scenarios:
- Private key suspected to be leaked
- App uninstalled permanently
- Security incident requiring rotation
- Administrative revocation
Future: Certificate Revocation List (CRL) or OCSP support for real-time revocation checking.
Authorization with OpenFGA
Easy AppServer uses OpenFGA for fine-grained, relationship-based access control (ReBAC).
Permission Model
OpenFGA uses a tuple-based authorization model where permissions are expressed as relationships:
Tuple Format: (user, relation, object)
Example Tuples:
(user:alice, viewer, app:todos) - Alice can view the todos app
(app:billing, accessible_by, route:/api) - Billing app can access route
(role:admin#assignee, write, setting:key) - Admin role can write setting
See Permission Model for detailed authorization architecture.
Permission Types
Route Permissions
Control which apps can access specific API routes:
Tuple: (app:billing, accessible_by, route:/api/invoices)
Check: Can app:billing access route:/api/invoices?
Hook Permissions
Control which apps can trigger or listen to hooks:
Tuple: (app:notifications, trigger, hook:user.created)
Check: Can app:notifications trigger hook:user.created?
Activity Permissions
Control which apps can execute activities:
Tuple: (app:worker, execute, activity:generate-report)
Check: Can app:worker execute activity:generate-report?
Setting Permissions
Control which users/roles can read or write settings:
Tuple: (role:admin#assignee, write, setting:api-key)
Check: Can role:admin write setting:api-key?
Permission Declaration
Applications declare required permissions in their manifest:
Code Reference: easy.proto/v2/protos/manifest.proto:10
import { defineManifest } from '@easy/appserver-sdk';
const manifest = defineManifest()
.name('de.easy-m.billing')
.version('1.0.0')
// Route permissions
.permission('route', '/api/users', 'read')
.permission('route', '/api/invoices', 'write')
// Hook permissions
.permission('hook', 'user.created', 'listen')
.permission('hook', 'invoice.paid', 'trigger')
// Activity permissions
.permission('activity', 'send-email', 'execute')
.build();
During app registration, the platform creates corresponding OpenFGA tuples:
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:43
// Grant permissions declared in manifest
for _, perm := range cmd.RequiredPermissions {
if err := s.tupleManager.GrantPermission(ctx, cmd.Name, perm); err != nil {
return nil, fmt.Errorf("failed to grant permission: %w", err)
}
}
Permission Scopes
Permissions operate at different scopes:
Platform-Level:
- System configuration access
- Global resource management
- Platform administration
Organization-Level (Multi-Tenancy):
- Tenant-scoped resources
- Organization-specific data
- User management within org
Application-Level:
- App-specific permissions
- Feature access within app
- App resource management
Resource-Level:
- Individual resource permissions
- Per-user resource access
- Fine-grained access control
Dangerous Permissions
Some permissions require explicit user consent due to their sensitivity:
Examples:
system:configuration:write- System configuration changesuser:data:read- Access to user personal datapayment:process- Payment processing capabilityexternal:api:access- Access to external APIs
Consent Flow:
- App declares dangerous permission in manifest
- During installation, user is prompted for consent
- User reviews permission details and risks
- User approves or denies permission
- Only granted if user approves
Permission Checks
In Application Services
Services check permissions before executing operations:
// Check if app has permission to access route
hasPermission, err := s.permissionChecker.Check(ctx, auth.PermissionRequest{
User: fmt.Sprintf("app:%s", appName),
Relation: "accessible_by",
Object: fmt.Sprintf("route:%s", routePath),
})
if !hasPermission {
return nil, ErrPermissionDenied
}
In GraphQL Resolvers
Resolvers check user permissions before returning data:
func (r *queryResolver) App(ctx context.Context, name string) (*generated.App, error) {
// Extract user from context
userCtx := getUserFromContext(ctx)
// Check permission
hasPermission, err := r.permissionChecker.Check(ctx, auth.PermissionRequest{
User: fmt.Sprintf("user:%s", userCtx.UserID),
Relation: "viewer",
Object: fmt.Sprintf("app:%s", name),
})
if !hasPermission {
return nil, errors.New("permission denied")
}
return r.marketplaceService.GetApp(ctx, name)
}
Via SDK
Apps can check permissions at runtime:
import { checkPermission } from '@easy/appserver-sdk';
async function approveInvoice(invoiceID) {
// Check if current user can approve invoices
const canApprove = await checkPermission({
user: currentUser.id,
relation: 'approver',
object: `invoice:${invoiceID}`
});
if (!canApprove) {
throw new Error('Permission denied: Cannot approve this invoice');
}
await invoiceService.approve(invoiceID);
}
Integration Flow
User Accessing App Feature
Complete flow showing all authentication and authorization layers:
1. User Authentication:
├─ User has valid Kratos session
└─ Session cookie verified
2. User Authorization:
├─ OpenFGA checks: Can user:alice view app:todos?
└─ Permission granted via tuple: (user:alice, viewer, app:todos)
3. User Action (e.g., create todo):
├─ Frontend calls AppServer GraphQL API
└─ GraphQL mutation: createTodo(title: "Buy milk")
4. App API Call:
├─ AppServer proxies request to app backend
└─ Request routed to /api/todos via intelligent proxy
5. App Authentication:
├─ App backend makes gRPC call to AppServer
├─ Request includes x-app-signature headers
├─ AppServer verifies signature using app's certificate
└─ AppContext created for app:todos
6. App Authorization:
├─ OpenFGA checks: Can app:todos access route:/api/todos?
└─ Permission granted via tuple: (app:todos, accessible_by, route:/api/todos)
7. Resource Authorization:
├─ OpenFGA checks: Can user:alice create todo?
└─ Permission granted via tuple: (user:alice, creator, resource:todos)
8. Action Executed:
└─ All checks pass, todo created successfully
Multi-Layer Security
The platform enforces security at multiple layers:
Request Flow:
├─ Layer 1: User Authentication (Kratos session)
├─ Layer 2: User Authorization (OpenFGA user permissions)
├─ Layer 3: App Authentication (Certificate + Signature)
├─ Layer 4: App Authorization (OpenFGA app permissions)
├─ Layer 5: Resource Authorization (OpenFGA resource permissions)
└─ Layer 6: Business Logic Validation (App-specific rules)
Each layer must pass for the request to succeed, providing defense in depth.
OAuth2 / OIDC
Ory Hydra provides OAuth2 and OpenID Connect flows for third-party integrations.
Use Cases
- Third-party app integrations - External apps accessing platform APIs
- API access tokens - Machine-to-machine authentication
- SSO across external services - Single sign-on for integrated services
- Mobile app authentication - Native mobile apps using OAuth2
OAuth2 Flows
Authorization Code Flow (for web apps):
1. App redirects user to Hydra authorization endpoint
2. User authenticates with Kratos
3. User grants consent to app
4. Hydra redirects back with authorization code
5. App exchanges code for access token
6. App uses access token to call platform APIs
Client Credentials Flow (for machine-to-machine):
1. App sends client_id and client_secret to Hydra
2. Hydra validates credentials
3. Hydra returns access token
4. App uses access token to call platform APIs
Token Refresh:
1. App's access token expires
2. App sends refresh token to Hydra
3. Hydra validates refresh token
4. Hydra issues new access token
5. App continues making API calls
Consent Management
Hydra manages user consent for third-party apps:
- Consent Screen: User reviews requested scopes
- Consent Storage: User consent persisted
- Consent Revocation: User can revoke access
- Scope Validation: Hydra enforces granted scopes
API Gateway (Oathkeeper)
Ory Oathkeeper acts as an authentication and authorization proxy.
Request Flow
Client Request:
├─ 1. Request hits Oathkeeper
├─ 2. Oathkeeper validates authentication (session or token)
├─ 3. Oathkeeper enriches headers with user context
├─ 4. Oathkeeper forwards to AppServer
└─ 5. AppServer extracts user from headers
Header Enrichment
Oathkeeper adds headers to authenticated requests:
X-User-ID: 550e8400-e29b-41d4-a716-446655440000
X-User-Email: alice@example.com
X-User-Verified: true
AppServer uses these headers to create UserContext without re-validating the session.
Bypass Rules
Public endpoints bypass authentication:
# Oathkeeper configuration
- id: public-health-endpoint
match:
url: <http|https>://<.*>/health
methods:
- GET
authenticators:
- handler: anonymous
Error Handling
Oathkeeper handles authentication failures:
- 401 Unauthorized: Invalid or missing credentials
- 403 Forbidden: Valid credentials but insufficient permissions
- 302 Redirect: Redirect to login for unauthenticated requests
Configuration
Key environment variables for authentication and authorization:
Kratos Configuration
# Kratos URLs
APPSERVER_KRATOS_PUBLIC_URL=http://localhost:4433
APPSERVER_KRATOS_ADMIN_URL=http://localhost:4434
# Session management
APPSERVER_SESSION_COOKIE_NAME=ory_kratos_session
APPSERVER_SESSION_LIFESPAN=24h
APPSERVER_SESSION_IDLE_TIMEOUT=2h
Hydra Configuration
# Hydra URLs
APPSERVER_HYDRA_PUBLIC_URL=http://localhost:4444
APPSERVER_HYDRA_ADMIN_URL=http://localhost:4445
# OAuth2 settings
APPSERVER_OAUTH2_ACCESS_TOKEN_LIFESPAN=1h
APPSERVER_OAUTH2_REFRESH_TOKEN_LIFESPAN=720h # 30 days
OpenFGA Configuration
# OpenFGA connection
APPSERVER_OPENFGA_API_URL=http://localhost:8090
APPSERVER_OPENFGA_STORE_ID=01HQXYZ...
APPSERVER_OPENFGA_MODEL_ID=01HQABC... # Optional
# Cache settings
APPSERVER_OPENFGA_CACHE_ENABLED=true
APPSERVER_OPENFGA_CACHE_TTL=5m
App Authentication Configuration
# Signature validation
APPSERVER_SIGNATURE_REPLAY_WINDOW=5m
APPSERVER_CLOCK_SKEW_TOLERANCE=30s
# Bootstrap registration (DISABLE IN PRODUCTION)
APPSERVER_AUTH_ALLOW_BOOTSTRAP_REGISTRATION=false
Best Practices
Certificate Management
Storage:
Good:
- Store certificates in encrypted key vault (HashiCorp Vault, AWS KMS)
- Restrict file permissions: chmod 400 private-key.pem
- Use environment variables for certificate paths
Bad:
- Commit certificates to Git
- Store in plain text files
- Share certificates between apps
Rotation:
Recommended Schedule:
- Development: Every 90 days
- Production: Every 180-365 days
- Compromise: Immediately
Automation:
- Set up cert-manager or similar tool
- Monitor expiration dates (alert at 30 days)
- Implement zero-downtime rotation
Permission Granularity
Good:
// Fine-grained permissions
.permission('route', '/api/invoices', 'read')
.permission('route', '/api/invoices', 'write')
.permission('route', '/api/invoices/approve', 'execute')
// Specific resource permissions
.permission('resource', 'invoice:draft', 'edit')
.permission('resource', 'invoice:approved', 'view')
Bad:
// Overly broad permissions
.permission('route', '/api/*', 'read')
.permission('route', '/api/*', 'write')
// No resource-level granularity
.permission('resource', 'invoice:*', 'admin')
Least Privilege Principle
Apply minimal permissions:
// Good: Only what's needed
.permission('hook', 'user.created', 'listen') // Listen only
.permission('activity', 'send-email', 'execute') // Execute specific activity
// Bad: Excessive permissions
.permission('hook', '*', 'trigger') // Can trigger ANY hook
.permission('activity', '*', 'execute') // Can execute ANY activity
Audit Logging
Log all authentication events:
logger.Info("user authenticated",
telemetry.String("user_id", userCtx.UserID.String()),
telemetry.String("email", userCtx.Email),
telemetry.String("session_id", userCtx.SessionID))
logger.Info("app authenticated",
telemetry.String("app_name", appCtx.AppName),
telemetry.String("app_id", appCtx.AppID.String()),
telemetry.Bool("verified", appCtx.Verified))
Log permission checks:
logger.Info("permission check",
telemetry.String("user", request.User),
telemetry.String("relation", request.Relation),
telemetry.String("object", request.Object),
telemetry.Bool("granted", hasPermission))
Session Timeout Configuration
Balance security and UX:
Session Lifespan: 24 hours (absolute)
Idle Timeout: 2 hours (inactivity)
Considerations:
- Longer lifespan: Better UX, lower security
- Shorter timeout: Higher security, more login prompts
- Idle timeout: Automatically log out inactive users
Troubleshooting
Signature Verification Fails
Problem: App authentication fails with "signature verification failed"
Diagnosis:
1. Check app certificate is valid and not expired
2. Verify timestamp is within replay window
3. Confirm message construction matches server expectation
4. Check clock sync between client and server
5. Verify private key matches certificate public key
Solution:
- Regenerate certificate if expired
- Sync system clocks (NTP)
- Review signature generation code
- Check for encoding issues (base64)
Permission Denied Errors
Problem: User/app receives "permission denied" error
Diagnosis:
1. Check OpenFGA for relevant tuples
2. Verify permission is declared in app manifest
3. Confirm user has required role or permission
4. Review permission check logic
Solution:
- Add missing permission tuple in OpenFGA
- Update app manifest with required permissions
- Grant user appropriate role
- Check permission relation type matches
Session Expired
Problem: User session expires unexpectedly
Diagnosis:
1. Check session lifespan configuration
2. Verify idle timeout settings
3. Review Kratos session settings
4. Check for session revocation events
Solution:
- Increase session lifespan if appropriate
- Adjust idle timeout
- Implement session renewal (before expiration)
- Check for security events causing revocation
Bootstrap Registration Not Working
Problem: Cannot register new app in development
Diagnosis:
1. Check APPSERVER_AUTH_ALLOW_BOOTSTRAP_REGISTRATION=true
2. Verify calling /easy.v2.Marketplace/Subscribe
3. Review app name in x-app-name header
4. Check for certificate validation errors
Solution:
- Enable bootstrap registration flag
- Ensure using correct gRPC method
- Verify app name is unique
- Review certificate generation logic
Related Concepts
- Permission Model (OpenFGA & Tuples) - Authorization details
- Application Manifest & Registration - Permission declaration
- gRPC Services - Request authentication
- Developer Platform & SDK - SDK signature helpers
Further Reading
- Ory Kratos Documentation - User authentication
- Ory Hydra Documentation - OAuth2/OIDC
- OpenFGA Documentation - Relationship-based authorization
- X.509 Certificates - Certificate format
- OAuth 2.0 RFC 6749 - OAuth2 standard