Skip to content

Authentication

DCS uses GitHub OAuth for authentication with optional Azure AD integration for enterprise customers.

Overview

User → Portal → GitHub OAuth → DCS Session → Authenticated

Authentication Flow

  1. User clicks "Sign In" in portal
  2. Redirected to GitHub OAuth
  3. User authorizes DCS application
  4. GitHub returns authorization code
  5. DCS exchanges code for access token
  6. Session created, user redirected

GitHub OAuth

Required Scopes

ScopePurpose
read:userRead user profile
user:emailRead user email
repoAccess 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/callback

Session Management

After authentication, a session cookie is set:

dcs_session: encrypted-session-token

Cookie attributes:

  • HttpOnly: True
  • Secure: True (production)
  • SameSite: Lax
  • Max-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

RolePermissions
viewerRead-only access
userRead, edit text/SEO, create dev requests
adminFull access including settings
global_adminAll 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

  1. Client calls /api/auth/logout
  2. Session cookie cleared
  3. GitHub token revoked (optional)
  4. 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=true

Dev Login

Navigate to /dev-login and select a persona:

PersonaRoleUse Case
Harper HostGlobal AdminFull access testing
Gavin GuestCompany ManagerStandard user flow
Olive ObserverViewerRead-only testing

Error Handling

Authentication Errors

ErrorCauseResolution
auth/invalid-codeOAuth code expiredRestart auth flow
auth/token-expiredSession expiredRe-authenticate
auth/insufficient-scopeMissing GitHub scopesRe-authorize with scopes
auth/user-deniedUser cancelled OAuthHandle gracefully

Error Response

json
{
  "error": "auth/token-expired",
  "message": "Your session has expired. Please sign in again.",
  "redirectTo": "/login"
}

Security

Best Practices

  1. Always use HTTPS in production
  2. Validate redirect URIs to prevent open redirects
  3. Use state parameter to prevent CSRF
  4. Short-lived tokens with refresh mechanism
  5. 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

Duff Cloud Services Documentation