Skip to main content

OAuth Flow

Aurora uses OAuth 2.0 for secure authentication with cloud providers. Each provider implements OAuth differently.

Supported OAuth Providers

Google Cloud Platform

OAuth 2.0 with consent screen

Tailscale

OAuth client credentials flow

OVH Cloud

OAuth 2.0 with custom scopes

Microsoft Azure

Service principal (client credentials)

Google Cloud Platform OAuth

GCP uses standard OAuth 2.0 authorization code flow.

Flow Diagram

1. Initiate OAuth Flow

POST /gcp/login
{
  "userId": "user_123"
}
Response:
{
  "login_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=AURORA_CLIENT_ID&redirect_uri=https://aurora.example.com/gcp/callback&response_type=code&scope=https://www.googleapis.com/auth/cloud-platform&state=user_123&access_type=offline&prompt=consent"
}
Redirect user to login_url:
  • User authenticates with Google
  • Reviews requested permissions
  • Grants consent
  • Google redirects to Aurora callback

3. Callback & Token Exchange

GET /gcp/callback Google redirects with:
  • code - Authorization code
  • state - User ID (for security)
Aurora:
  1. Validates state parameter
  2. Exchanges code for tokens
  3. Stores tokens in Vault
  4. Triggers post-auth setup
  5. Redirects to frontend
Redirect URL:
https://aurora.example.com/chat?login=gcp_setup_pending&task_id=abc123

4. Post-Auth Setup

Asynchronous setup tasks:
  1. Fetch user’s GCP projects
  2. Enable required APIs
  3. Create service accounts
  4. Configure IAM permissions
  5. Set up billing exports
  6. Initialize project metadata
  7. Complete setup
Poll Status:
GET /gcp/setup/status/abc123
Progress Response:
{
  "state": "PROGRESS",
  "status": "Enabling Cloud Resource Manager API",
  "complete": false,
  "progress": 57,
  "step": 4,
  "total_steps": 7
}
Complete Response:
{
  "state": "SUCCESS",
  "status": "Setup completed",
  "complete": true,
  "result": {
    "projects_configured": 3,
    "apis_enabled": 15
  }
}

OAuth Scopes

GCP OAuth requires:
  • https://www.googleapis.com/auth/cloud-platform - Full cloud platform access
  • https://www.googleapis.com/auth/compute - Compute Engine
  • https://www.googleapis.com/auth/cloudresourcemanager - Project management

Token Management

  • Access Token - Short-lived (1 hour), used for API calls
  • Refresh Token - Long-lived, used to get new access tokens
  • Expiration - Stored in database (expires_at field)
  • Refresh - Automatic when access token expires

Tailscale OAuth

Tailscale uses OAuth 2.0 client credentials flow.

Flow Diagram

Client Credentials Flow

POST /tailscale_api/tailscale/connect
{
  "clientId": "oauth-client-123",
  "clientSecret": "tskey-client-secret",
  "tailnet": "example.com"
}
Aurora:
  1. Requests access token from Tailscale
  2. Validates token by fetching devices
  3. Stores credentials in Vault
  4. Generates SSH key pair
  5. Creates reusable auth key
Response:
{
  "success": true,
  "message": "Tailscale connected successfully",
  "tailnet": "example.com",
  "tailnetName": "Example Org",
  "deviceCount": 12
}

Token Refresh

POST /tailscale_api/tailscale/refresh-token Manually refresh access token:
{
  "success": true,
  "message": "Token refreshed successfully",
  "expiresAt": "2024-03-15T12:00:00Z"
}

OAuth Scopes

Tailscale scopes:
  • devices - Read device information
  • devices:write - Manage devices
  • acl - Read ACL configuration
  • acl:write - Modify ACL
  • keys - Manage auth keys

Azure Service Principal

Azure uses service principal authentication (similar to OAuth client credentials).

Flow Diagram

Service Principal Authentication

POST /azure/login
{
  "userId": "user_123",
  "tenantId": "tenant-guid",
  "clientId": "app-guid",
  "clientSecret": "secret-value",
  "subscriptionId": "sub-guid"
}
Aurora:
  1. Creates ClientSecretCredential
  2. Requests management token
  3. Validates by fetching subscriptions
  4. Stores credentials in Vault
Response:
{
  "message": "Successfully logged in to Azure",
  "subscription_id": "sub-guid",
  "subscription_name": "Production Subscription"
}

Token Expiration

  • Access tokens expire after 1 hour
  • Automatically refreshed on API calls
  • Uses stored service principal credentials
  • No manual refresh required

OAuth Security

State Parameter

Prevents CSRF attacks:
// Encode user_id in state
const state = encodeURIComponent(userId);
const authUrl = `${OAUTH_URL}?state=${state}`;

// Validate on callback
const userId = decodeURIComponent(request.query.state);
if (!userId) {
  throw new Error('Invalid state parameter');
}

External ID (AWS)

Adds security layer for role assumption:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::AURORA_ACCOUNT:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "UNIQUE_EXTERNAL_ID"
        }
      }
    }
  ]
}
  • Each workspace has unique external ID
  • Prevents confused deputy problem
  • Required for role assumption

Token Storage

HashiCorp Vault:
# Store tokens
vault_client.write(
    f'aurora/users/{user_id}/gcp',
    {
        'access_token': access_token,
        'refresh_token': refresh_token,
        'expires_at': expires_at
    }
)

# Database stores reference
db.execute(
    "INSERT INTO user_tokens (user_id, provider, secret_ref) VALUES (%s, %s, %s)",
    (user_id, 'gcp', f'vault:kv/data/aurora/users/{user_id}/gcp')
)

Credential Rotation

GCP:
  • Access tokens expire after 1 hour
  • Refresh tokens used to get new access tokens
  • Refresh tokens rotated on use (optional)
AWS:
  • STS credentials expire after 1 hour (default)
  • Automatically re-assumed on expiration
  • External ID never changes
Azure:
  • Access tokens expire after 1 hour
  • Client secrets expire after configured period
  • No automatic rotation (manual renewal required)
Tailscale:
  • Access tokens expire after 90 days (default)
  • Manual refresh via /tailscale/refresh-token
  • Client credentials never expire

Error Handling

OAuth Errors

Invalid Grant:
{
  "error": "invalid_grant",
  "error_description": "Authorization code expired"
}
Action: Restart OAuth flow Access Denied:
{
  "error": "access_denied",
  "error_description": "User denied consent"
}
Action: Inform user, allow retry Invalid Client:
{
  "error": "invalid_client",
  "error_description": "Invalid client credentials"
}
Action: Verify Aurora OAuth configuration

Token Refresh Errors

Refresh Token Expired:
{
  "error": "invalid_grant",
  "error_description": "Token has been expired or revoked"
}
Action: Trigger re-authentication Network Error:
try:
    response = oauth_client.refresh_token(refresh_token)
except requests.exceptions.RequestException as e:
    logger.error(f'Token refresh failed: {e}')
    # Retry with exponential backoff

Best Practices

Frontend Implementation

class OAuthConnector {
  async connect(provider, userId) {
    // 1. Initiate flow
    const { login_url } = await this.getLoginUrl(provider, userId);
    
    // 2. Open OAuth window
    const authWindow = window.open(login_url, '_blank', 'width=600,height=700');
    
    // 3. Poll for completion
    const result = await this.pollForCompletion(authWindow);
    
    return result;
  }
  
  async pollForCompletion(authWindow) {
    return new Promise((resolve, reject) => {
      const checkInterval = setInterval(() => {
        try {
          // Check if window closed
          if (authWindow.closed) {
            clearInterval(checkInterval);
            // Check connection status
            this.checkStatus().then(resolve);
          }
        } catch (e) {
          // Cross-origin error means still on OAuth page
        }
      }, 500);
      
      // Timeout after 5 minutes
      setTimeout(() => {
        clearInterval(checkInterval);
        authWindow.close();
        reject(new Error('OAuth timeout'));
      }, 300000);
    });
  }
}

Secure Callback Handling

@app.route('/oauth/callback')
def oauth_callback():
    # 1. Validate state
    state = request.args.get('state')
    if not state:
        return 'Missing state parameter', 400
    
    user_id = decode_state(state)
    if not user_id:
        return 'Invalid state', 400
    
    # 2. Exchange code for token
    code = request.args.get('code')
    if not code:
        return 'Missing authorization code', 400
    
    try:
        tokens = exchange_code_for_tokens(code)
    except OAuthError as e:
        logger.error(f'Token exchange failed: {e}')
        return redirect(f'{FRONTEND_URL}?login=failed')
    
    # 3. Store tokens securely
    store_tokens_in_vault(user_id, tokens)
    
    # 4. Trigger post-auth setup
    task = setup_account.delay(user_id)
    
    # 5. Redirect to frontend
    return redirect(f'{FRONTEND_URL}?login=success&task_id={task.id}')

Token Refresh

def get_access_token(user_id, provider):
    """Get valid access token, refreshing if needed."""
    tokens = get_tokens_from_vault(user_id, provider)
    
    # Check expiration
    if time.time() >= tokens['expires_at'] - 300:  # 5 min buffer
        # Refresh token
        new_tokens = refresh_access_token(tokens['refresh_token'])
        store_tokens_in_vault(user_id, new_tokens)
        return new_tokens['access_token']
    
    return tokens['access_token']

Build docs developers (and LLMs) love