Authentication
DCS uses GitHub OAuth for authentication with optional Azure AD integration for enterprise customers.
Overview
User → Portal → GitHub OAuth → DCS Session → AuthenticatedAuthentication Flow
- User clicks "Sign In" in portal
- Redirected to GitHub OAuth
- User authorizes DCS application
- GitHub returns authorization code
- DCS exchanges code for access token
- Session created, user redirected
GitHub OAuth
Required Scopes
| Scope | Purpose |
|---|---|
read:user | Read user profile |
user:email | Read user email |
repo | Access repositories |
Configuration
GitHub OAuth is configured per installation:
yaml
# Environment variables
GITHUB_CLIENT_ID: your-client-id
GITHUB_CLIENT_SECRET: your-client-secret
GITHUB_REDIRECT_URI: https://portal.duffcloudservices.com/auth/callbackSession Management
Session Cookie
After authentication, a session cookie is set:
dcs_session: encrypted-session-tokenCookie attributes:
HttpOnly: TrueSecure: True (production)SameSite: LaxMax-Age: 7 days
Session Refresh
Sessions are automatically refreshed when:
- User is active
- Token is within refresh window (< 1 day remaining)
Session Expiry
Sessions expire:
- After 7 days of inactivity
- When user explicitly logs out
- When GitHub token is revoked
API Authentication
Session-Based
Portal APIs use session cookies automatically:
typescript
// Cookie sent automatically
const response = await fetch('/api/sites', {
credentials: 'include'
})Token-Based
For server-to-server integration:
bash
curl -X GET "https://portal.duffcloudservices.com/api/sites" \
-H "Authorization: Bearer <api-token>"User Context
Current User
typescript
interface User {
id: string
email: string
name: string
avatarUrl: string
githubId: string
role: 'admin' | 'user' | 'viewer'
companies: Company[]
}Fetch Current User
typescript
const response = await fetch('/api/me', {
credentials: 'include'
})
const user = await response.json()Authorization
Role-Based Access
| Role | Permissions |
|---|---|
viewer | Read-only access |
user | Read, edit text/SEO, create dev requests |
admin | Full access including settings |
global_admin | All sites, system settings |
Company-Scoped Access
Users are scoped to companies:
typescript
interface UserCompanyRole {
companyId: string
role: 'admin' | 'user' | 'viewer'
}Site-Level Permissions
Permissions can be further restricted per site:
typescript
interface SitePermission {
siteId: string
canEdit: boolean
canPublish: boolean
canManageSettings: boolean
}Protected Routes
Frontend Route Guards
typescript
// router/guards.ts
import { useAuthStore } from '@/stores/auth'
export function requireAuth(to, from, next) {
const auth = useAuthStore()
if (!auth.isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } })
return
}
next()
}API Middleware
All /api routes except public endpoints require authentication:
go
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := validateSession(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserKey, session.User)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Logout
Logout Flow
- Client calls
/api/auth/logout - Session cookie cleared
- GitHub token revoked (optional)
- User redirected to home
Implementation
typescript
async function logout() {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
})
// Clear local state
authStore.clear()
// Redirect
router.push('/')
}Development Mode
Fake Authentication
For local development, enable fake auth:
bash
# .env
PORTAL_FAKE_AUTH_ENABLED=true
VITE_PORTAL_FAKE_AUTH=trueDev Login
Navigate to /dev-login and select a persona:
| Persona | Role | Use Case |
|---|---|---|
| Harper Host | Global Admin | Full access testing |
| Gavin Guest | Company Manager | Standard user flow |
| Olive Observer | Viewer | Read-only testing |
Error Handling
Authentication Errors
| Error | Cause | Resolution |
|---|---|---|
auth/invalid-code | OAuth code expired | Restart auth flow |
auth/token-expired | Session expired | Re-authenticate |
auth/insufficient-scope | Missing GitHub scopes | Re-authorize with scopes |
auth/user-denied | User cancelled OAuth | Handle gracefully |
Error Response
json
{
"error": "auth/token-expired",
"message": "Your session has expired. Please sign in again.",
"redirectTo": "/login"
}Security
Best Practices
- Always use HTTPS in production
- Validate redirect URIs to prevent open redirects
- Use state parameter to prevent CSRF
- Short-lived tokens with refresh mechanism
- Secure session storage with encryption
CSRF Protection
typescript
// Include CSRF token in state-changing requests
const response = await fetch('/api/sites/123/text', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
credentials: 'include',
body: JSON.stringify(data)
})Next Steps
- SEO API — SEO metadata endpoints
- Sites API — Site management
- Text Content API — Public text API
