Getting Started

Table of contents
  1. Prerequisites
  2. Running the Example
    1. Cloning the Source Code
    2. Replacing Environment Variables
    3. Exposing localhost to the Internet
    4. Installing Dependencies
    5. Running the Webapp
  3. Testing the Example
  4. Understanding the Example
    1. GET /
      1. Building an Authorization Endpoint URL
      2. Storing Code Verifier and State
    2. GET /callback/canpass
      1. Extracting Callback Parameters
      2. Validating Authorization Callback
      3. Handling authorization endpoint errors
      4. Exchanging Authorization Code for Access Token
      5. Making API Requests on Behalf of a User
      6. Handling Bot
      7. Making API Requests as an App Installed on a Community
    3. POST /callback/cand
      1. Webhook Event
      2. Handling Webhook Event
  5. Conclusion
    1. See also

This guide walks you through the process of writing a Node.js webapp that

  • Requests users to authorize and install a canD app in their communities.
  • Retrieves user information on behalf of the user.
  • Creates a product within the community, acting as an app installed in the community.
  • Subscribes to the product creation webhook event.

Prerequisites

You should have a canD app and a community where you are the admin. At the moment, only invited teams can own and use these. If you are interested in CAN, feel free to contact contact@canlab.co. We will provision and provide a necessary environment for your team.

This guide presupposes that you have a fundamental understanding of JavaScript and the Express npm module.

To run the example provided, you should have Node.js version 18 or later installed.

Running the Example

Let’s run the complete example first, test it out, and then delve into the details of what happened.

Cloning the Source Code

Clone the canfoundation/cand-app-js-example repository or download the source code from the repository, and navigate to the directory where the code is stored.

git clone https://github.com/canfoundation/cand-app-js-example
cd cand-app-js-example

Replacing Environment Variables

Open the index.js file and replace the env constant with your environment’s appropriate values.

const env = {
  PORT: 3000,
  CAND_APP_CLIENT_ID: 'YOUR_CAND_APP_CLIENT_ID',
  CAND_APP_REDIRECT_URI: 'http://localhost:3000/callback/canpass',
};
  1. Replace YOUR_CAND_APP_CLIENT_ID with your canD app’s client id.
  2. Ensure the CAND_APP_REDIRECT_URI value is registered as the redirect uri in your canD app.

Exposing localhost to the Internet

This guide assumes that you wish to test the webapp in a local environment without deploying it. If you prefer deployment, please deploy the webapp and accordingly update the redirect uris and webhook URL of your canD app. If you don’t intend to test the webhook functionality, you may skip this section.

To receive the webhook events in local environment without deploying, you can expose localhost to the internet using tunneling services such as Beeceptor, Serveo, and Smee.

For example, Serveo allows you to expose localhost without the need for installing external packages. Open a new terminal and execute the following command:

ssh -R 80:localhost:3000 serveo.net

You should see an output similar to this:

Forwarding HTTP traffic from https://tulerit.serveo.net

This means that HTTP traffic to https://tulerit.serveo.net will be forwarded to http://localhost:3000. As this process involves port binding, you may edit the source code and restart the webapp at any time, independent of the tunneling status.

In this scenario, your webhook URL will be https://tulerit.serveo.net/callback/cand. Be sure to register this url in your canD app.

Installing Dependencies

At this point, all environment configuration is complete. Run npm install to install the dependencies in the directory where the source code is located.

npm install

Running the Webapp

Next, execute the following command to start the webapp:

npm run start

You should see a message indicating that the app is listening on port 3000 or the port you specified, displayed in the console.

Testing the Example

Now, let’s explore the webapp from a user’s perspective. Open http://localhost:3000/?community_id=COMMUNITY_ID in your browser, where COMMUNITY_ID represents the community where you wish to install your canD app.

Here’s what your users will experience:

sequenceDiagram
  participant User
  participant Example
  participant CANpass
  participant canD
  rect rgb(191, 223, 255)
  note right of User: GET / handler
  User ->> Example: 1. Visits http://localhost:3000/?community_id=COMMUNITY_ID
  Example ->> User: 2. Redirects to the authorization endpoint URL
  end
  User ->> CANpass: 3. Signs in to CANpass
  CANpass ->> User: 4. Redirects to the consent page
  User ->> CANpass: 5. Decides to accept or reject the authorization request
  rect rgb(191, 223, 255)
  note left of CANpass: GET /callback/canpass handler
  CANpass ->> Example: 6. Redirects to redirect_uri with the authorization result
  Example ->> CANpass: 7. Exchange the authorization code for an access token
  CANpass ->> Example: 8. Responds with tokens
  Example ->> canD: 9. Fetches the user information
  canD ->> Example: 10. Responds with the user information
  Example ->> canD: 11. Creates a product using the access token
  canD ->> Example: 12. Responds with the created product details
  Example ->> User: 13. Displays with the result
  end
  1. In Step 2, the user is redirected to the sign-in page at https://canpass.me, the authorization server of the canD ecosystem, according to OAuth2 standards. The sign-in page is styled based on the community’s settings and provides the community’s login options.
  2. If the user already has a session on https://canpass.me, Step 3 is skipped.
  3. After signing in, the user is redirected to the consent page to reviews permissions requested by your canD App and decide whether to grant or deny them. If the user has previously authorized the given scope for the same canD app within the same community, they will be directly redirected to the webapp, bypassing Steps 4 and 5.
  4. In Step 4, if the user is a community admin, they will see all requested permissions, including those for reading and writing products both as a community member and as a canD app (a.k.a., bot), as well as subscribing to product webhook events. Non-admin users will only see permissions related to reading and writing products as community members. Even if bot scopes and webhook scopes are included in the authorization endpoint URL, these will be omitted if the user lacks the necessary authorization privileges.
  5. If the user denies the authorization request, they will be redirected to the webapp and presented with an error message. The sequence diagram above does not cover this error case. Steps 7-12 are considered skipped, with only Step 13 occurring.
  6. If the user accepts the authorization request, they will be redirected to the webapp and presented with a success message.

Understanding the Example

The example webapp is composed of a basic package.json and an index.js file that creates and runs an Express app with 3 endpoint handlers. The package.json can be disregarded for this context; instead, focus primarily on the index.js file.

GET /

This handler constructs the authorization endpoint URL and redirects incoming requests to that url.

Building an Authorization Endpoint URL

The authorization endpoint URL, https://canpass.me/oauth2/authorize, is where users review the permissions requested by your canD app and decide to accept or deny them. This url employs a query string to use parameters that dictate the UX users will experience

const createCodeVerifier = () => btoa(String.fromCharCode(...new Uint8Array(crypto.getRandomValues(new Uint8Array(32)).buffer)));
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 nonce = `${Math.random()}`;
const state = {nonce};
const stateString = JSON.stringify(state);
const scopes = ['member:MOIM:product:read', 'member:MOIM:product:write', 'bot:MOIM:product:read', 'bot:MOIM:product:write', 'webhook:MOIM:product'];
const searchParams = new URLSearchParams({
  response_type: 'code',
  action: 'signin',
  social_logins: 'all',
  client_id: env.CAND_APP_CLIENT_ID,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
  redirect_uri: env.CAND_APP_REDIRECT_URI,
  scope: scopes.join(' '),
  state: stateString,
  // community_id should be included in the query
  ...req.query,
});

url.search = searchParams.toString();
  1. redirect_uri: An authorization callback uri to be redirected after processing the authorization request. It should be registered in the redirect uris of the canD App in advance.
  2. 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. In this example, it’s supposed to be given in the query string for the sake of convenience.

  3. codeVerifier and codeChallenge: These elements are used to verify that your canD app made the authorization request when the authorization callback is invoked. The codeVerifier should be generated by creating a base64 url encoded string from 32 random bytes and stored for use in the authorization callback handler. The codeChallenge is derived by hashing the codeVerifier with sha-256 and then encoding the result into base64 url format. If you prefer to use a client secret, the codeVerifier and codeChallenge can be disregarded. However, it’s important to ensure that your canD app manages the client secret securely before relying on it. To utilize a client secret, it must first be generated on the canD app settings page.
  4. nonce and state: The state is a random string used to prevent Cross-Site Request Forgery (CSRF) attacks, returned as a query parameter in the authorization callback, redirect_uri. It helps in maintaining and restoring the application state across authorization requests. The nonce is used to ensure the randomness of the state. The state should be stored for later use in the authorization callback handler.
  5. scope: This is a space-separated list of permissions indicating the actions your canD app is authorized to perform on the user’s or community’s resources. It’s advisable to limit the scope to minimize potential damage should the access token be compromised.
  6. action: Specifies the page to be shown if the user needs to sign in or sign up due to the absence of a session. It can be either signin or signup. If not specified, it defaults to signin.
  7. social_logins: A parameter to show the community’s login option. To include several login options, repeat the social_logins parameter with each option in the desired order, e.g., social_logins=google&social_logins=facebook. To display all login options, use all, a reserved login option.
  8. redirect_uri: The uri to which the user will be redirected after the authorization request is processed. This uri must be pre-registered in the canD app’s redirect URIs.
  9. community_id: Identifies the community whose resources will be accessed and modified. The sign-in and sign-up pages, as well as login options, are customized based on the community’s settings. In this example, for convenience, community_id is provided through the query string.

Storing Code Verifier and State

As mentioned, codeVerifier and state should be stored in the authorization callback handler.

db.codeVerifiers[stateString] = codeVerifier;

Regardless of where your canD app handles the access token (e.g., browser, backend, mobile, desktop), it must be stored to retrieve and validate the authorization callback request. For backend apps, it’s crucial to set a timeout and remove the stored items to prevent potential memory leaks.

GET /callback/canpass

This handler serves as the authorization callback handler, as indicated by the redirect_uri query parameter in the authorization endpoint URL. It validates the authorization callback and exchanges the provided authorization code for access tokens. Additionally, for demonstration purposes, it makes API requests to canD using the access tokens.

When users decide to accept or reject the authorization request, CANpass processes the authorization and redirects them to your canD app. This means that users will stay on CANpass until your canD app responds after they make their decision. For an improved UX, aim to respond to the request promptly.

Extracting Callback Parameters

The authorization callback is a GET HTTP request that includes the following query parameters:

const {state: stateString, error, error_description, code} = req.query;
  1. state: The state previously passed to the authorization endpoint URL.
  2. code: The authorization code to be exchanged the access tokens. If the authorization failed, this will not be present.
  3. error and error_description: These fields provide details on any errors that occurred during the authorization process. If the authorization was successful, they will not be present.

Validating Authorization Callback

Since the authorization endpoint URL is public, anyone can invoke your canD app’s authorization callback handler. To filter out invalid requests, your canD app should validate requests using the state and codeVerifier stored when building the authorization endpoint URL.

const codeVerifier = db.codeVerifiers[stateString];

if (!codeVerifier) {
  res.sendStatus(400);
  return;
}

delete db.codeVerifiers[stateString];

Retrieve the code verifier using the given state. If the code verifier doesn’t exist, it indicates that the authorization callback did not originate from your canD app. For such invalid requests, send a 400 response. If it exists, delete the code verifier to prevent possible memory leaks.

Handling authorization endpoint errors

Failed authorization requests include an error and error_description as query parameters.

if (error) {
  if (error !== 'access_denied') {
    console.error(`Check the error: ${error} and ${error_description}`);
  }
  res.status(400).send(`Error: ${error} and ${error_description}`);

  return;
}

Except for cases where the user denies the authorization request (where error is access_denied), you should examine all error cases and address them.

If the redirect_uri included in the authorization endpoint URL is incorrect, this handler will not receive errors, and users will encounter errors on CANpass. Ensure it is correctly set.

Exchanging Authorization Code for Access Token

Succeeded authorization requests include a code query parameter. The code is generated for each authorization request and is intended for one-time use.

const tokenRes = 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: env.CAND_APP_CLIENT_ID,
    code,
    code_verifier: codeVerifier,
    redirect_uri: env.CAND_APP_REDIRECT_URI,
  }).toString(),
});
const tokenBody = await tokenRes.json();

console.log('CANpass token endpoint response status and body', tokenRes.status, tokenBody);

if (tokenRes.status !== 200) {
  res.sendStatus(400);
  return;
}

You can exchange the code for access tokens by calling the token endpoint as shown above. In addition to code, the token endpoint requires client_id, code_verifier, and redirect_uri for authentication. If your canD app opted to use the client secret instead of attaching the code_challenge when building the authorization endpoint, you should include client_secret, setting it to the client secret value instead of code_verifier.

If the response status code is not 200, the response body will include error and error_description properties in JSON format. For instance, if an incorrect code is provided, the following error message should be returned:

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

If the response status code is 200, the response body will include the following properties in JSON format:

  1. access_token: The access token to authenticate API requests on behalf of a user.
  2. expires_in: The expiration time of the access token in seconds, set to 1 hour (3600 seconds).
  3. refresh_token: The refresh token used to obtain a new access token with a reset expiration date. The expiration date is set to 90 days.
  4. scope: The authorized scope. Note that it may not include all scope items that were requested in the authorization endpoint URL.
  5. user_id: The id of the user who authorized the scope.
  6. community_id: The community id that was included in the authorization endpoint URL.
  7. bot_access_token: The access token for the bot to make API requests. It is provided only if bot scopes starting with bot: or webhook scopes starting with webhook: are included in the scope. Unlike the access token and refresh token, it does not expire, and its value remains consistent across multiple authorizations.

At this step, you can create your canD app’s user entity with user_id and community_id if necessary. If more details about the user are needed, refer to the next section.

Making API Requests on Behalf of a User

With the access token now available, your canD app can make API requests to Moim, which functions as a canD module and a resource server in terms of OAuth2. Let’s proceed to fetch the user details, corresponding to Steps 9-10 in the sequence diagram.

const meRes = await fetch('https://api.cand.xyz/me', {
  headers: {
    'content-type': 'application/json',
    'authorization': `Bearer ${tokenBody.access_token}`,
    'x-can-community-id': tokenBody.community_id,
  },
});
const meBody = await meRes.json();

console.log('Moim GET /me response status and body', meRes.status, meBody);

All canD modules require the Authorization request header for authentication. Add the Authorization request header with the value Bearer ${accessToken}.

Furthermore, canD modules providing community features, such as Moim, also require the x-can-community-id request header. Attach the x-can-community-id request header, setting its value to the community id associated with the access token. To access resources from another community, a new access token associated with that community id is necessary, even for the same user.

The /me endpoint returns user details such as name, email, locale, and timezone. These details can be used to build your canD app’s user entity. Note that the id in the response is contingent upon both the user and the community. If the same user authorizes a different community, while the user_id in the token endpoint response remains constant, the id in the /me response will differ.

Refer to the Moim API reference for details of the above endpoint.

Handling Bot

If the scope property in the token endpoint response includes bot items starting with bot: or webhook items starting with webhook:, a bot_access_token property is also provided. With the bot_access_token, your canD app can make API requests as an app installed on the community.

if (tokenBody.bot_access_token) {
  if (!db.bots[tokenBody.community_id]) {
    const bot = {id: tokenBody.community_id, accessToken: tokenBody.bot_access_token};

    db.bots[tokenBody.community_id] = bot;
    console.log('New bot is created and stored', bot);
  }
}

If a bot_access_token is provided for a community for the first time, it indicates that users who are admins of the community specified in the authorization endpoint URL have installed your canD app in their community. In such cases, create a corresponding bot entity in your canD app to later retrieve the bot access token by the community id.

Note that whenever users with admin privileges authorize a request that includes bot or webhook items in its scope, a bot_access_token is provided. This may indicate that a bot has been created, its scope has been updated, or it might signify no change if no updates are made.

Making API Requests as an App Installed on a Community

For demonstration purposes, the authorization callback handler creates a product as a bot to trigger the product creation webhook event. This only works if the bot_access_token is available in the token endpoint response.

const productRes = await fetch('https://api.cand.xyz/products/', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'authorization': `Bearer ${tokenBody.bot_access_token}`,
    'x-can-community-id': tokenBody.community_id,
  },
  body: JSON.stringify({
    "name": "Example fancy product",
    "type": "normal",
    "isDisplayed": true,
    "blocks": [
      {
        "type": "text",
        "content": "sample text"
      }
    ],
    "images": {
      "mobile": [
        "https://dummyimage.com/350x350/000/fff&text=Product"
      ],
      "web": [
        "https://dummyimage.com/350x350/000/fff&text=Product"
      ]
    },
    "status": "onSale",
    "price": 99,
    "normalPrice": 100,
    "originalPrice": 101,
    "supplyPrice": 80,
    "description": "description",
    "sku": "sku#1",
    "stockCount": 3,
    "weight": 0
  }),
});
const productBody = await productRes.json();

console.log('Moim POST /products response status and body', productRes.status, productBody);

To make API requests as an app installed on a community, set the Authorization request header to Bearer ${bot_access_token}. For details of the above endpoint, refer to the Moim API reference.

POST /callback/cand

This handler is invoked when canD modules trigger webhook events to which your canD app is subscribed. A webhook event is an HTTP POST request with a JSON body. The response from this handler is not significant; you can simply end it.

Although this endpoint is not public, theoretically, someone might guess it, forge fake webhook events, and call the handler. Use a webhook URL that is difficult to guess. Measures to validate the authenticity of webhook events will be introduced soon.

Webhook Event

The webhook event is defined by the request body of the webhook event request.

const {module, type, context, payload} = req.body;

console.log('Webhook event given', module, type, context, payload);

It includes the following properties:

  1. module: The canD module id, such as MOIM.
  2. type: The type of the webhook event, e.g., product.created, as defined in the canD Module.
  3. context: The context object for the webhook event.
  4. originCommunityId: The id of the origin community, one of whose children is the community in question.
  5. userId: The id of the user in the origin community.
  6. communityId: The id of the community where the event occurs.
  7. profileId: The id in the community of the user who performed the action that triggered the event.
  8. payload: The payload object for the webhook event. For details, see the canD module reference.

Handling Webhook Event

Currently, most webhook event payloads contain only an entity’s id where some change has occurred. To retrieve the full contents, you need to find the bot access token for the community where the event occurred and authenticate the request with it as shown below.

const botAccessToken = await db.bots[context.originCommunityId];
const productRes = await fetch(`https://api.cand.xyz/products/${payload.id}`, {
  headers: {
    'content-type': 'application/json',
    'authorization': `Bearer ${botAccessToken}`,
    'x-can-community-id': context.originCommunityId,
  },
});
const productBody = await productRes.json();

console.log('Moim GET /products/${proudctId} response status and body', productRes.status, productBody);

This also implies that when subscribing to changes of some entity through a webhook, you should also request the scope to read that entity. This will be improved by providing the full contents as well as its previous version in future updates.

Conclusion

In this guide, we explored the basic features of CANpass and canD: how to request user authorization and app installation in their communities, how to make API requests on behalf of the user, how to make API requests as an app installed on a community, and how to subscribe to webhook events.

See also


Back to top

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