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'
1const axios = require('axios');
2
3async function refreshAccessToken(refreshToken) {
4 const credentials = Buffer.from(
5 `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
6 ).toString('base64');
7
8 const response = await axios.post(
9 'https://app.deel.com/oauth2/tokens',
10 new URLSearchParams({
11 grant_type: 'refresh_token',
12 refresh_token: refreshToken,
13 redirect_uri: 'https://yourapp.com/callback'
14 }),
15 {
16 headers: {
17 'Authorization': `Basic ${credentials}`,
18 'Content-Type': 'application/x-www-form-urlencoded'
19 }
20 }
21 );
22
23 return response.data; // Contains new access_token and refresh_token
24}

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

Proactive rotation: Don’t wait until tokens expire! Set up a scheduled job to refresh tokens every 25 days to avoid disruption. Store the new refresh token for future use.

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
  • Store new tokens securely

If a request fails with 401:

  1. Attempt to refresh the token
  2. Retry the original request with new token
  3. If refresh fails, prompt user to re-authorize
  • 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
  • redirect_uri doesn’t match

Solutions:

  • Authorization codes are single-use only
  • Request new authorization if refresh token expired
  • Ensure redirect_uri exactly matches 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