Authorization flow

Reference for the authorization request, the redirect and the token exchange.

This page describes each step of the OAuth 2 authorization flow in detail. For a guided first run, see the Quickstart.

📘

Conventions

Values in capitals, such as YOUR_CLIENT_ID, are placeholders. URLs on the reserved .example domain are fictitious: https://app.example/oauth/callback stands for your redirect URL, and https://api.fiduciary.example for the api_url received during the flow.

Overview

StepWhoWhat happens
1Your applicationRedirects the user's browser to the Horus authorization page.
2The userLogs in, selects a license, and authorizes your integration.
3HorusRedirects the browser to your redirect URL with a code, your state and the license's api_url.
4Your serverExchanges the code for an access token and a refresh token, on the api_url.
5Your serverCalls the API on the api_url with the access token.

Step 1 — Authorization request

Redirect the user's browser to:

GET https://my-horus.com/fr/api/oauth2/authorize

Use https://my-horus.com/nl/api/oauth2/authorize to display the Horus pages in Dutch.

ParameterRequiredDescription
client_idYesYour integration's client ID.
response_typeYesAlways code.
redirect_uriYesThe URL of the page in your application that receives the result. It must be registered for your integration.
stateRecommendedA random value generated by your application for this request, returned unchanged in step 3. See The state parameter.

Query parameter values must be URL-encoded. Example:

https://my-horus.com/fr/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https%3A%2F%2Fapp.example%2Foauth%2Fcallback&state=Xk9pQ2vL7tRb4mZw

The state parameter

state protects your users against cross-site request forgery (CSRF). Without it, an attacker could make a user's browser land on your callback with an authorization code of the attacker's choosing, and connect the wrong license to your user's account.

  1. Before redirecting, generate a random, unpredictable value (at least 16 random bytes).
  2. Store it in the user's session.
  3. Send it as state.
  4. On the callback, compare the returned state with the stored value. If it is missing or different, reject the request and do not exchange the code.

Use a new value for every authorization request. Never use a constant, a user ID or any other guessable value.

Step 2 — User consent

On the Horus pages, the user:

  1. logs in with their Horus account,
  2. selects the license to connect,
  3. authorizes your integration to access the data available through their account.

Step 3 — Redirect to your application

Access granted

The user's browser is redirected to your redirect_uri with these parameters:

ParameterDescription
codeThe authorization code, valid for 2 minutes.
stateThe value sent in step 1. Verify it.
api_urlThe base URL of the API of the license the user selected.
https://app.example/oauth/callback?code=Hjhfn45k&state=Xk9pQ2vL7tRb4mZw&api_url=https://api.fiduciary.example
🚧

Store the api_url

api_url is specific to the selected license. Store it with the connection and use it for the token exchange and every later request. Never hard-code it.

Access denied

If the user refuses, the browser is redirected to the same URL with an error parameter:

https://app.example/oauth/callback?error=access_denied

Step 4 — Token exchange

From your server, send a POST request to the token endpoint of the received api_url:

POST {api_url}/oauth2/access_token
Content-Type: application/x-www-form-urlencoded
ParameterDescription
client_idYour integration's client ID.
client_secretYour integration's client secret.
codeThe authorization code received in step 3.
grant_typeMust be auth_code.
redirect_uriThe same value as in step 1.
🚧

auth_code, not authorization_code

The Horus API expects grant_type=auth_code, which differs from the value used by many OAuth 2 libraries. If you use such a library, check that you can override it.

curl -X POST "$API_URL/oauth2/access_token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
  --data-urlencode "code=Hjhfn45k" \
  --data-urlencode "grant_type=auth_code" \
  --data-urlencode "redirect_uri=https://app.example/oauth/callback"
// C#: exchange the authorization code on the license's api_url
var response = await http.PostAsync($"{apiUrl}/oauth2/access_token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["client_id"] = clientId,
        ["client_secret"] = clientSecret,
        ["code"] = code,
        ["grant_type"] = "auth_code",
        ["redirect_uri"] = "https://app.example/oauth/callback"
    }));

Response:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "h4vtwzwlfip68zxLCJhbGhsgciO...",
  "refresh_token": "8xLOxBtZp8r2tN3kq..."
}
FieldDescription
token_typeAlways Bearer.
expires_inLifetime of the access token, in seconds (1 hour).
access_tokenThe token to send with every API request.
refresh_tokenSingle-use token to obtain a new token pair. See Token lifecycle.

Step 5 — Call the API

Send the access token in the Authorization header of every request, to the license's api_url:

curl "$API_URL/users.me" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Security checklist

  • The client_secret and the token exchange stay on your server.
  • state is random, stored per user session, and verified on every callback.
  • The authorization code is exchanged immediately (2-minute validity).
  • api_url, access token and refresh token are stored together, per license, and encrypted at rest.
  • Tokens and secrets never appear in logs or URLs.

Did this page help you?