Invoke Token Endpoint

After successful authentication, Singpass will return an authorization code and state back to your application redirect URL as described under the section Invoke Authorization Endpoint.

The value of the code parameter received will be used to call the Singpass token endpoint to exchange for the security tokens (i.e. ID token and access token) per the authorization flow.

Token endpoint for different environments:

Token Endpoint can only be called from server-side code.

Step 1: Setup Token Endpoint Code

  • Create a new server-side code file called api.js and copy the logic needed for login from the code panel. This file contains the function to generate the client assertion as explained in section Setup Client Assertion. For the demo Singpass application, the api.js file is within the Singpassdemoappserver code.

Do not use the sample private key as it has been compromised. For demo purposes, all keys have been displayed but It is recommended not to directly store your private key/ public key in your code, you should follow the best practices to store them in either Database or Secure Storage, or perhaps store them as environment variables, or in a config files, should avoid hardcoding.

//Api.js

exports.handler = async (event) => {
      try {   
        const REACT_APP_SIGNATURE_PRIVATE_KEY = {
          kty: "EC",
          d: "0GlHbGc8vSnyiB-Lf4_im_WFwrxM0MJjkk96o1-K3JQ",
          crv: "P-256",
          x: "wg11s6ZpBc0my5gT-mYatTZRDhgStyd_0qARVBwAWa4",
          y: "hlVoYWwlCuTMnm79Ppmf3RslIwDRhqdCCnm01PkhA2s"
        };
        const REACT_APP_ENCRYPTION_PRIVATE_KEY =  {
          kty: "EC",
          d: "p4YZHS0_BS4VMUayEtt38qi2sMdkhs4JRFlks7HJCD8",
          crv: "P-256",
          x: "0GR5oBa1FINjCZP_W-nR8Yqoz4E_9j7lgCuRPh9PZTA",
          y: "0leGfxdQSJdtubopqhj5uhPVYV3LSd_yf3y2DdRD5No"
        }
    REACT_APP_CLIENT_ID="tLRDBkf1CNy5Rsi34mEKuOD5EpQAwjIq"
    REACT_APP_JWTTOKENURL="https://stg-id.singpass.gov.sg"
    REACT_APP_SPTOKENURL="https://stg-id.singpass.gov.sg/token"
    REACT_APP_KID="testing123"
    REACT_APP_REDIRECT_URI="https://singpassdemoapp.netlify.app/callback"     
       const code =event.queryStringParameters.code;
        const jose = require("jose");
        const moment = require("moment");
        const axios = require("axios");
        const alg = "ES256";
        //Signature Keys
        const privateKey = await jose.importJWK(REACT_APP_SIGNATURE_PRIVATE_KEY, alg);
        const nowTime = moment().unix();
        const futureTime = moment().add(2, "minutes").unix();
        const jwt = await new jose.SignJWT({
          sub: REACT_APP_CLIENT_ID,
          iss: REACT_APP_CLIENT_ID,
          aud: REACT_APP_JWTTOKENURL,
          iat: nowTime,
          exp: futureTime,
        })
          .setProtectedHeader({
            alg: "ES256",
            kid: REACT_APP_KID,
            typ: "JWT",
          })
          .sign(privateKey);
       const url = REACT_APP_SPTOKENURL;
        const { data } = await axios.post(
          url,
          new URLSearchParams({
            client_id: REACT_APP_CLIENT_ID,
            redirect_uri: REACT_APP_REDIRECT_URI,
            code: code,
            client_assertion_type:
              "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            grant_type: "authorization_code",
            client_assertion: jwt,
          }),
          {
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          }
        );
      
        //Enc Keys (only for agency use where profile is direct-pii)
       try {
          const descprivateKey = {
            kty: "EC",
            d: "p4YZHS0_BS4VMUayEtt38qi2sMdkhs4JRFlks7HJCD8",
            crv: "P-256",
            x: "0GR5oBa1FINjCZP_W-nR8Yqoz4E_9j7lgCuRPh9PZTA",
            y: "0leGfxdQSJdtubopqhj5uhPVYV3LSd_yf3y2DdRD5No",
          };
    
          const privateKey2 = await jose.importJWK(
            descprivateKey,
            "ECDH-ES+A256KW"
          );
          const { plaintext } = await jose.compactDecrypt(
            data.id_token,
            privateKey2
          );
          const dto = new TextDecoder().decode(plaintext);
          const result = await jose.decodeJwt(dto);
          const NRIC = result.sub.substring(2, 11);
          const UUID= result.sub.substring(14);
          //Return NRIC
          return {
            statusCode: 200,
            body: JSON.stringify({ data: NRIC , UUID: UUID}),
            headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type",
        "Access-Control-Allow-Methods": "GET, POST, OPTION"
            },
          };
          //Return error
        } catch (e) {
          console.log(e);
        } 
      } catch (e) {
        if (e.response?.data) {
          console.log(e.response.data);
          }
       return {
         statusCode: 500,
          body: JSON.stringify({ data: e }),
          headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type",
        "Access-Control-Allow-Methods": "GET, POST, OPTION"
          }, 
        }; 

      }
    
  
    
  }

Step 2: Update request parameters

  • Update request parameters accordingly for the token endpoint with the following attributes:

KeyDescription

client_id

This should be client_id of the registered client provided in the Application configuration for each Application

redirect_uri

The redirect URL being used in this auth session. The value will be validated against the list of redirect URIs that were pre-configured in Application Configuration.

grant_type

The type of grant being requested. This must be set to authorization_code

code

The code issued earlier in the auth session

client_assertion_type

This MUST be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer

client_assertion

A JWT identifying the client as setup in the previous step

Create attributes for all the request parameters and update them accordingly. For the demo singpass application, update all the following fields within the api.js file.

     const REACT_APP_SIGNATURE_PRIVATE_KEY = {
          kty: "EC",
          d: "0GlHbGc8vSnyiB-Lf4_im_WFwrxM0MJjkk96o1-K3JQ",
          crv: "P-256",
          x: "wg11s6ZpBc0my5gT-mYatTZRDhgStyd_0qARVBwAWa4",
          y: "hlVoYWwlCuTMnm79Ppmf3RslIwDRhqdCCnm01PkhA2s"
        }; 
         //Update to the signature JWT private key
        const REACT_APP_ENCRYPTION_PRIVATE_KEY =  {
          kty: "EC",
          d: "p4YZHS0_BS4VMUayEtt38qi2sMdkhs4JRFlks7HJCD8",
          crv: "P-256",
          x: "0GR5oBa1FINjCZP_W-nR8Yqoz4E_9j7lgCuRPh9PZTA",
          y: "0leGfxdQSJdtubopqhj5uhPVYV3LSd_yf3y2DdRD5No"
        }
        //Update to the encryption JWT private key. Mandatory only if profile type is direct-pii-allowed
    const REACT_APP_CLIENT_ID="tLRDBkf1CNy5Rsi34mEKuOD5EpQAwjIq"
//Update to the ClientID obtain from Singpass Developer Portal 
    const REACT_APP_JWTTOKENURL="https://stg-id.singpass.gov.sg"
//Default to staging JWT endpoint
    const REACT_APP_SPTOKENURL="https://stg-id.singpass.gov.sg/token"
//Default to staging token endpoint
    const REACT_APP_KID="testing123"
//Update to the signature JWT kid key ID
    const REACT_APP_REDIRECT_URI="https://singpassdemoapp.netlify.app/callback" 
//Update to the Redirect/Callback URL indicated in Singpass Developer Portal 

Step 3: Call the Token Endpoint and Test

  • After configuring the token request parameter, and calling the server function through a postman call by providing the code retrieved from the authorization endpoint response, upon successful authorization, your application will be responded to with an ID token and access token like the following example.

{
  "access_token" : "XAvWII9OKTwB1GInRMNi+H3cXb0/FHHqHQ+ks2TV1SU=",
  "token_type" : "Bearer",
  "id_token" : "eyJraWQiOiJuZGlfc3RnXzAxIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJzPVM4ODI5MzE0Qix1PTFjMGNlZTM4LTNhOGYtNGY4YS04M2JjLTdhMGU0YzU5ZDZhOSIsImF1ZCI6InVQQVh2RFphZzR2STRMWlp2U0pIaUFndHNBekFFZ3NmIiwiYW1yIjpbInB3ZCIsInN3ayJdLCJpc3MiOiJodHRwczovL3N0Zy1pZC5zaW5ncGFzcy5nb3Yuc2ciLCJleHAiOjE3MTU2NzM1OTYsImlhdCI6MTcxNTY3Mjk5Niwibm9uY2UiOiJGZEJYOVRHSi9qdGRUTEhOYlV0ckRla1FRY2RNRWZtRFlhRVU4eGZtVWlFPSJ9.ppxiB9S4R3v_kDYOpgkIG8hd3D2AH1UP3K1Gc243MF4I6lSQTANVHZ84RTYSRYatDSac2mkuw81EOBNSqgAnsw"
}

Token Endpoint Response Fields

KeyDescription

access_token

A random string that isn’t used.

token_type

The type of token being requested, Bearer only so far.

id_token

The ID token with relevant claims in JWT format signed by the Singpass. Note that the example response body shows a JWS (3-part structure separated by dots), but the format will differ for a JWS in JWE (5-part structure). Refer here for more details about the ID token structure.

Step 4: Decode the ID Token

  • After receiving the ID token, your application needs to decrypt the ID token with your encryption JWT private key to obtain the NRIC/UUID. For the demo singpass application, the following code works as the decrypt function.

//Api.js
{
          const privateKey2 = await jose.importJWK(
            REACT_APP_ENCRYPTION_PRIVATE_KEY,
            "ECDH-ES+A256KW"
          );
          const { plaintext } = await jose.compactDecrypt(
            data.id_token,
            privateKey2
          );
          const dto = new TextDecoder().decode(plaintext);
          const result = await jose.decodeJwt(dto);
          const NRIC = result.sub.substring(2, 11);
          const UUID= result.sub.substring(14);
          //Return NRIC
          return {
            statusCode: 200,
            body: JSON.stringify({ data: NRIC , UUID: UUID}),
            headers: {
              "Content-Type": "application/json",
              "Access-Control-Allow-Origin": "*",
              "Access-Control-Allow-Headers": "Content-Type",
              "Access-Control-Allow-Methods": "GET, POST, OPTION"
            },
          };

If you have received the response successfully, congratulations you have successfully integrated with Singpass.

Generally follows OIDC error response specifications. For more information, please refer to Token Error Response specifications.