Skip to content

CIAM by examples

Service MarketPlace

The first Example regards the application Service Marketplace. This application uses the PKCE code flow directly from the frontend made with react. The following class and methods have been used to generate the necessary tokens for the PKCE flow:

import Base64 from 'crypto-js/enc-base64';
import sha256 from 'crypto-js/sha256';
/**
 * Returns URL Safe b64 encoded string from an int8Array.
 * @param inputArr
 */
const urlEncodeArr = inputArr => {
  return base64EncArr(inputArr)
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
};
/**
 * base64 url encode a string
 */
const base64URL = value =>
  value
    .toString(Base64)
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
/**
 * Base64 encode byte array
 * @param aBytes
 */
const base64EncArr = aBytes => {
  const eqLen = (3 - (aBytes.length % 3)) % 3;
  let sB64Enc = '';
  for (
    let nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0;
    nIdx < nLen;
    nIdx++
  ) {
    nMod3 = nIdx % 3;
    nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
    if (nMod3 === 2 || aBytes.length - nIdx === 1) {
      sB64Enc += String.fromCharCode(
        uint6ToB64((nUint24 >>> 18) & 63),
        uint6ToB64((nUint24 >>> 12) & 63),
        uint6ToB64((nUint24 >>> 6) & 63),
        uint6ToB64(nUint24 & 63)
      );
      nUint24 = 0;
    }
  }
  return eqLen === 0
    ? sB64Enc
    : sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? '=' : '==');
};
/**
 * Base64 string to array encoding helper
 * @param nUint6
 */
const uint6ToB64 = nUint6 => {
  return nUint6 < 26
    ? nUint6 + 65
    : nUint6 < 52
      ? nUint6 + 71
      : nUint6 < 62
        ? nUint6 - 4
        : nUint6 === 62
          ? 43
          : nUint6 === 63
            ? 47
            : 65;
};
/**
 * PKCEGenerator contains the functions to perform the Proof Key for Code Exchange by OAuth Public Clients.
 * See the RFC for more information: https://tools.ietf.org/html/rfc7636.
 */
export class PKCEGenerator {
  constructor() {
    this.cryptoObj = window.crypto;
  }
  /**
   * creates cryptographically random string
   */
  generateCodeVerifier = () => {
    const arrayBuffer = new Uint32Array(32);
    this.cryptoObj.getRandomValues(arrayBuffer);
    return urlEncodeArr(arrayBuffer);
  };
  generateCodeChallengeFromVerifier = codeVerifier =>
    base64URL(sha256(codeVerifier));
  /**
   * Generates PKCE Codes
   */
  generateCodes = () => {
    const codeVerifier = this.generateCodeVerifier();
    const codeChallenge = this.generateCodeChallengeFromVerifier(codeVerifier);
    return {
      codeVerifier,
      codeChallenge
    };
  };
}

This class and methods are used to initialize and make the necessary call to the CIAM for the PKCE code flow

import axios from 'axios';import { PKCEGenerator } from './pkce';import { User } from '../api';
import { Helper } from '../helpers/helpers';/**
 * CLIENT_ID is the CIAM trusted ID
 */
const CLIENT_ID = process.env.REACT_APP_AUTHENTICATION_CLIENT_ID;/**
 * REDIRECT_URI is the CIAM redirect uri once the login is completed
 */
const REDIRECT_URI = process.env.REACT_APP_CIAM_REDIRECT_URI;/**
 * REDIRECT_HOMEPAGE is the CIAM redirect homepage once the login is completed
 */
const REDIRECT_HOMEPAGE = process.env.REACT_APP_PUBLIC_URL;/**
 * authUrl is the endpoint where the user will perform the login
 */
const authUrl = 'https://ciam.auth.wfp.org/oauth2/authorize';/**
 * tokenUrl is the endpoint where to request `access_token`
 */
export const tokenUrl = 'https://ciam.auth.wfp.org/oauth2/token';/**
 * getLoginURL()
 * based on the passed-in `codeChallenge`
 */
export const getLoginURL = codeChallenge =>
  `${authUrl}?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&redirect_homepage=${REDIRECT_HOMEPAGE}&scope=openid&response_type=code&code_challenge_method=S256&code_challenge=${codeChallenge}`;/*
 * headers are the headers accepted by ciam
 */
const headers = {
  'Content-Type': 'application/x-www-form-urlencoded'
};/**
 * retrieveToken()
 * based on the passed-in url and codeVerifier
 * which are the parameters needed to close the PKCE flow
 * returns `access_token`.
 */
export const retrieveToken = (url, codeVerifier) => {
  const callbackUrl = new URL(url);
  // the callbackUrl should contain the `code`
  const code = callbackUrl.searchParams.get('code');
  if (code) {
    const params = new URLSearchParams();
    params.append('client_id', CLIENT_ID);
    params.append('grant_type', 'authorization_code');
    params.append('redirect_uri', REDIRECT_URI);
    params.append('code', code);
    params.append('code_verifier', codeVerifier);    return axios.post(tokenUrl, params, { headers }).then(response => {
      const { refresh_token, access_token } = response.data;
      localStorage.setItem('token', access_token);
      localStorage.setItem('refreshToken', refresh_token);
      return access_token;
    });
  }
};/**
 * onCiamLogin()
 * starts login flow with CIAM by opening a new window where the user
 * will perform the login or registration.
 */
export const onCiamLogin = () => {
  const pkceObj = new PKCEGenerator();
  const { codeVerifier, codeChallenge } = pkceObj.generateCodes();
  const loginUrl = getLoginURL(codeChallenge);
  // pop-up a window to display CIAM login
  const login = window.open(loginUrl, '', 'width=800,height=800');
  setInterval(() => {
    try {
      retrieveToken(login.location, codeVerifier).then(token => {
        User.getUser().then(response => {
          Helper.SaveUserData(response.data.profile);
          // by default redirect the user to /home if not staff
          let redirectUrl = response.data.profile.is_staff
            ? '/admin/index.html'
            : '/home';          // redirect to complete registration if profile is not fulfilled
          if (!Helper.UserInfoFullfilled(response.data.profile)) {
            redirectUrl = '/complete-registration';
          }          window.location.href = redirectUrl;
          login.close();
        });
      });
    } catch (error) {
      // Do nothing on error
    }
  }, 5000);
};const clearAndRedirectToLogin = error => {
  localStorage.clear();
  window.location = '/login';
  return Promise.reject(error);
};/*
 * refreshAccessToken()
 * based on passed-in:
 * `client`: axios instance.
 * `error`: the request that raised a 401.
 * `milliseconds`: timeout to retry the request.
 *
 * performs access token refresh and request retry.
 */
export const refreshAccessToken = (client, error, milliseconds) => {
  const refreshToken = localStorage.getItem('refreshToken');
  if (!refreshToken) {
    return clearAndRedirectToLogin(error);
  }  const { config } = error;
  const originalRequest = config;  const params = new URLSearchParams();
  params.append('client_id', CLIENT_ID);
  params.append('grant_type', 'refresh_token');
  params.append('refresh_token', refreshToken);  return axios
    .post(tokenUrl, params, { headers })
    .then(response => {
      const { refresh_token, access_token } = response.data;
      localStorage.setItem('token', access_token)
      localStorage.setItem('refreshToken', refresh_token);
      return new Promise((resolve, reject) => {
        setTimeout(() => resolve(client(originalRequest)), milliseconds);
      });
    })
    .catch(e => clearAndRedirectToLogin(e));
};

The following class and methods are used to create the frontend and initialize the interaction

import React from 'react';
import { Blockquote, Modal, Link } from '@wfp/ui';import { onCiamLogin } from '../../auth/ciam';export const Login = () => {
  return (
    <div className="wfp--modal__dialog-background">
      <Modal
        backgroundImage="images/landing.jpg"
        danger={false}
        hideClose
        modalAriaLabel=""
        modalHeading="Login"
        modalLabel="WFP Service Marketplace"
        onRequestSubmit={onCiamLogin}
        open
        primaryButtonDisabled={false}
        primaryButtonText="Login with CIAM"
        secondaryButtonText=""
        shouldSubmitOnEnter
      >
        <Blockquote info>
          To be able to access you will be asked to enter your login
          credentials. You will be redirected to the application after login.
          For any question or support, please
          <Link href="/contact-us"> contact Us</Link> |{' '}
          <Link href="/home">Go back home</Link>.
        </Blockquote>
      </Modal>
    </div>
  );
};export default Login;