BuildPass supports OAuth 2.0 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for integrator applications that need to access builder data on behalf of users. This flow is ideal for third-party applications that require user consent.
Overview
The Authorization Code flow allows your application to:
- Direct users to BuildPass for authentication and consent
- Receive an authorization code after user approval
- Exchange the code for access and refresh tokens
- Use access tokens to make API requests on behalf of the user
- Use refresh tokens to obtain new access tokens
When to Use This Flow
Use the Authorization Code flow when:
- Your application needs to access BuildPass data on behalf of a specific user/builder
- You want users to explicitly authorize your application
- You need long-lived access via refresh tokens
- You’re building a mobile app, SPA, or desktop application (public clients)
- You’re building a backend application that requires user authorization (confidential clients)
Client Types and Authentication
This flow works with both Confidential and Public clients:
Confidential Clients
Backend applications that can securely store a client_secret. Must authenticate with both client_id and client_secret when exchanging codes for tokens.
Examples: Web servers, backend services
Public Clients
Applications that cannot securely store secrets. Authenticate with only client_id and rely on PKCE for security.
Examples: Mobile apps, single-page applications, desktop apps
PKCE is required for all authorization requests, regardless of client type. This provides an additional layer of security beyond traditional OAuth 2.0.
Security Features
BuildPass implements industry-standard security measures:
- ✅ PKCE (RFC 7636) - Required for all authorization requests
- ✅ State Parameter - Prevents CSRF attacks
- ✅ Authorization Code Reuse Protection - Codes are single-use only
- ✅ Refresh Token Rotation - New refresh token issued on each use
- ✅ Token Revocation - RFC 7009 compliant revocation endpoint
- ✅ Rate Limiting - Protects against abuse
- ✅ Comprehensive Audit Logging - All authorization events tracked
Flow Diagram
Step-by-Step Implementation
Step 1: Generate PKCE Values
Before redirecting users, generate a code verifier and challenge:
// Generate random code_verifier (43-128 characters)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// Create code_challenge from verifier using SHA256
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Store the code_verifier securely in your session - you’ll need it in Step 3!
Step 2: Redirect User to Authorization Endpoint
Redirect the user to BuildPass for authorization:
GET https://api.buildpass.global/oauth/authorize
Required Parameters:
| Parameter | Description |
|---|
client_id | Your application’s client ID |
redirect_uri | Registered callback URL where BuildPass will send the authorization code |
response_type | Must be code |
scope | Space-separated list of requested scopes (e.g., read:builders read:subcontractors) |
code_challenge | Base64-URL-encoded SHA256 hash of your code_verifier |
code_challenge_method | Must be S256 (SHA256) |
state | Random string to prevent CSRF attacks |
Optional Parameters:
| Parameter | Description |
|---|
builder_id | Pre-select a specific builder (if user has access to multiple) |
Example Authorization URL:
const authUrl = new URL('https://api.buildpass.global/oauth/authorize');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'read:builders read:subcontractors');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', randomState);
// Redirect user
window.location.href = authUrl.toString();
Step 3: Handle the Authorization Callback
After user approval, BuildPass redirects back to your redirect_uri with:
Success Response:
https://yourapp.com/callback?code=abc123&state=xyz789
Error Response:
https://yourapp.com/callback?error=access_denied&error_description=User+denied+consent&state=xyz789
Always validate the state parameter matches what you sent to prevent CSRF attacks!
Step 4: Exchange Authorization Code for Tokens
Exchange the authorization code for access and refresh tokens:
For Confidential Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/token \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic base64(client_id:client_secret)' \
--data '{
"grant_type": "authorization_code",
"code": "received_authorization_code",
"redirect_uri": "https://yourapp.com/callback",
"code_verifier": "your_stored_code_verifier"
}'
For Public Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/token \
--header 'Content-Type: application/json' \
--data '{
"grant_type": "authorization_code",
"client_id": "your_client_id",
"code": "received_authorization_code",
"redirect_uri": "https://yourapp.com/callback",
"code_verifier": "your_stored_code_verifier"
}'
Required Parameters:
| Parameter | Description |
|---|
grant_type | Must be authorization_code |
code | Authorization code from callback |
redirect_uri | Must match the original request |
code_verifier | Original code verifier used to generate the challenge |
client_id | Required for public clients (in request body) |
Client Authentication:
Confidential Clients - Provide credentials via HTTP Basic Authentication (recommended):
Authorization: Basic base64encode(client_id:client_secret)
Or in the request body (alternative):
{
"client_id": "your_client_id",
"client_secret": "your_client_secret",
...
}
Public Clients - Include only client_id in the request body:
{
"client_id": "your_client_id",
...
}
Public clients must never include a client_secret. Security for public clients relies entirely on PKCE and the registered redirect URIs.
Success Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "550e8400-e29b-41d4-a716-446655440000",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:builders read:subcontractors"
}
Token Details:
- access_token: Use this to make API requests (lifetime: 1 hour)
- refresh_token: Use this to get new access tokens (lifetime: 30 days)
- expires_in: Access token lifetime in seconds (3600 = 1 hour)
- scope: Granted scopes (may differ from requested scopes)
Step 5: Use the Access Token
Include the access token in API requests:
curl --request GET \
--url https://api.buildpass.global/builders \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
Step 6: Refresh the Access Token
When the access token expires, use the refresh token to get a new one:
For Confidential Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/token \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic base64(client_id:client_secret)' \
--data '{
"grant_type": "refresh_token",
"refresh_token": "your_refresh_token"
}'
For Public Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/token \
--header 'Content-Type: application/json' \
--data '{
"grant_type": "refresh_token",
"client_id": "your_client_id",
"refresh_token": "your_refresh_token"
}'
Response:
{
"access_token": "new_access_token",
"refresh_token": "new_refresh_token",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:builders read:subcontractors"
}
Refresh Token Rotation: BuildPass automatically rotates refresh tokens. The old refresh token is revoked and a new one is issued. Always store the new refresh_token from the response.
Step 7: Revoke Tokens (Optional)
When a user disconnects your integration or you need to invalidate tokens:
For Confidential Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/revoke \
--header 'Content-Type: application/json' \
--header 'Authorization: Basic base64(client_id:client_secret)' \
--data '{
"token": "token_to_revoke",
"token_type_hint": "refresh_token"
}'
For Public Clients:
curl --request POST \
--url https://api.buildpass.global/oauth/revoke \
--header 'Content-Type: application/json' \
--data '{
"client_id": "your_client_id",
"token": "token_to_revoke",
"token_type_hint": "refresh_token"
}'
Parameters:
| Parameter | Description |
|---|
token | The token to revoke (access or refresh token) |
token_type_hint | Optional: access_token or refresh_token |
Per RFC 7009, the endpoint returns 200 OK regardless of whether the token was found, preventing token scanning attacks.
Available Scopes
Request only the scopes your application needs:
| Scope | Description |
|---|
read:builders | Access builder information |
read:subcontractors | Access subcontractor information |
read:projects | Access project information |
read:insurances | Access insurance certificates |
read:prequalifications | Access prequalification documents |
read:contacts | Access contact information |
read:swms | Access SWMS documents |
read:timesheets | Access timesheet data |
read:inductions | Access induction records |
Scope Changes and Re-Authorization
Important: If your application’s allowed scopes change, users must re-authorize your application to grant the new permissions.
When you request different scopes:
- Users will see a new consent screen showing the updated permissions
- Previous authorization codes and tokens remain valid with their original scopes
- New tokens will include the updated scopes
We do not currently support incremental consent (automatically upgrading existing tokens with new scopes).
Token Security Best Practices
Storage Guidelines
Critical Security Requirements:
For All Clients:
- Encrypt tokens at rest - Use your platform’s secure storage
- Never log tokens - Treat them as sensitive credentials
- Use HTTPS only - All communication must be over TLS
- Validate redirect URIs - Ensure they match registered URIs exactly
For Confidential Clients:
- Protect client_secret - Store securely using secret managers (AWS Secrets Manager, HashiCorp Vault, etc.)
- Never expose secrets client-side - Keep credentials server-side only
- Use HTTP Basic Auth - Preferred over body parameters
- Rotate credentials periodically - Contact BuildPass for rotation
For Public Clients:
- Never store tokens in browser localStorage or sessionStorage - Vulnerable to XSS attacks
- Use platform-specific secure storage:
- iOS: Keychain
- Android: KeyStore
- Desktop: OS-specific credential managers
- Implement proper PKCE - Generate cryptographically random verifiers
- Validate state parameter - Prevent CSRF attacks
- Use short-lived in-memory storage where possible for access tokens
Token Handling
// ✅ GOOD: Secure token storage (Node.js example)
const encryptedToken = encrypt(accessToken, encryptionKey);
await secureDb.store('access_token', encryptedToken);
// ❌ BAD: Never do this
localStorage.setItem('access_token', accessToken); // Vulnerable to XSS!
console.log('Token:', accessToken); // Never log tokens!
Detecting Token Expiration
// Check expires_in from token response
const expiresAt = Date.now() + (response.expires_in * 1000);
// Before making API requests:
if (Date.now() >= expiresAt) {
// Token expired, refresh it
await refreshAccessToken();
}
Error Responses
All OAuth errors follow RFC 6749 standards:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
Common Error Codes:
| Error Code | Description | HTTP Status |
|---|
invalid_request | Missing or malformed parameters | 400 |
invalid_client | Invalid client credentials | 401 |
invalid_grant | Invalid/expired authorization code | 400 |
invalid_scope | Requested scopes not allowed | 400 |
access_denied | User denied authorization | 400 |
server_error | Internal server error | 500 |
Rate Limits
OAuth endpoints are rate-limited to prevent abuse:
| Endpoint | Limit | Window |
|---|
/oauth/authorize | 30 requests | 10 seconds |
/oauth/token | 60 requests | 10 seconds |
/oauth/revoke | 30 requests | 10 seconds |
Rate limit headers are included in responses:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
Retry-After: 5
Authorization Code Reuse Protection
Security Feature: Authorization codes can only be used once. Attempting to reuse a code will:
- Return an
invalid_grant error
- Revoke all refresh tokens associated with that authorization
- Require the user to re-authorize your application
This protects against authorization code interception attacks.
Testing Your Integration
Step-by-Step Checklist
Example Error Scenarios
Expired Authorization Code:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
Invalid PKCE Verifier:
{
"error": "invalid_grant",
"error_description": "Invalid PKCE verifier"
}
Authorization Code Reused:
{
"error": "invalid_grant",
"error_description": "Authorization code has already been used"
}
RFC Compliance
BuildPass OAuth implementation is fully compliant with:
- ✅ RFC 6749 - OAuth 2.0 Authorization Framework
- ✅ RFC 7636 - Proof Key for Code Exchange (PKCE)
- ✅ RFC 7009 - Token Revocation
- ✅ RFC 6750 - Bearer Token Usage
Support
For questions or issues with OAuth integration:
Need help with your integration? Our team is here to assist with implementation questions and troubleshooting.