OAuth Authorization Flow

Complete OAuth 2.0 setup from testing to production in 15 minutes

15 mins
Intermediate

Related API Endpoints

Implement OAuth 2.0 to let DoorFlow users authorize your application. This guide covers the complete flow from testing to production.

Overview

DoorFlow uses the OAuth 2.0 authorization code flow. Your application will:

  1. Create an OAuth application in the developer portal
  2. Get client_id and client_secret
  3. Implement the authorization code flow
  4. Test with a DoorFlow sandbox account
  5. Request approval for production
  6. Connect to real customer accounts

Time to implement: 30-60 minutes for a basic integration.

Application States

Your OAuth application has different states:

Testing (enabled: true, approved: false)

  • Development state
  • Connected to DoorFlow sandbox account
  • Test with fake doors and events
  • Full OAuth flow works
  • Not visible to customers

Approved (enabled: true, approved: true)

  • Production ready
  • Can connect to real customer accounts
  • Listed in DoorFlow marketplace (optional)
  • Full OAuth flow works with any DoorFlow account

Disabled/Suspended

  • Application not active
  • Cannot authorize new accounts

Step 1: Create OAuth Application

  1. Go to DoorFlow Developer Portal
  2. Sign in or create developer account
  3. Click "Create New Application"
  4. Fill out form:

Required fields

Name: Your application name (shown to users)
Description: What your app does
Redirect URI: Your callback URL where DoorFlow redirects after authorization
  • Development: Can use http://localhost:3000/callback
  • Production: Must be HTTPS https://yourapp.com/callback
  • Used for both testing and approved states
Development: Can use http://localhost:3000/callback
Production: Must be HTTPS https://yourapp.com/callback
Used for both testing and approved states
Scopes: What permissions you need
  • account.person - Manage people and credentials
  • account.channel.readonly - View doors
  • account.event.access.readonly - View access events
  • See [Choosing the Right Scopes] for full list
account.person - Manage people and credentials
account.channel.readonly - View doors
account.event.access.readonly - View access events
See [Choosing the Right Scopes] for full list
  1. Click "Create Application"

What you get

client_id - Public identifier (safe to embed in frontend)
client_secret - Secret key (NEVER expose in frontend)
Application starts in "Testing" state
Linked to a DoorFlow sandbox account

Step 2: Testing State

Your application is now in testing state. This means:

You can

Implement the full OAuth flow
Test with DoorFlow sandbox account
Use client_id and client_secret immediately
Test all scopes you requested
View fake access events in sandbox

You cannot

Connect to real customer accounts
Be listed in marketplace
Use in production

Sandbox account features

Pre-configured test doors/channels
Virtual events generated automatically
Safe environment for testing
No real hardware affected

Step 3: Implement OAuth Flow

The OAuth authorization code flow has 5 steps:

1. User clicks "Connect to DoorFlow"
   ↓
2. Redirect to DoorFlow authorization URL
   ↓
3. User logs in and grants permission
   ↓
4. DoorFlow redirects back with authorization code
   ↓
5. Exchange code for access token (server-side)

3.1: Build Authorization URL

When user clicks "Connect to DoorFlow" button:

javascript
function connectToDoorFlow() {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: 'YOUR_REDIRECT_URI',
    scope: 'account.person account.event.access.readonly',
    state: generateRandomString(32) // CSRF protection
  });

  // Store state for verification
  sessionStorage.setItem('oauth_state', params.get('state'));

  // Redirect user to DoorFlow
  window.location.href = `https://api.doorflow.com/oauth/authorize?${params}`;
}

function generateRandomString(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}

Parameters

response_type - Always "code"
client_id - Your application's client ID
redirect_uri - Must match registered URI exactly
scope - Space-separated list of scopes
state - Random string for CSRF protection (recommended)

3.2: User Authorizes

DoorFlow shows authorization screen:

  • Your app name and description
  • Which scopes you're requesting
  • Which DoorFlow account to connect

Testing state: User can only connect the sandbox account

Approved state: User can connect any DoorFlow account they have access to

User clicks "Authorize" or "Deny"

3.3: Handle Callback

DoorFlow redirects back to your redirect_uri:

If approved:

https://yourapp.com/callback?code=AUTH_CODE_HERE&state=SAME_STATE

If denied:

https://yourapp.com/callback?error=access_denied&state=SAME_STATE

Your callback handler:

javascript
// Node.js/Express example
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query;

  // 1. Verify state (CSRF protection)
  if (state !== req.session.oauth_state) {
    return res.status(403).send('Invalid state - possible CSRF attack');
  }

  // 2. Check for errors
  if (error) {
    console.log('User denied authorization:', error);
    return res.redirect('/error?message=Authorization denied');
  }

  // 3. Exchange code for token
  try {
    const tokens = await exchangeCodeForToken(code);

    // 4. Store tokens securely
    req.session.access_token = tokens.access_token;
    req.session.refresh_token = encrypt(tokens.refresh_token); // Encrypt!

    // 5. Redirect to app
    res.redirect('/dashboard');

  } catch (error) {
    console.error('Token exchange failed:', error);
    res.redirect('/error?message=Authentication failed');
  }
});

3.4: Exchange Code for Token

CRITICAL: This must happen server-side only. Never expose client_secret in frontend.

javascript
async function exchangeCodeForToken(code) {
  const response = await fetch('https://api.doorflow.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: process.env.REDIRECT_URI,
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET // Server-side only!
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token exchange failed: ${error.error_description}`);
  }

  return await response.json();
}

Response:

json
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "a1b2c3...",
  "scope": "account.person account.event.access.readonly",
  "created_at": 1704067200
}

Fields

access_token - Use for API calls (1 hour lifetime)
refresh_token - Use to get new access tokens
expires_in - Seconds until token expires (3600 = 1 hour)
scope - Scopes that were actually granted

3.5: Use Access Token

Include in Authorization header for all API requests:

javascript
async function callDoorFlowAPI(endpoint, options = {}) {
  const response = await fetch(`https://api.doorflow.com${endpoint}`, {
    ...options,
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      ...options.headers
    }
  });

  return await response.json();
}

// Example: Get account info
const account = await callDoorFlowAPI('/api/3/account');
console.log('Connected to:', account.name);

Step 4: Test with Sandbox Account

Your testing state application connects to a DoorFlow sandbox account.

Testing checklist

Test authorization flow:

  • Click "Connect to DoorFlow" in your app
  • Authorize with sandbox account
  • Verify redirect back to your app
  • Confirm tokens stored correctly
Click "Connect to DoorFlow" in your app
Authorize with sandbox account
Verify redirect back to your app
Confirm tokens stored correctly

Test API calls:

  • Create a person in sandbox
  • Issue a credential
  • Query channels
  • View events
Create a person in sandbox
Issue a credential
Query channels
View events

Test token refresh:

  • Wait for token to expire (1 hour) or manually expire
  • Refresh token using refresh_token
  • Verify new tokens work
Wait for token to expire (1 hour) or manually expire
Refresh token using refresh_token
Verify new tokens work

Test error handling:

  • What if user denies authorization?
  • What if token expires?
  • What if API returns errors?
What if user denies authorization?
What if token expires?
What if API returns errors?

Sandbox account features

Pre-created channels (doors) with IDs like 1340, 1341
Virtual access events generated periodically
People and credentials you create are only in sandbox
Safe to experiment - nothing affects real systems

Step 5: Request Production Approval

When testing is complete and you're ready for production:

  1. Test thoroughly - Make sure your integration works
  2. Review security

    Client secret never exposed in frontend
    Refresh tokens encrypted in database
    Using HTTPS redirect URI
    State parameter validated
  3. In Developer Portal

    Click "Request Approval" on your application
    Fill out production readiness form
    DoorFlow team reviews your application
  4. DoorFlow reviews

    Checks OAuth implementation
    Verifies security practices
    Tests your integration
    May request changes
  5. Approval granted

    Application state changes to "Approved"
    Can now connect to any DoorFlow customer account
    Optional: Listed in DoorFlow marketplace

Timeline: Approval typically takes 3-5 business days.

Step 6: Production Use

Once approved, your application can connect to real customer accounts.

What changes

Authorization URL works for any DoorFlow account (not just sandbox)
Users see your app in their DoorFlow account settings
Real doors, real credentials, real events
Your app may be listed in marketplace

What stays the same

OAuth flow is identical
Same client_id and client_secret
Same scopes
Same API endpoints

Production best practices

Monitor authorization rates:

  • How many users authorize?
  • How many deny?
  • Track conversion rate
How many users authorize?
How many deny?
Track conversion rate

Handle token refresh:

  • Automatic refresh on 401
  • Don't interrupt user experience
  • See [OAuth Token Refresh] guide
Automatic refresh on 401
Don't interrupt user experience
See [OAuth Token Refresh] guide

Monitor API usage:

  • Stay under rate limits (120 req/minute)
  • Log all API errors
  • Set up alerts
Stay under rate limits (120 req/minute)
Log all API errors
Set up alerts

Support customers:

  • Clear error messages
  • Help docs for users
  • Support email in your app
Clear error messages
Help docs for users
Support email in your app

Token Storage

Access token

Short-lived (1 hour)
Store in session or memory
OK to lose on restart

Refresh token

Long-lived (doesn't expire)
MUST store encrypted in database
Use to get new access tokens
Never log or expose

Example storage:

javascript
// Store tokens (encrypt refresh token!)
async function saveTokens(userId, tokens) {
  await db.query(
    'INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expires_at) VALUES (?, ?, ?, ?)',
    [
      userId,
      tokens.access_token,
      encrypt(tokens.refresh_token), // Encrypt!
      Date.now() + (tokens.expires_in * 1000)
    ]
  );
}

// Retrieve tokens
async function getTokens(userId) {
  const row = await db.query(
    'SELECT access_token, refresh_token, expires_at FROM oauth_tokens WHERE user_id = ?',
    [userId]
  );

  return {
    access_token: row.access_token,
    refresh_token: decrypt(row.refresh_token),
    expires_at: row.expires_at
  };
}

Common Errors

"redirect_uri_mismatch"

  • URI doesn't match registered value exactly
  • Check trailing slashes, http vs https
  • URLs are case-sensitive

"invalid_client"

  • Client ID or secret is wrong
  • Check credentials from developer portal

"invalid_grant"

  • Authorization code expired (10 minute lifetime)
  • Code already used (one-time use only)
  • Redirect URI doesn't match

"invalid_scope"

  • Requesting scope your app doesn't have
  • Update scopes in developer portal

"access_denied"

  • User clicked "Deny"
  • Handle gracefully in your app

Security Checklist

Before going live:

  • Client secret stored server-side only (never in frontend)
  • Using HTTPS for redirect URI in production
  • State parameter validated (CSRF protection)
  • Refresh tokens encrypted in database
  • Tokens never logged
  • Error handling implemented
  • Token refresh working
  • Tested thoroughly in sandbox

Complete Example

Full OAuth implementation:

javascript
// server.js (Node.js/Express)
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');

const app = express();

app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true }
}));

// Connect to DoorFlow button
app.get('/connect', (req, res) => {
  const state = crypto.randomBytes(32).toString('hex');
  req.session.oauth_state = state;

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.DOORFLOW_CLIENT_ID,
    redirect_uri: process.env.DOORFLOW_REDIRECT_URI,
    scope: 'account.person account.event.access.readonly',
    state: state
  });

  res.redirect(`https://api.doorflow.com/oauth/authorize?${params}`);
});

// OAuth callback
app.get('/callback', async (req, res) => {
  const { code, state, error } = req.query;

  // Verify state
  if (state !== req.session.oauth_state) {
    return res.status(403).send('Invalid state');
  }

  // Handle denial
  if (error) {
    return res.redirect('/error?message=Authorization denied');
  }

  try {
    // Exchange code for token
    const tokenResponse = await fetch('https://api.doorflow.com/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: process.env.DOORFLOW_REDIRECT_URI,
        client_id: process.env.DOORFLOW_CLIENT_ID,
        client_secret: process.env.DOORFLOW_CLIENT_SECRET
      })
    });

    if (!tokenResponse.ok) {
      throw new Error('Token exchange failed');
    }

    const tokens = await tokenResponse.json();

    // Store tokens securely
    req.session.access_token = tokens.access_token;
    // In production: encrypt and store refresh_token in database

    // Get account info
    const accountResponse = await fetch('https://api.doorflow.com/api/3/account', {
      headers: { 'Authorization': `Bearer ${tokens.access_token}` }
    });

    const account = await accountResponse.json();

    res.redirect(`/dashboard?connected=${account.name}`);

  } catch (error) {
    console.error('OAuth error:', error);
    res.redirect('/error?message=Authentication failed');
  }
});

app.listen(3000);

Next Steps

Token expired?

  • [OAuth Token Refresh] - Automatic token refresh

Building mobile app?

  • [OAuth PKCE] - Secure mobile OAuth without client secret

Need help with scopes?

  • [Choosing the Right Scopes] - Scope selection guide

Ready to code?

  • [Common Workflows] - Complete examples using OAuth