Authorization Code flow

This is the most common and most secure oauth flow, and we recommend using it whenever applicable. According to the latest OAuth 2.0 Security Best Current Practice document we RECOMMEND clients to use the Authorization Code flow with Proof Key for Code Exchange (PKCE) even for confidential clients

High level overview with actors

  1. Create an OAuth client
    Go to the account section by clicking your user avatar at the bottom of the left bar and then selecting Account -> OAuth -> Register Client.
    Actor: most probably a client developer/admin
  2. Make the authorization request. Actor: client codebase
  3. When prompted for login, sign-in with your user credentials or federated login (SAML).
    Actor: end-user/resource-owner
  4. When prompted for end-user consent overview permissions and provide grant.
    Actor: end-user/resource-owner
  5. Make the access token request.
    Actor: client codebase
  6. Use the issued access_token on the resource server/any applicable place, for example on our public API. Actor: client codebase

Low level steps:

Step 1 - OAuth client creation

You can follow the online documentation to create an OAuth client in Timeline. Mind, that for security reasons every resource-owner who will provide the OAuth grant needs to be part of the Account in-which this client is being created. This rule is necessary because every cloud-based Timeline installment is multi-tenant.

When the client has been created you will be provided with sensitive credentials. The Client ID and Client secret are opaque keys which you need to persist into your system and store them securely. The easiest way to do that is to create a UI page into you application where the integrator (who creates the OAuth client in Timeline) can copy&paste these values into a form. Needless to say such application should operate over TLS so no men-in-the-middle (or similar) attacks can sniff these credentials. When your application will later initiate the OAuth Authorization request you will need some information. These are

  • client_id
  • client_secret
  • issuer
  • authorize endpoint path

You got the id & secret from the created client, and you can get the rest by either parsing the metadata document, or copy&pasting that information to the form by hand. Having said that, we recommend, in the form you either have a field for the metadata URI and your backend system will parse the metadata and extract the issuer and other information out from it, or create multiple inputs for the respective information in the form.

You can find the metadata document using the path below, depending on your instance:
https://{your.timeline.instance}.com/api/auth/oauth/.well-known/oauth-authorization-server

Step 2 - Authorization request

Depending on your business requirements you will need to fetch some information about a Timeline user at some point in their journey in your application. When you need to call Timeline's public API you have to check if the actual end-user have a valid accessToken in your system or not. If there is no available accessToken for your user, you have to trigger the OAuth Authorization Code flow. You do that by making the authorization request.

Specification: https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 If the PKCE addition has been enabled on the oauth client you also have to provide PKCE properties. Specification: https://datatracker.ietf.org/doc/html/rfc7636#section-4

In short, you will have to generate a code_verifier, using that you will generate a code_challange and a code_challange_method. If you can, generate these on the backend. You will have to send the code_challange and the code_challange_method with the request while the code_verifier has to be saved to a store (im memory store, database, ...etc).

You will have to redirect your end-user's browser agent to the constructed authorize URI. Let's see how to construct it.

Base URL: <ISSUER>/api/auth/authorize

Query string params:

response_type - REQUIRED - code

client_id - REQUIRED - read it out from your database/store. In step 1, you saved this by a form post

redirect_uri - REQUIRED - in step 1, when creating the oauth client in Timeline, you had to provide a "Redirect URI" which is (probably) an endpoint URI of your backend. Commonly something like <YOUR_SERVER_BASE_URL>/api/oauth/callback or similar. It has to be a 1:1 match.

scope - OPTIONAL - when creating the oauth client you had to select a few (or every) scopes according to your needs. Those define the absolute set of scopes your client might be able to use. Here, with every authorize request you have the possibility to "loosen" the scopes which are needed for this specific use-case of yours. That is, with this parameter you can send just a subset of the scopes selected upon client creation. If left blank, you will request an accessToken with all the scopes selected upon client creation. Mind, that you should only request a minimal set of scopes which are absolutely necessary for your current use-case as requesting access for scopes which are unrelated for your use-case might result in a rejected grant by your end-user. For example, if your current use-case is to list the end-users projects in Timeline you only need the project:read scope and if you'd request the repository:read scope as well, your end-user might find it fraudulent on the consent screen to provide permission for her repositories when all is required are her projects.

state - OPTIONAL - since the authorization code flow is a redirection based flow you user will leave your application, and she will be redirected to Timeline for authentication and to provide grant. At the end she will be redirected back to your "Redirect URI". There, you will have the option to "resume" the user to continue where she "left off". But you won't necessarily know where the user left-off her session. You can use this parameter to encode any information which could help you to resume the user. As per specification whatever you send in this parameter will be sent back un-altered with the response

login_hint - OPTIONAL - this has been borrowed from the OIDC specification. If you are able to identify your end-user prior to the authorization request you can send the user's email address (make sure to URL encode it) with this parameter, and optionally it might ease your end-user's experience at Timeline. For example, any federated authentication can be improved knowing the user beforehand of authentication.

nonce - OPTIONAL - when doing an OpenID Connect flow you can send a nonce parameter which is very similar to the "state" parameter, except, this one will be included unmodified in the id_token while the state value will be sent back as a query parameter

With all that, you can construct the URI

const params = new URLSearchParams();
params.set('response_type', 'code');
params.set('redirect_uri', 'https://example.com/api/oauth/callback');
params.set('scope', 'project:read project:write');
params.set('client_id', 'example-client-id');
params.set('state', 'example-state');
if (isPKCEEnabled) {
    params.set('code_challenge', 'example-code-challenge');
    params.set('code_challenge_method', 'S256');
}
if (isNonceEnabled) {
    params.set('nonce', 'example-nonce');
}
window.location.assign(`${authorizationEndpointUrl}?${params.toString()}`);
    

Step 3 - End-user authentication & grant

At this point your end-user's (resource owner) browser will be redirected to the "authorize" endpoint. As per specification Timeline will

  • Check if the resource owner has a valid session with Timeline (a.k.a is the user logged in to Timeline?). If not, we will challange the user for authentication
  • If authenticated, we will redirect the resource owner's browser to the consent page to collect resource owner grant

Note. the consent page is a UI page where we list the requested resources for which your application wants to get access (grant) from the resource owner. The resource owner can overview the requested permissions, and she can deny some, or all of them. If she denies only some of them, you will still get the grant but when obtaining the accessToken on the token endpoint the issued accessToken Won't have all the requested scopes only those which the resource owner allowed. This is why you should only request a minimal set of scopes with the authorize request.

If the resource owner clicks on the "Allow" button on the consent screen she provided "grant" and we will redirect her browser agent back to "Redirect URI" endpoint with a query parameter of "code". The value is an opaque string without holding any information to you. This string is the "grant" and you can exchange it for an accessToken on the token endpoint. Mind, that you have 1 minute to exchange the code for an accessToken. For example, we do a HTTP 302 <redirect_uri>?code=aa-bbb-qwerty

Note. all of these redirects were front channel communication over a browser agent. When we provide the grant by a redirect to your "Redirect URI" endpoint you have to exchange the grant for an accessToken on the back channel. That is, you have to call the token endpoint with an XHR call and NOT with a browser redirect, or in other words, exchanging the code to an accessToken should NOT be visible to the end-user.

On the possible errors you can get, check the Troubleshooting section.

Step 4 - Exchanging the grant

Parse the code value, this is called the "grant". You have to call the token endpoint and send this value to obtain an access_token. We suggest you do this on your server side component. Mind, that if you made the authorize request with PKCE you will have to send the saved code_verifier you used to generate the code_challange value.

const params = new URLSearchParams();
params.set('grant_type', 'authorization_code');
params.set('redirect_uri', 'https://example.com/api/oauth/callback');
params.set('code', 'aa-bbb-qwerty');
if (isPKCEEnabled) {
    params.set('code_verifier', '<your code verifier from the previous steps>');
}
let auth;
if (isPublicClient) {
    // Public clients must send client_id
    params.set('client_id', 'example-client-id');
} else {
    // Confidential clients must do client authentication
    auth = {
        username: clientID,
        password: clientSecret
    };
}
const { data: tokenRecord } = await axios.post(tokenEndpointUrl, params.toString(), {
    auth,
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
});
    

On the possible errors you can get, check the Troubleshooting section.

Step 5 - Using the issued accessToken

In the previous step, we got the tokenRecord as response. Let's investigate

{
  "access_token": "b2F1dGg6MzU1Nzc2MWQtMGIxMi00MDA5LWEwYmEtNTljMWQ3ZDc4OWI3OjI0MzkxZTMxLWE5ZjQtNDNkOC05Y2FhLWYzMWMzMTJkYzhiZQ==",
  "token_type": "bearer",
  "expires_at": "2023-09-07T11:51:02.286Z",
  "expires_in": 86400000,
  "scope": "project:read project:write repository:read repository:write zoneinfo",
}
    

These are the bare minimum fields which will be included in tokenRecord. Depending on the OAuth client you created additional fields might be present. If you enabled the "Refresh Token" flow on the client additional fields will appear

{
  "access_token": "b2F1dGg6MzU1Nzc2MWQtMGIxMi00MDA5LWEwYmEtNTljMWQ3ZDc4OWI3OjI0MzkxZTMxLWE5ZjQtNDNkOC05Y2FhLWYzMWMzMTJkYzhiZQ==",
  "token_type": "bearer",
  "expires_at": "2023-09-07T11:51:02.286Z",
  "expires_in": 86400000,
  "scope": "project:read project:write repository:read repository:write zoneinfo",
  "refresh_token": "ZWUwMDc0NmMtMDJkZi00YzE4LWEyMjUtNzNmZjQxNTQ3OTMzOmNlOTg0MDZlLTFjOWQtNGU2NC04M2UyLTE0MjQyYmZjNTQyZQ==",
  "refresh_token_expires_at": "2023-09-13T11:51:02.418Z",
  "refresh_token_expires_in": 604800000
}
    

If you also enable "OpenID-Connect" then an id_token will be also present with additional OIDC claims

{
  "access_token": "b2F1dGg6MzU1Nzc2MWQtMGIxMi00MDA5LWEwYmEtNTljMWQ3ZDc4OWI3OjI0MzkxZTMxLWE5ZjQtNDNkOC05Y2FhLWYzMWMzMTJkYzhiZQ==",
  "token_type": "bearer",
  "expires_at": "2023-09-07T11:51:02.286Z",
  "expires_in": 86400000,
  "scope": "email family_name given_name locale name openid phone_number project:read project:write repository:read repository:write zoneinfo",
  "refresh_token": "ZWUwMDc0NmMtMDJkZi00YzE4LWEyMjUtNzNmZjQxNTQ3OTMzOmNlOTg0MDZlLTFjOWQtNGU2NC04M2UyLTE0MjQyYmZjNTQyZQ==",
  "refresh_token_expires_at": "2023-09-13T11:51:02.418Z",
  "refresh_token_expires_in": 604800000,
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJmYW1pbHlfbmFtZSI6IktsaW5nIiwiZ2l2ZW5fbmFtZSI6IkF0dGlsYSIsImxvY2FsZSI6ImVuLVVTIiwibmFtZSI6IkF0dGlsYSBLbGluZyIsInBob25lX251bWJlciI6IiszNjIwMzk3OTM4NyIsInN1YiI6IjEyIiwiaWF0IjoxNjk0MDAxMDYyLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDoxMzM4L2FwaS9hdXRoIiwiYXVkIjoiZDBmZThhMmJhNmZmZGI3ZTIxNTYxYjEzMzg5MTViMmQiLCJleHAiOjE2OTQwODc0NjI1MjZ9.Wkgd9FFMpKOfysxzmYSdfQJDzMN9D17k6i5P8aZgCBDDmq_f1L0w5tehe2JV2AEhi3gsNZya5e6eUkjZWWmS6MaCECi9uL8BbR2xHNtErteDQZiK6rx1LAIshrBAFb_U3ENGtVOoUxFcxGm-hE2UER6oXKMNRS0EbXymxY_PzmhvShy9KRDSpO08VA7giIzvV6zi-w6fHnvJ7ZlGSSyMvDpmcP-g3x_SJ_An6v4nlu-0TEcGdb9VQhyN1VNF1EpdCuBdoPBJ35UlXQucv3S8y4dlfOYY2QJsGXUqv6Cumku3t-jUVvmbhyPV2-XBv6BEmMwufxTuNX_mVwVdqj27sKkDNCQEDZVAUOYwMDaYLiPTPr33YCOuN9tJ4lUzS_8M-cr1-vX88FerKOoVIKiMVgu7xcYzHnUOM_m4pNZOV9v4XRGI9lt-jmcZyaGHy2sTTs4SKZAMVAkYWFVjrGoTNwjwKzSavPUfIIIucARsIGwlQdY6aHLmvSzCDmxjTWLU4nZxhvsi3R3caJyHokT8AcHuKrCNVpCd4EHp1rtQDAPTP6nMM6y62aSe3jQcUpSSnDpVFTw-4RCEVn_aQWO6TtS1Z3UQZyVO1vlYGu7QSvLjrSKSnohECm4GWw-lC-VppQhBnFQFKHeWPqNw6rBKDmN6vJ__d6iphbDLUy_5iE4"
}
    

Note. please be mindful of the scope property of the response, it is possible that the access_token doesn't contain all the scopes which you requested because the resource owner denied some of them on the consent screen. With the scope property you can check what scopes the resource owner allowed.

Now you just have to send the access_token value as an Authorization: Bearer token alongside of the public API requests to be authenticated.

Intropsect endpoint

If any time you have to check if an access_token is valid you can use the introspect token endpoint. Specification: https://datatracker.ietf.org/doc/html/rfc7662.

URL: <ISSUER>/api/auth/introspect

You have to make a POST request with Content-Type = application/x-www-form-urlencoded with fields:

token - REQUIRED - an access_token

token_type_hint - OPTIONAL - access_token or refresh_token

You also have to do Client Authentication which means you have to send your client's client_id & client_secret as Authentication: Basic header.

Important. To prevent token scanning attacks, the endpoint MUST also require some form of authorization to access this endpoint, such as client authentication as described in OAuth 2.0 [RFC6749]

If the token is no longer active you'd get

{
  "active": false
}
    

otherwise

{
    "active": true,
    "exp": 1694174721,
    "token_type": "access_token",
    "username": "attila.kling@abbyy.com",
    "sub": 2,
    "client_id": "d0fe8a2ba6ffdb7e21561b1338915b2d",
    "scope": "email family_name given_name locale name openid phone_number project:read project:write repository:read repository:write zoneinfo"
}
    

Troubleshooting

Per the OAuth specification upon the Authorize request

Important. If the request fails due to a missing, invalid, or mismatching redirection URI, or if the client identifier is missing or invalid, the authorization server SHOULD inform the resource owner of the error and MUST NOT automatically redirect the user-agent to the invalid redirection URI.

The resource owner will see an error page explaining the issue, and they won't be redirected to your "Redirect URI" endpoint. In other error cases we will redirect the user to "Redirect URI" with the error, error_description and optionally error_uri, error_code and state query parameters. In case of the Public API, you might get an error response with the body of

{
  "error": "some_error",
  "error_description": "some_error_description",
  "error_params": {},
  "error_uri": "some_error_uri"
}
    

Let's see what kind of errors you can get

error error_description notes
SERVER_ERROR The authorization server encountered an unexpected condition that prevented it from fulfilling the request
INVALID_SCOPE Invalid scope <scope(s)> If you provide an invalid, non-existing scope with the authorize request
INVALID_CLIENT The client is not authorized to request an authorization code using this method If you provide an invalid client_id upon client authentication, or if the client_secret does not match
INVALID_CLIENT The client is not registered
INVALID_CLIENT A public client should not be able to do client authentication with the authorization server You shouldn't try to do client authentication with a public client. Public clients doesn't have a client_secret
INVALID_USER The authenticated user doesn't have access for the specified oauth client When the resource owner autheticates she is only allowed to login with a user who is part of the same account in which the oauth client has been created
UNAUTHORIZED_CLIENT Public clients are forbidden from using authentication method: ${authorizationMethod} A public client attempted to do client authentication with the Basic or Client Password scheme
UNAUTHORIZED_CLIENT Confidential clients MUST authenticate with the authorization server
UNAUTHORIZED_CLIENT The authenticated client is not authorized to use this grant type Make sure to use only those grant types which are enabled on the oauth client
UNSUPPORTED_GRANT_TYPE The authorization grant type is not supported by the authorization server We only support authorization_code, refresh_token and client_credentials
UNSUPPORTED_RESPONSE_TYPE Unsupported response_type We only support code response type
INVALID_GRANT Invalid or expired authorization code! https://tools.ietf.org/html/rfc6749#section-10.5
INVALID_GRANT Code challenge failed!
INVALID_GRANT ClientID or RedirectURI does not match with the issued authorization code! Security measure to not let a code to be exchanged if the code was issued for a different oauth client
INVALID_REQUEST URI must not contain a fragment https://www.rfc-editor.org/rfc/rfc6749#section-3.1
INVALID_REQUEST The use of TLS is mandatory https://www.rfc-editor.org/rfc/rfc6749#section-3.1
INVALID_REQUEST Must use HTTP POST or GET https://www.rfc-editor.org/rfc/rfc6749#section-3.1
INVALID_REQUEST Unsupported code challenge method Supported: sha256, s256, plain
INVALID_REQUEST Redirect URI mismatch! You have to send an exact match with the provided value on OAuth client registration
INVALID_REQUEST Duplicated query parameter(s):
INVALID_REQUEST Missing required parameter(s):
INVALID_REQUEST Invalid request parameter(s):
ACCESS_DENIED The resource owner or authorization server denied the request When the resource owner clicked on "Deny" on the consent screen denoting that she doesn't want to allow access
INVALID_ACCESS_TOKEN Invalid Access Token! For the userinfo endpoint if a not existing or invalid access_token has been sent
EXPIRED_ACCESS_TOKEN Access Token has expired!
INVALID_REFRESH_TOKEN Invalid Refresh Token!
EXPIRED_REFRESH_TOKEN Refresh Token has expired!
INSUFFICIENT_SCOPE Insufficient scope for requested resource!
UNSUPPORTED_ACCESS_TOKEN The provided access token, granted by the client_credentials grant type, can not be used to fulfill the request In the public API we don't support the client_credentials grant

TTLs
Entity TTL Notes
access_token 1 day
refresh_token 1 week
refresh_token_abs 1 month When refreshing an access_token with a refresh_token, we will provide a new refresh_token with a new TTL but there is an absolute time when you have to do the OAuth flow again
id_token 1 day
authorization_code 1 min

22.02.2024 17:28:05

Usage of Cookies. In order to optimize the website functionality and improve your online experience ABBYY uses cookies. You agree to the usage of cookies when you continue using this site. Further details can be found in our Privacy Notice.