OAuth2

What is OAuth2?

OAuth 2.0 is the industry-standard protocol for authorization. It allows your app to access specific data in user accounts with explicit user consent, without requiring users to share their passwords.

  • Secure Access: Users authorize without sharing passwords
  • Granular Permissions: Request only the scopes your app needs
  • Token-Based: Access and refresh tokens for secure API calls
  • User Consent: Explicit approval for each permission

How OAuth2 Works

OAuth2 uses the authorization code grant flow:

The flow ensures secure, controlled access by:

  1. Requiring explicit user consent
  2. Using temporary authorization codes
  3. Providing time-limited access tokens
  4. Supporting granular permissions through scopes

Creating an OAuth2 App

2

Create new app

Click Create new app

3

Select app type

Choose the appropriate app type:

Organization App: Generate organization-level access tokens

Use for:

  • Contract data reading
  • Timesheets management
  • Invoice adjustments
  • SCIM API integration
  • Accounting data access

Provides access to all organization resources based on granted scopes.

4

Fill out app details

Provide the following information:

  • App Name: Displayed to users during authorization
  • Description: Explain what your app does
  • Redirect URI: Where users return after authorization (e.g., https://yourapp.com/callback)
  • Logo: Optional - shown on consent screen
5

Save credentials

After creation, you’ll receive:

  • Client ID: Public identifier for your app
  • Client Secret: Secret key for authenticating your app

The client secret is only shown once. Store it securely immediately. If you lose it, you’ll need to regenerate it.

OAuth2 Flow Implementation

Step 1: Request Permissions

Redirect users to Deel’s authorization endpoint with the required parameters:

https://app.deel.com/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPES}&state={STATE}

Required Parameters:

ParameterDescription
client_idYour app’s client ID
redirect_uriURL where user returns after authorization (must match registered URI)
scopeSpace-separated list of requested scopes (e.g., contracts:read contracts:write)
stateRandom string to prevent CSRF attacks (you validate this on return)

Example URL:

$https://app.deel.com/oauth2/authorize?client_id=abc123&redirect_uri=https://yourapp.com/callback&scope=contracts:read%20contracts:write&state=random_string_xyz
1// Generate authorization URL
2const authUrl = new URL('https://app.deel.com/oauth2/authorize');
3authUrl.searchParams.append('client_id', process.env.CLIENT_ID);
4authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
5authUrl.searchParams.append('scope', 'contracts:read contracts:write');
6authUrl.searchParams.append('state', generateRandomState()); // Implement CSRF protection
7
8// Redirect user
9res.redirect(authUrl.toString());

Step 2: User Authorizes

The user sees a consent screen showing:

  • Your app name and logo
  • What permissions (scopes) you’re requesting
  • Option to approve or deny

When they approve, they’re redirected to your redirect_uri with an authorization code:

https://yourapp.com/callback?code=AUTH_CODE&state=random_string_xyz

Always verify the state parameter matches what you sent to prevent CSRF attacks.

Step 3: Exchange Code for Access Token

Exchange the authorization code for an access token by making a POST request:

$curl -X POST 'https://app.deel.com/oauth2/tokens' \
> -H 'Authorization: Basic BASE64_ENCODED_CREDENTIALS' \
> -H 'Content-Type: application/x-www-form-urlencoded' \
> -d 'grant_type=authorization_code' \
> -d 'code=AUTH_CODE' \
> -d 'redirect_uri=https://yourapp.com/callback'

Authentication:

  • Use HTTP Basic authentication
  • Encode your credentials: base64(client_id:client_secret)

Example encoding:

$echo -n "client_id:client_secret" | base64
1const axios = require('axios');
2
3// Encode credentials
4const credentials = Buffer.from(
5 `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
6).toString('base64');
7
8// Exchange code for token
9const response = await axios.post(
10 'https://app.deel.com/oauth2/tokens',
11 new URLSearchParams({
12 grant_type: 'authorization_code',
13 code: authCode,
14 redirect_uri: 'https://yourapp.com/callback'
15 }),
16 {
17 headers: {
18 'Authorization': `Basic ${credentials}`,
19 'Content-Type': 'application/x-www-form-urlencoded'
20 }
21 }
22);
23
24const { access_token, refresh_token, expires_in } = response.data;

Response:

1{
2 "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
3 "token_type": "Bearer",
4 "expires_in": 2592000,
5 "refresh_token": "def502000a2b3c4d5e6f...",
6 "scope": "contracts:read contracts:write"
7}
  • Access tokens are valid for 30 days (2,592,000 seconds)
  • Refresh tokens are valid for 90 days

Step 4: Make Authenticated Requests

Use the access token to make API requests. OAuth2 requests require two headers:

$curl -X GET 'https://api.letsdeel.com/rest/v2/contracts' \
> -H 'Authorization: Bearer ACCESS_TOKEN' \
> -H 'x-client-id: CLIENT_ID'

OAuth2 requests require both the Authorization header AND the x-client-id header. Missing either will result in a 401 error.

1const axios = require('axios');
2
3const deelAPI = axios.create({
4 baseURL: 'https://api.letsdeel.com/rest/v2',
5 headers: {
6 'Authorization': `Bearer ${accessToken}`,
7 'x-client-id': process.env.CLIENT_ID
8 }
9});
10
11// Make authenticated request
12const contracts = await deelAPI.get('/contracts');
13console.log(contracts.data);

Token Rotation

Access tokens expire after 30 days. Refresh them before expiration to maintain uninterrupted access.

Refreshing Access Tokens

Make a POST request with your refresh token:

$curl -X POST 'https://app.deel.com/oauth2/tokens' \
> -H 'Authorization: Basic BASE64_ENCODED_CREDENTIALS' \
> -H 'Content-Type: application/x-www-form-urlencoded' \
> -d 'grant_type=refresh_token' \
> -d 'refresh_token=REFRESH_TOKEN' \
> -d 'redirect_uri=https://yourapp.com/callback'
1import requests
2import base64
3import os
4
5def refresh_access_token(refresh_token):
6 credentials = base64.b64encode(
7 f"{os.getenv('CLIENT_ID')}:{os.getenv('CLIENT_SECRET')}".encode()
8 ).decode()
9
10 response = requests.post(
11 'https://app.deel.com/oauth2/tokens',
12 headers={
13 'Authorization': f'Basic {credentials}',
14 'Content-Type': 'application/x-www-form-urlencoded'
15 },
16 data={
17 'grant_type': 'refresh_token',
18 'refresh_token': refresh_token,
19 'redirect_uri': 'https://yourapp.com/callback'
20 }
21 )
22
23 return response.json() # Contains new access_token and refresh_token

Response includes:

  • New access token (valid for another 30 days)
  • Token expiration time (2,592,000 seconds)
  • New refresh token (use this for next refresh)
  • Token type (Bearer)
  • Authorized scopes

Refresh tokens are single-use. Each successful token refresh invalidates the previous refresh token and returns a new one. If you attempt to reuse a refresh token that has already been exchanged, the server returns an invalid_grant error. Reuse of a refresh token may indicate token compromise, and the authorization server may revoke all tokens associated with that grant as a security measure.

Proactive rotation: Do not wait until tokens expire. Set up a scheduled job to refresh tokens every 25 days to avoid disruption. Always store the new refresh token returned in the response, as the previous one is no longer valid.

Best Practices for Token Rotation

Implement automatic token refresh in your application:

  • Check token expiration before each request
  • Refresh proactively (5 days before expiration)
  • Handle refresh failures gracefully
  • Always store and use the newly returned refresh token after each refresh, as the previous one is immediately invalidated
  • Store new tokens securely

If a request fails with 401:

  1. Attempt to refresh the token
  2. Retry the original request with the new token
  3. If the refresh fails with invalid_grant, the refresh token may have already been used (not just expired). Prompt the user to re-authorize

Deel implements refresh token rotation as defined in RFC 9700 Section 4.14.2:

  • Each refresh token can only be exchanged once for a new access token and refresh token pair
  • After a successful exchange, the previous refresh token becomes inactive immediately
  • If a refresh token is used more than once, the authorization server treats this as a potential compromise signal
  • The server may revoke all tokens associated with the grant to protect the account
  • Always replace your stored refresh token with the new one returned in the refresh response
  • Store tokens encrypted at rest
  • Never expose tokens in URLs or logs
  • Use secure session storage for web apps
  • Implement token revocation on logout

Scopes Reference

Request only the scopes your application needs. Each API endpoint specifies which scopes are required - check the API Reference for details.

Scope patterns: Scopes follow the pattern {resource}:read or {resource}:write (e.g., contracts:read, timesheets:write).

Common scope combinations:

Use CaseRecommended Scopes
Read-only contract viewercontracts:read
Contract management appcontracts:read contracts:write
Timesheet integrationtimesheets:read timesheets:write
Invoice managementinvoice-adjustments:read invoice-adjustments:write
Full workforce managementcontracts:read contracts:write people:read people:write
Accounting integrationaccounting:read contracts:read
Integration TypeRecommended App TypeReason
Contract data readingOrganization AppAccess all org contracts
Contract creationPersonal AppUser-specific permissions
Timesheets managementOrganization AppOrg-wide timesheet access
Invoice adjustmentsOrganization AppManage all org invoices
SCIM APIOrganization AppUser provisioning across org
Accounting integrationOrganization AppFinancial data for entire org
SSO integrationPersonal AppIndividual user authentication

Troubleshooting

Common causes:

  • Invalid or expired access token
  • Missing x-client-id header
  • Token doesn’t have required scopes

Solutions:

  • Refresh the access token
  • Verify both Authorization and x-client-id headers are present
  • Check token scopes match endpoint requirements

Common causes:

  • Token lacks required scopes
  • User doesn’t have permission to access resource
  • App type mismatch (personal app trying to access org resources)

Solutions:

  • Review requested scopes during authorization
  • Use organization app for org-wide resources
  • Verify user has appropriate permissions

When exchanging code or refreshing token:

  • Authorization code already used
  • Refresh token expired
  • Refresh token already used (refresh tokens are single-use)
  • redirect_uri does not match

Solutions:

  • Authorization codes are single-use only
  • Request new authorization if the refresh token has expired
  • Store the new refresh token from each refresh response and discard the old one. If you receive this error unexpectedly, it may indicate token compromise — re-authorize the user
  • Ensure redirect_uri exactly matches the registered URI

Cause:

  • CSRF protection - returned state doesn’t match sent state

Solutions:

  • Store state value in session before redirect
  • Validate returned state matches stored value
  • Generate new state for each authorization request

Security Best Practices

Always verify the state parameter to prevent CSRF attacks

Never expose client secret in client-side code or version control

Redirect URIs must use HTTPS (except localhost for development)

Request only the permissions your app actually needs

Store access and refresh tokens encrypted at rest

Revoke tokens when users disconnect or log out

Next Steps