Anypoint Sign In Button

Interactive demo

This demo lets you preview the sign in button element with various configuration options.

Authorization status

User signed in: false

Authorization code:

Usage

Anypont sign in button is a web component and can be used in any web environment.

Learn more about using web components at Open WC project.

Installation

npm install --save @anypoint-web-components/anypoint-signin

In an html element

<html>
  <head>
    <script type="module">
      import '@anypoint-web-components/anypoint-signin/anypoint-signin.js';
    </script>
  </head>
  <body>
    <anypoint-signin></anypoint-signin>

    <script>
    (async () => {
      await customElements.whenDefined('anypoint-signin');
      const button = document.body.querySelector('anypoint-signin');
      button.onsignedin = (e) {
        if (e.target.signedIn) {
          console.log('User is signed in');
        } else {
          console.log('User is not signed in');
        }
      };
    })();
    </script>
  </body>
</html>

In a LitElement template

import { LitElement, html } from 'lit-element';
import '@anypoint-web-components/anypoint-signin/anypoint-signin.js';

class SampleElement extends LitElement {

  constructor() {
    super();
    this.clientId = '...';
    this.redirectUri = '...';
    this.scopes = 'profile';
  }

  render() {
    return html`
      <anypoint-signin
        .clientId="${this.clientId}"
        .scopes="${this.scopes}"
        .redirectUri="${this.redirectUri}"
        @signedin-changed="${this._signedinHandler}"
      ></anypoint-signin>
    `;
  }

  _signedinHandler(e) {
    this.isSignedIn = e.target.signedIn;
  }
}
customElements.define('sample-element', SampleElement);

Requesting a token

At the moment Anypoint authorization server only supports authorization code OAuth 2 flow.

The button starts the authorization flow and returns authorization code. The code should be then used the exchange it to access token using a server component.

In an html file
async function exchangeCode(e) {
  const { code } = e.detail;
  const init = {
    method: 'POST',
    body: code
  };
  const tokenExchangeUrl = 'YOUR SERVER URL';
  const response = await fetch(tokenExchangeUrl, init);
  const token = await response.json();
  const button = document.body.querySelector('anypoint-signin');
  button.signedIn = !!token;
};
window.addEventListener('oauth2-code-response', exchangeCode);
In a LitElement element
import { LitElement, html } from 'lit-element';
import '@anypoint-web-components/anypoint-signin/anypoint-signin.js';

class SampleElement extends LitElement {
  constructor() {
    super();
    ...
    this.exchangeCode = this.exchangeCode.bind(this);
  }

  connectedCallback() {
    super.connectedCallback();
    window.addEventListener('oauth2-code-response', this.exchangeCode);
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    window.removeEventListener('oauth2-code-response', this.exchangeCode);
  }

  async exchangeCode(e) {
    const { code } = e.detail;
    const init = {
      method: 'POST',
      body: code
    };
    const tokenExchangeUrl = 'YOUR SERVER URL';
    const response = await fetch(tokenExchangeUrl, init);
    this.token = await response.json();
    const button = this.shadowRoot.querySelector('anypoint-signin');
    button.signedIn = !!this.token;
  }
}
customElements.define('sample-element', SampleElement);

Exchanging the code

Thwe server must make a request to Anypoint authorization server with OAuth 2 standard parameters in the request body. These are:

  • "grant_type" - Always set to "authorization_code"
  • "client_id" - The same client ID used in the button
  • "code" - Received from the authorization server code
  • "redirect_uri" - Registered in authorization server settings redirect URI
  • "client_secret" - You will find client secret in your OAuth application details

This parameters have to be sent to token endpoint as application/x-www-form-urlencoded request.

Authorization endpoint
https://anypoint.mulesoft.com/accounts/api/v2/oauth2/token

You can choose any language and library you like to create an API to exchange the code for token. Below we present Express route for Node.

Express.js example
const express = require('express');
const https = require('https');

const router = express.Router();

const apiBase = 'https://anypoint.mulesoft.com/accounts/api';
const tokenEndpoint = `${apiBase}/v2/oauth2/token`;

/**
 * A route that support authorization for anypoint-signing button.
 */
class AuthApiRoute extends BaseApi {
  /**
   * Exchanges authorization code for access token in Anypoint service.
   * The request body is the received code.
   *
   * @param {Object} req
   * @param {Object} res
   */
  async anypointTokenRequest(req, res) {
    const { body } = req;
    if (!body || typeof body !== 'string') {
      res.status(400).send({
        error: true,
        message: 'Missing payload message'
      });
      return;
    }
    try {
      const data = await this._exchangeToken(body);
      res.status(200).send({
        error: false,
        data,
      });
    } catch (e) {
      res.status(500).send({
        error: true,
        message: e.message
      });
    }
  }
  /**
   * Exchanges code for access token.
   * @param {String} code Received authrization code
   * @return {Promise}
   */
  async _exchangeToken(code) {
    const body = this._getTokenExchangeBody(code);
    const [mediaType, rawBody] = await this._makeRequest(body);
    return this._processCodeResponse(rawBody, mediaType);
  }
  /**
   * Creates message body for OAuth 2 token exchange
   * @param {String} code Received authrization code
   * @return {String}
   */
  _getTokenExchangeBody(code) {
    const parts = [];
    parts[parts.length] = ['grant_type', 'authorization_code'];
    parts[parts.length] = ['client_id', 'YOUR CLIENT ID'];
    parts[parts.length] = ['redirect_uri', 'YOUR REDIRECT URI'];
    parts[parts.length] = ['client_secret', 'YOUR CLIENT SECRET'];
    parts[parts.length] = ['code', code];
    return parts
      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
      .join('&');
  }
  /**
   * Makes a request to Anypoint authorization server.
   * @param {String} body
   * @return {Promise}
   */
  _makeRequest(body) {
    const options = {
      method: 'POST',
      headers: {
        'content-type': 'application/x-www-form-urlencoded'
      }
    };
    return new Promise((resolve, reject) => {
      const req = https.request(tokenEndpoint, options, (res) => {
        const { statusCode } = res;

        if (statusCode >= 500) {
          const message = 'Authorization server error.';
          reject(new Error(message));
          return;
        }

        res.setEncoding('utf8');
        const contentType = res.headers['content-type'];
        let data = '';

        res.on('data', (chunk) => {
          data += chunk;
        });

        res.on('end', () => {
          if (statusCode >= 400 && statusCode < 500) {
            reject(new Error(`Client error: ${data}`));
          } else {
            resolve([contentType, data]);
          }
        });
      });

      req.on('error', (e) => {
        reject(e.message);
        logging.error(e);
      });

      req.write(body);
      req.end();
    });
  }
  /**
   * Processes token request body and produces map of values.
   *
   * @param {String} body Body received in the response.
   * @param {String} contentType Response content type.
   * @return {Object} Response as an object.
   * @throws {Error} Exception when body is invalid.
   */
  _processCodeResponse(body, contentType) {
    if (!body) {
      throw new Error('Code response body is empty.');
    }
    let tokenInfo;
    if (contentType.indexOf('json') !== -1) {
      tokenInfo = JSON.parse(body);
    } else {
      tokenInfo = {};
      body.split('&').forEach((p) => {
        const item = p.split('=');
        const name = item[0];
        const value = decodeURIComponent(item[1]);
        tokenInfo[name] = value;
      });
    }
    return tokenInfo;
  }
}

const api = new AuthApiRoute();
router.post('/token', api.anypointTokenRequest.bind(api));
module.exports = router;