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