Use Your Own Provider for Mail Sent with IBM Cloud App ID

7 min read

IBM Cloud App ID’s Cloud Directory feature

With IBM Cloud App ID’s Cloud Directory feature, you can add sign-up and sign-in to your mobile or web apps and create a user registry to manage users. Cloud Directory supports sending email messages to your users to verify their email address, allows them to reset their password, and more. By default, App ID takes care of delivering the email messages for you but you can define a custom extension point that will be invoked when an email needs to be sent.

While App ID can send the email messages for you, there are some benefits in using your own email provider solution:

  • Use your own domain: App ID sends its emails from appid.cloud.ibm.com, and this domain will be visible to your users. By using your own mail dispatcher, you further reduce the chance of emails being filtered as spam.

  • Insights and troubleshooting: Gain insights from your email provider, including how many people opened the emails and which messages were not delivered. This can help solve issues since you can track individual messages and see overall statistics.

Working example

In this example, we will use SendGrid as an email provider solution and we’ll implement the extension point as an IBM Cloud Function using Node.JS 8. It is important to note that you are not in any way confined to this selection. You can use any email solution or use an SMTP server directly. Also, the extension point can be implemented on any platform using any language or tool, as long as it can be triggered by an HTTP POST request.

Prerequisites

  • IBM Cloud Functions CLI

  • NPM

  • Access to a SendGrid account (trial account also possible)

  • App ID instance with Cloud Directory enabled

Obtaining SendGrid details

To connect our extension point to SendGrid, we’ll need an API Key. We are going to obtain the API key from SendGrid UI. To generate an API key, go to API Keys under Settings in the navigation menu on the left. Click on Create API Key. SendGrid allows you to define fine-grained permissions per API Key—for our purposes, we only need Full Access to the Mail Send API.

We’ll need the API key for later, so copy the value or keep the browser tab open on the page (if you leave you will not be able to see the API key and will have to create a new one).

Creating a Cloud Function

Go to the IBM Cloud console, click on Create Resource and search for “functions”. Once you are inside the Cloud Functions console, create a new Action and select Node.JS 8 as the runtime.

Writing the Cloud Function Action

The Cloud Functions console allows us to edit the Action code inside the browser, but this editor is limited and will not allow us to easily add third-party dependencies. Since we’re going to use the SendGrid SDK for Node.JS, we’ll develop our Action as an NPM module and upload it.

Let’s set up a basic NPM module with 2 files.

package.json:

{
  "name": "email-dispatcher",
  "main": "index.js",
  "dependencies": {
    "@sendgrid/mail": "^6.3.1",
    "bluebird": "^3.5.2",
    "jsonwebtoken": "^8.3.0",
    "jwk-to-pem": "^2.0.0",
    "request": "^2.88.0"
  }
}

index.js:

const sgMail = require('@sendgrid/mail');
const {promisify} = require('bluebird');
const request = promisify(require('request'));
const jwtVerify = promisify(require('jsonwebtoken').verify);
const jwtDecode = require('jsonwebtoken').decode;
const jwkToPem = require('jwk-to-pem');
 
/**
 *
 * This function obtains public keys for your App ID instance,
 * for best performance - the result should be cached.
 *
 * @return The public keys for your App ID instance.
 *
 */
async function obtainPublicKeys() {
  // Your App ID instance OAuth Server URL
  const oauthServerUrl = '<OAUTH-SERVER-URL>';
 
  // Send request to App ID's public keys endpoint
  const keysOptions = {
    method: 'GET',
    url: `${oauthServerUrl}/publickeys`
  };
  const keysResponse = await request(keysOptions);
  return JSON.parse(keysResponse.body).keys;
}
 
async function verifySignature(keysArray, kid, jws) {
  const keyJson = keysArray.find(key => key.kid === kid);
  if (keyJson) {
  const pem = jwkToPem(keyJson);
    await jwtVerify(jws, pem);
    return;
  }
  throw new Error ("Unable to verify signature");
}
 
/**
 *
 * main() will be run when you invoke this action
 *
 * @param Cloud Functions actions accept a single parameter, which must be a JSON object.
 *
 * @return The output of this action, which must be a JSON object.
 *
 */
async function main(params) {
  // The API key for SendGrid, put as a param in your Cloud-Function
  const sgApiKey = params.sgApiKey;
 
  // Init Sendgrind
  sgMail.setApiKey(sgApiKey);
 
  // Decode message to get information
  const data = jwtDecode(params.jws, {complete: true});
 
  // Extract kid from header
  const kid = data.header.kid;
 
  const keysArray = await obtainPublicKeys();
 
  // Verify the signature of the payload with the public keys
  await verifySignature(keysArray, kid ,params.jws);
 
  // Send the email with Your SendGrid account
  const message = data.payload.message;
  const msg = {
    to: message.to,
    from: message.from.address,
    subject: message.subject,
    html: message.body,
  };
  console.log(`Sending email to ${message.to}`);
  let sendgridResponse = await sgMail.send(msg);
 
  return {result : 'email_sent',sendgridResponse};
}
 
exports.main = main;

All the code we need can be found in the index.js file. The main function is the function that will be called when a request to the Action’s URL is sent; the body of the request will be in the params object. The data sent from App ID is in a JWS format, signed with the tenant’s private key. The payload of the JWS will look similar to this:

{
  "tenant": "<TENANT-ID>",
  "iss": "https://<REGION>.appid.cloud.ibm.com/oauth/v4/<TENANT-ID>",
  "iat": 1536842658625,
  "jti": "<UUID>",
  "message": {
    "to": "example@example.com",
    "from": {
      "name": "My service",
      "address": "no-reply@example.com"
    },
    "replyTo": {
      "name": "My service",
      "address": "reply@example.com"
    },
    "subject": "Welcome John Doe",
    "body": "<p>Thank you for joining our service. <br/> </p>"
  }
}

As we can see, all the information about the email to be sent will be inside the message field.

Here are the steps the main function performs:

  1. Decode the JWS sent from App ID.

  2. Extract the ID of the key used to sign the data from the JWS header.

  3. Obtain the public key associated with the key ID from App ID’s public keys endpoint.

  4. Verify the signature with the obtained public key.

  5. Send the email using the SendGrid SDK.

Note: For better performance, the public key can be cached.

Setting SendGrid API key and tenant ID

Before we can use the Action, we need to provide two parameters. The first is the OAuthServerUrl of our App ID instance, which can be found in the App ID console under Service Credentials tab. We’ll need to replace the placeholder for it inside the obtainPublicKeys function.

The other parameter is the SendGrid API key, which we obtained in a previous step. Go to the Cloud Functions console and click Parameters in the left navigation bar. Click Add, set the parameter name to sgApiKey, and the parameter value to “<SENDGRID-API-KEY>” (make sure to wrap the API key in quotes).

Uploading the Action

Before we can upload the action code, we’ll need to run: npm install

Once this is done, we are ready to upload. We’ll use the Cloud Functions CLI to upload our code. We’ll need to zip the entire folder first (our twp files and the node_modules folder). You can use any zip tool or run: zip -r action.zip *

Now we can upload the zip we created by running: ibmcloud wsk action update <ACTION_NAME> --kind nodejs:8 action.zip

Enabling web action and obtaining URL

To allow App ID to call our Cloud Function, we’ll need to provide it with the URL. We can see the URL by going to the Endpoints menu of our Cloud Function Action. In the web action section, click on Enable as Web Action. The URL should be in the following format: https://<REGION>.functions.cloud.ibm.com/api/v1/web/<ACCOUNT-NAME>_<ACCOUNT-SPACE>/default/<ACTION-NAME>

Configuring App ID

Now we have all the information we need to configure our App ID instance and use our Cloud Function Action. We can do this by using the API to set a custom email dispatcher. We’ll need to provide the App ID instance’s tenant ID and an IAM token that is authorized to make changes in this instance. We’ll also need to supply the Cloud Function information in the payload to the request in the following format:

{
  "provider": "custom",
  "custom": {
    "url": "https://<REGION>.functions.cloud.ibm.com/api/v1/web/<ACCOUNT-NAME>_<ACCOUNT-SPACE>/default/<ACTION-NAME>",
    "authorization": {
      "type": "none"
    }
  }
}

Testing

App ID provides a REST API to test your custom email dispatcher. When you call this API, you supply an email address. App ID will attempt to send an email to the provided address by calling the custom email dispatcher that was configured. It will return a response which will contain information on whether the request succeeded. If the request failed, it will supply the status code and headers returned from the custom dispatcher (if a response was successfully returned). You can test and debug your Cloud Functions action by using this API.

Try it out!

We’d love to hear your feedback and questions. Get help for technical questions at Stack Overflow with the ibm-appid tag. For non-technical questions, use IBM developerWorks with the appid tag. For defect or support needs, use the support section in the IBM Cloud menu.

To get started with App ID, check it out in the IBM Cloud Catalog

Be the first to hear about news, product updates, and innovation from IBM Cloud