Authentication

Table of contents
  1. Overview
  2. Building an authorization endpoint url
  3. Understanding OAuth2 scope
  4. Handling authorization callbacks
  5. Exchanging authorization code with access token
  6. Making API requests
    1. Making API requests on behalf of a user
    2. Making API requests as an app installed on a community
  7. Refreshing access tokens
  8. Introspecting tokens
  9. Handling errors
    1. /authorize endpoint
    2. /token endpoint

Overview

To authenticate API provided by canD Modules, canD Apps should obtain an access token by asking users for granting permissions it needs via the OAuth2 protocol and include it in API requests. canD supports the following OAuth2 grant types:

  1. Authorization Code Flow: This grant type is suitable for confidential OAuth2 clients that can securely manage secrets, such as web applications with a backend. You should generate the client secret in the App Settings page in order to enable it.
  2. Authorization Code with Proof Key for Code Exchange (PKCE) Flow: Public OAuth2 clients that cannot securely manage secrets, including web applications without a backend, mobile apps, desktop apps, etc., should use this grant type. For enhanced security, confidential OAuth2 clients can also use PKCE in conjunction with client secrets.

Building an authorization endpoint url

An authorization endpoint url is a url for users to review and authorize permissions requested by the canD App. In terms of UX, when the user clicks the authorization endpoint url, they are redirected from the canD App to https://canpass.me and after completing authorization they are redirected back to the canD App with the authorization result.

The url should have https://canpass.me/oauth2/authorize origin and it can have the following query parameters:

  1. resposne_type: It should be set to code.
  2. client_id: The OAuth2 client id value of the canD App.
  3. scope: A space separated value of permissions that indicates what actions the canD app can perform on which resources of the user or the community. Try to minimize scopes to reduce the damage if the access token is leaked.
  4. redirect_uri: A callback uri to be redirected after processing the authorization request. It should be registered in the redirect uris of the canD App.
  5. state: A random string to prevent Cross-Site Request Forgery (CSRF). As it’s sent back as a query parameter of redirect_uri, you can utilize it to store and load the application state across authorization requests. Generate a unique value per authorization request and store it in the app so that when redirect_uri is called it can validate if the attached state parameter exists in the app and reject the request if it doesn’t.
  6. code_challenge: It’s a base64 url encoded SHA256 hash of the code_verifier which is cryptographically random value and to be included in the token endpoint body to validate the code_challenge included in the authorization endpoint url. The code_verifier and code_challenge should be newly generated per authorization request. Refer to the below code snippet for the details. You can skip this parameter if you prefer to use the client secret only.
  7. code_challenge_method: It’s needed only if the code_challenge parameter is set and should be set to S256.
  8. community_id: The community identifier of resources to access and mutate. Sign in and sign up pages and login options are customized according to the community’s settings.
  9. action: A page to show if the user has no session so needs to sign in or sign up. It should be one of signin and signup. If it’s not given, it’s set to signin.

Here is an example to build an authorization endpoint url in JavaScript. Open the browser’s developer console and execute it.

// Creates a code verifier by generating a base64 encoded 32 random bytes string
const createCodeVerifier = () => btoa(String.fromCharCode(...new Uint8Array(crypto.getRandomValues(new Uint8Array(32)).buffer)));
// Converts a code verifier to a code challenge by creating a sha-256 hash from a given code verifier and encodes it into base64 url
const createCodeChallenge = async (verifier) => btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(verifier))))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');

const url = new URL('https://canpass.me/oauth2/authorize');
const codeVerifier = createCodeVerifier();
const codeChallenge = await createCodeChallenge(codeVerifier);
const communityId = 'COMMUNITY_ID';
const nonce = `${Math.random()}`;
const state = {nonce, key: 'value'};
const scopes = ['member:MOIM:content:read', 'member:MOIM:content:write'];
const searchParams = {
  response_type: 'code',
  action: 'signin',
  client_id: 'CLIENT_ID',
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
  redirect_uri: 'https://example.com/callback/v2/oauth2/canpass',
  community_id: communityId,
  scope: scopes.join(' '),
  state: JSON.stringify(state),
};

// Encodes query parameters automatically
url.search = new URLSearchParams(searchParams).toString();

console.log('Authorization endpoint url:', url.toString());
console.log('code verifier:', codeVerifier);
console.log('code challenge:', codeChallenge);

The above code will log a result similar to the following:

Authorization endpoint url: https://canpass.me/oauth2/authorize?response_type=code&action=signin&client_id=CLIENT_ID&code_challenge=IAqnRiS06TqQD20heXIm1TGiQlV_yQsebpRdhU4zeeo&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback%2Fv2%2Foauth2%2Fcanpass&community_id=COMMUNITY_ID&scope=member%3AMOIM%3Acontent%3Aread+member%3AMOIM%3Acontent%3Awrite&state=%7B%22nonce%22%3A%220.5441891057972421%22%2C%22key%22%3A%22value%22%7D
code verifier: iAjKUyckyYjy9eavouAglkGVocCDeJvWCC5gQMMJGWQ=
code challenge: IAqnRiS06TqQD20heXIm1TGiQlV_yQsebpRdhU4zeeo

If you want to use the client secret only instead of the code challenge and verifier, just opt out code_challenge entry and code_challenge_method entry in searchParams object literal. Then the url should be similar to the following:

Authorization endpoint url: https://canpass.me/oauth2/authorize?response_type=code&action=signin&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback%2Fv2%2Foauth2%2Fcanpass&community_id=COMMUNITY_ID&scope=member%3AMOIM%3Acontent%3Aread+member%3AMOIM%3Acontent%3Awrite&state=%7B%22nonce%22%3A%220.5685220622298841%22%2C%22key%22%3A%22value%22%7D

When users visit the authorization endpoint URL, they will first be prompted to sign in or sign up, in case they don’t already have a session. Following this, if the permissions requested by the canD app haven’t been previously granted, users will be asked to authorize these permissions.

Once the authorization process is finished, the authorization server, https://canpass.me, appends the result of the authorization process as query parameters to the redirect_uri attached to the authorization endpoint url and responds a 302 response setting the url as the location header.

Understanding OAuth2 scope

A scope item in canD has the following format: ${subject}:${module}:${resource}:${action}

  • Subject: The actor of API request. It should be one of user who is a user not bound to any community, member who is a user joined a specific community, bot that is an app isntalled on a specific community, and webhook that is the canD system to call canD App when webhook events occur. Note that user and member is the user that the canD App is asking for the authorization while bot is the canD app itself.
  • module: The identifier of canD Module.
  • Resource: The name of resource to access. It depends on the canD Module.
  • Action: The name of action that can be done on the resource. It depends on the canD Module. It can be optional. If it doesn’t exist, the last trailing comma should not exist as well like webhook:MOIM:product.

In summary, a scope item stands for a permission to perform an action on a resource within a module as a subject. For example, member:MOIM:members:read means that a permission to read a channel’s members of MOIM as a member.

Note that which user granted the permissions does matter as well. If the user has no access to some channel, the canD App won’t be able to access that channel too.

Refer to each canD Module’s reference for the full list of available scope item.

Handling authorization callbacks

After the authorization process is completed, the browser redirects to the redirect_uri specified in the authorization endpoint url, appending query parameters that represent the outcome of the authorization request. A successful authorization request’s redirect_uri has a code which is the authorization code to exchange it with the access token and state parameter which was included in the original authorization endpoint url as follows:

HTTP/1.1 302 Found
Location: https://example.com/callback/canpass?code=ae697fd0127ef45afca4aad1046ee4990616fdd8&state=%7B%22nonce%22%3A%220.6294249836910808%22%2C%22key%22%3A%22value%22%7D

You can check it in action with the network panel of the browser’s developer console. To decode the url, execute the following code snippet through the browser’s developer console:

const url = new URL('https://example.com/callback/canpass?code=ae697fd0127ef45afca4aad1046ee4990616fdd8&state=%7B%22nonce%22%3A%220.6294249836910808%22%2C%22key%22%3A%22value%22%7D');

url.searchParams.forEach((value, key) => console.log(`${key}: ${value}`));

It should print the following result.

code: ae697fd0127ef45afca4aad1046ee4990616fdd8
state: {"nonce":"0.6294249836910808","key":"value"}

On the receipt of the successful callback, the canD app should validate it by checking if the given state value exists in the app and only if it does you should call the token endpoint to exchange the code with the access token. If the state is not found in the canD app, it indicates the authorization callback may be forged.

In case of an error, error and error_description query parameters are given instead of code and state. See /authorize endpoint section of Handling errors for how to handle such errors.

Exchanging authorization code with access token

Now you have the authorization code, you can exchange it with the access token through the token endpoint. If you prefer to do that on the browser directly, execute the following code snippet replacing the parameters properly:

const client_id = 'CLIENT_ID';
const code = 'ae697fd0127ef45afca4aad1046ee4990616fdd8';
const code_verifier = 'iAjKUyckyYjy9eavouAglkGVocCDeJvWCC5gQMMJGWQ=';
const res = await fetch('https://canpass.me/oauth2/token', {
  method: 'POST',
  headers: {'content-type': 'application/x-www-form-urlencoded'},
  body: new URLSearchParams({grant_type: 'authorization_code', client_id, code, code_verifier})
});
const body = await res.json();

console.log('Response status', res.status);
console.log('Response headers', JSON.stringify(Object.fromEntries(res.headers), null, 2));
console.log('Response body', JSON.stringify(body, null, 2));

The above code will log a result similar to the following:

Response status 200
Response headers {
  "cache-control": "no-store",
  "content-length": "527",
  "content-type": "application/json; charset=utf-8",
  "pragma": "no-cache"
}
Response body {
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJWWE5sY2pwaU5HRmpNRE13T1Mwd05XSXdMVFF6T1RndFltWXlNQzAzWm1SbU9EUXdZek0yTm1VPSIsImNhbkFjY291bnQiOiIiLCJ3ZWIzQWRkcmVzcyI6bnVsbCwiaWF0IjoxNzAxMTcxMzc0LCJleHAiOjE3MDExNzQ5NzR9.7HSppf3S3wAVYZ1lZQvkgGoG4eQJGU3om73TpgXzDVo",
  "token_type": "Bearer",
  "expires_in": 3599,
  "refresh_token": "e55920243879cbb0356c2c2ccd62e6d5a0d23977",
  "scope": "user:email:read member:channels:read bot:thread:write",
  "user_id": "VXNlcjpiNGFjMDMwOS0wNWIwLTQzOTgtYmYyMC03ZmRmODQwYzM2NmU=",
  "community_id": "G0W72D2X7V",
  "bot_access_token": "f2d8a3e7c519223ee71d0bd9869a0464ace2f3a4"
}

The equivalent curl command for the above code should look like this:

curl -X POST https://canpass.me/oauth2/token \
  -H "content-type: application/x-www-form-urlencoded" \
  -d "\
  grant_type=authorization_code&\
  client_id=CLIENT_ID&\
  code=ae697fd0127ef45afca4aad1046ee4990616fdd8&\
  code_verifier=iAjKUyckyYjy9eavouAglkGVocCDeJvWCC5gQMMJGWQ="

The token endpoint request to exchange the authorization code with the access token should meet the followings:

  1. POST method
  2. https://canpass.me/oauth2/token origin
  3. application/x-www-form-urlencoded content-type request header
  4. The body should include the following items:
    1. grant_type: authorization_code
    2. client_id: The canD app’s OAuth2 client id
    3. code: The code appended to the redirect_uri
    4. code_verifier: the code verifier that was used to generate the code challenge. It’s needed only if the code challenge was included in the original authorization endpoint url.
    5. client_secret: The canD app’s OAuth2 client secret. It’s needed only if the client has the client secret.

The above token endpoint request’s response is JSON and has the following items:

  1. access_token: The access token to authenticate API on behalf of a user who joined the community.
  2. token_type: Bearer. It’s constant.
  3. expires_in: The access token expiration date in seconds. It’s set to 1 hour, 3600s.
  4. refresh_token: The refresh token to obtain a new access token with reset expiration date. The expiration date is 90 days.
  5. scope: The authorized scope. Note that it may not include all scope items that was included in the authorization endpoint url.
  6. user_id: The user id who authorized the scope.
  7. community_id: The community id that was included in the authorization endpoint url.
  8. bot_access_token: The access token for the bot to make API requests as an app’s installed on the community. It’s given only if the bot scope that starts with bot: is included in the scope. Unlike the access token and refresh token it does not expire and its value remains consistent across multiple authorizations.

If there was an error, the response has other than 200 status code and a JSON body with error and error_description items. See /token endpoint section of Handling errors for how to handle such errors.

Making API requests

Every canD Module functions as a resource server in the OAuth2 framework and employs Bearer Authentication to authenticate API requests. To comply with this, simply add an Authorization request header with the value set to Bearer TOKEN where TOKEN is the access token or bot access token.

However, note that besides the authentication scheme, others like HTTP method and request/response headers and body format depend on each canD Module. Refer to each canD Module’s doc for the details.

Making API requests on behalf of a user

To perform API requests on a user’s behalf, the access token issued through user authorization is needed. The access token allows the canD app to make API requests that perform actions specified in the token’s scope as a user who joined the community associated with the token.

Here’s a curl example to retrieve the information of the member who is a user joined the specified community in Moim canD module:

> curl -X GET https://api.cand.xyz/me \
  -H "content-type: application/json" \
  -H 'x-can-community-id: COMMUNITY_ID' \
  -H 'Authorization: Bearer ACCESS_TOKEN'
  
{
  "id": "UWB0O7URM",
  "name": "DH",
  "type": "USER",
  "timezone": "America/New_York",
  "createdAt": 1701241331527,
  "updatedAt": 1701241390085,
  "locale": "en-US",
  "currency": "USD",
  "status": 0
}

Making API requests as an app installed on a community

If the authorization endpoint url contains bot scope items that starts with bot:, the bot access token is given together with the access token. This bot access token allows canD app to make API requests as a bot, an app installed on a community. Note that it’s not the user who installed the canD app. The bot access token has bot scope items only specified in the authorization endpoint url.

Here’s a curl example to retrieve the information of the bot installed on the specified community in Moim canD module:

> curl -X GET https://api.cand.xyz/me \
  -H "content-type: application/json" \
  -H 'x-can-community-id: COMMUNITY_ID' \
  -H 'Authorization: Bearer BOT_ACCESS_TOKEN'

{
  "id": "BDABP7KJE",
  "name": "Bot",
  "type": "BOT",
  "timezone": "America/New_York",
  "createdAt": 1701404651759,
  "updatedAt": 1701404651759,
  "locale": "en",
  "currency": "USD",
  "status": 0
}                                                                                                                                         

Refreshing access tokens

If it’s enough to identify the user one-time e.g. ‘Continue as X member’ feature, the canD app wouldn’t need to manage tokens. However, if it needs to retrieve users’ access tokens later and want to avoid asking users for authorization every time, store the refresh token in the canD app and issue a new access token when necessary. This way, you can extend the token expiration to maximum 90 days unless the user revokes the token explicitly.

To try out to refresh the token on the browser, open the developer tools and execute the following code snippet:

const client_id = 'CLIENT_ID';
const refresh_token = 'REFRESH_TOKEN';
const res = await fetch('https://canpass.me/oauth2/token', {
  method: 'POST',
  headers: {'content-type': 'application/x-www-form-urlencoded'},
  body: new URLSearchParams({grant_type: 'refresh_token', client_id, refresh_token})
});
const body = await res.json();

console.log('Response status', res.status);
console.log('Response headers', JSON.stringify(Object.fromEntries(res.headers), null, 2));
console.log('Response body', JSON.stringify(body, null, 2));

It should log a similar to the followings:

Response status 200
Response headers {
  "cache-control": "no-store",
  "content-length": "527",
  "content-type": "application/json; charset=utf-8",
  "pragma": "no-cache"
}
Response body {
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJWWE5sY2pwaE56UXhNbVF5TVMwM09HWXpMVFJoTURBdE9XSTJaQzFsT1RVMlpERTNaREkwWWpRPSIsImNhbkFjY291bnQiOiIiLCJ3ZWIzQWRkcmVzcyI6bnVsbCwiaWF0IjoxNzAzMjMyOTc3LCJleHAiOjE3MDMyMzY1Nzd9.F4b6BOqwHqXI8Te0HArvp9tPY2_yQ9SbRO_Z7RfZdjw",
  "token_type": "Bearer",
  "expires_in": 3599,
  "scope": "user:email:read member:channels:read",
  "user_id": "VXNlcjphNzQxMmQyMS03OGYzLTRhMDAtOWI2ZC1lOTU2ZDE3ZDI0YjQ=",
  "community_id": "G0W72D2X7V",
  "refresh_token": "a9490ad6feea5a3358f15124e0715643b1ac85b6"
}

The equivalent curl command for the above code should look like this:

curl -X POST https://canpass.me/oauth2/token \
  -H "content-type: application/x-www-form-urlencoded" \
  -d 'grant_type=refresh_token&\
  client_id=CLIENT_ID&\
  refresh_token=REFRESH_TOKEN'

The token endpoint request to refresh the access token should meet the followings:

  1. POST method
  2. https://canpass.me/oauth2/token origin
  3. application/x-www-form-urlencoded content-type request header
  4. The body should include the following items:
    1. grant_type: refresh_token
    2. client_id: The canD app’s OAuth2 client id
    3. refresh_token: The refresh token associated the access token to refresh
    4. client_secret: The canD app’s OAuth2 client secret. It’s needed only if the client has the client secret.

The above token endpoint request’s response is JSON and has the following items:

  1. access_token: The new access token.
  2. token_type: Bearer. It’s constant.
  3. expires_in: The new access token expiration date in seconds. It’s set to 1 hour, 3600s.
  4. scope: The original access token’s scope.
  5. user_id: The user id who authorized the scope.
  6. community_id: The community id that was included in the authorization endpoint url.
  7. refresh_token: The original refresh token used to issue a new access token. The expiration date isn’t affected.

If the refresh token whose expiration is 90 days itself is expired, the user revoked the authorization, or other scopes which are not bound to the refresh token is needed, you have no option but to ask the user for authorization and obtain the new access token and refresh token.

In the wild, canD Apps would have a logic that if an API request fails due to the expired access token, it refreshes the access token, and then if it succeeds, try again the original request with the new access token, and if refreshing the access token fails due to the expired refresh token, it asks the user for the authorization again.

Note that it applies to access tokens to authenticate API on behalf of users only. The bot access tokens don’t expire unless the user uninstalls the app from the community.

Introspecting tokens

For debugging purpose to introspect tokens, you can utilize the token introspection endpoint. If the token is available, you can try out the following code snippet on the browser’s developer console:

const token = 'TOKEN';
const res = await fetch('https://canpass.me/oauth2/introspect', {
  method: 'POST',
  headers: {'content-type': 'application/x-www-form-urlencoded'},
  body: new URLSearchParams({token})
});
const body = await res.json();

console.log('Response status', res.status);
console.log('Response headers', JSON.stringify(Object.fromEntries(res.headers), null, 2));
console.log('Response body', JSON.stringify(body, null, 2));

It should log a similar to the followings:

Response status 200
Response headers {
  "content-length": "256",
  "content-type": "application/json"
}
Response body {
  "active": true,
  "token_type": "access_token",
  "scope": "user:email:read member:MOIM:members:read bot:MOIM:members:read",
  "subject_type": "USER",
  "subject_id": "VXNlcjozYmRjYjQ3Ny0yYzM4LTRhZGUtYjUyYi0yZDZmNWNjMTBiYzY=",
  "community_id": "G0ZL30BF6H",
  "exp": 1703143100
}

The equivalent curl command for the above code should look like this:

curl -X POST https://canpass.me/oauth2/introspect \
  -H "content-type: application/x-www-form-urlencoded" \
  -d 'token=TOKEN'

The token introspection endpoint request should meet the followings:

  1. POST method
  2. https://canpass.me/oauth2/introspect origin
  3. application/x-www-form-urlencoded content-type request header
  4. The body should include the following items:
    1. token: The token to introspect. It can be access token, refresh token, and bot access token.
    2. token_type_hint: The optional hint to infer the token type. It should be one of access_token, refresh_token, and bot_access_token.

The above token endpoint request’s response is JSON and has the following items:

  1. active: Whether the token is active or not. When it’s false, other properties are omitted.
  2. token_type: The given token’s type. It should be one of access_token, refresh_token, and bot_access_token.
  3. scope: The token’s scope.
  4. subject_type: The subject type of API requests with the given token. It should be USER for access token and refresh token or APP for bot access token.
  5. subject_id: The subject id of API reequests with the given token. It should be the user id who authorized the scope for access token and refresh token and the app id installed for bot access token.
  6. community_id: The community id associated with the token.
  7. exp: The expiration time of the token specified as a Unix timestamp.

Handling errors

/authorize endpoint

A failed authorization request’s redirect_uri has error which is the error code and error_description which is the description for the error as query parameters. For example, if a user determines to cancel in the authorization page, the redirected redirect_uri would look like this:

https://example.com/callback/canpass?error=access_denied&error_description=the+user+canceled+the+authentication

The full list of errors will be provided soon.

/token endpoint

A failed token endpoint request’s response has error which is the error code and error_description which is the description for the error as JSON items. In case of the invalid authorization code, the response would look like this:

HTTP/1.1 400 Bad Request
access-control-allow-credentials: true
content-type: application/json; charset=utf-8
vary: origin
access-control-expose-headers: WWW-Authenticate,Server-Authorization
cache-control: no-cache
content-length: 92
Date: Thu, 14 Dec 2023 05:25:44 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"invalid_grant","error_description":"Invalid grant: authorization code is invalid"}

The full list of errors will be provided soon.


Back to top

Copyright © 2018-2023 CAN Lab PTE Ltd. All Rights Reserved.