What is Singpass authentication?

Using SingPass/CorpPass user can login to the application and also retrieve the government data to fill the form automatically for KYC purpose.

What is the role of Singpass?

Singpass facilitates millions of personal and corporate transactions annually, covering a significant portion of the population aged 15 and above. The high adoption rate makes it one of the most widely used national digital identity systems globally. The numbers provided in the statement highlight the system’s effectiveness in streamlining digital interactions and promoting efficient access to government services for the citizens and residents of Singapore. However, for the most up-to-date and accurate information, you may want to check the latest statistics and official announcements from the relevant authorities in Singapore.

How secure is Singpass?

Yes, it is secure due to Multi-Factor Authentication (MFA), Secure Encryption, Identity Verification, Continuous Monitoring and Updates and provide proper User Education.

I am going to develop back-end and front end code both using the JS tech stack node and angular, and integrate both for a working demo.

Back-end code will work as a service layer that will consume the MyInfo API’s. It would authenticate the SingPass. Once SingPass is authenticated and authorized from MyInfo. MyInfo will return the user data and the requested government data to the service layer.

Service layer will send the user data to the front end, and angular front end will use that data to fill the form, some fields are not permitted to edit.

MyInfo enables citizens and Singapore residents to manage the use of their personal data for simpler online transactions. To gain access to the protected domain, partners and app developers will have to deal with the organization’s authentication and authorization mechanism, which varies from domain to domain. source: ndi-api.gov.sg

SingPass authentication example

Data Available:

Gov Provided: This data should not be edited on digital forms.
User Provided: This data should continue to be editable on digital forms.

Data Type:

Personal,
Contact,
Income & CPF,
Education & Employment,
Family,
Vehicle & Driver License,
Property

Logical Overview:

source: ndi-api.gov.sg

MyInfo currently only supports web-based integration. Native mobile applications are not currently supported. Integrating with MyInfo requires your application to invoke 3 different APIs as part of the OAuth2.0 authorization code flow.

API’s:

  1. Authorize

SingPass authentication process followed by consent page approval by the user. At the end of this process, system will return to you a short-lived “authorization code”. This API is triggered over the browser via 302 redirect.

Key Requirement:
i. Private key: provided/generated by CA
ii. Public certificate: provided/generated by CA
iii. ClientID : provided by myinfo during on boarding
iv. Client secret : provided during the on boarding
v. Callback: App page to redirect.

Sandbox API: https://sandbox.api.myinfo.gov.sg/com/v3/authorise
Test API: https://test.api.myinfo.gov.sg/com/v3/authorise
Production API: https://api.myinfo.gov.sg/com/v3/authorise

2. Token

This API is invoked by your application server to obtain an “access token”, which can be used to call the person API for the actual data. Your application needs to provide a valid “authorization code” from the authorize API in exchange for the “access token”. The “access token” will be valid for 30 minutes.

Note: This API is a server-to-server call (does not go through browser)

Sandbox API: https://sandbox.api.myinfo.gov.sg/com/v3/token
Test API: https://test.api.myinfo.gov.sg/com/v3/token
Production API: https://api.myinfo.gov.sg/com/v3/token

3. Person

This API returns a JSON response with the personal data that was requested. Your application needs to provide a valid “access token” in exchange for the JSON data.

Once your application receives this JSON data, you can use this data to populate the online form on your application.

Note: This API is a server-to-server call (does not go through browser)

Sandbox API: https://sandbox.api.myinfo.gov.sg/com/v3/person
Test API: https://test.api.myinfo.gov.sg/com/v3/person
Production API: https://api.myinfo.gov.sg/com/v3/person

Developing the back end API using the node.js

package.json :

{
  "name": "myinfoprobackend",
  "version": "1.8.0",
  "author": "seoinfotech",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "bluebird": "^3.4.6",
    "body-parser": "^1.18.3",
    "colors": "^1.2.1",
    "cookie-parser": "~1.4.3",
    "cors": "^2.8.5",
    "debug": "^4.1.0",
    "easy-soap-request": "^3.3.1",
    "express": "^4.13.4",
    "jose": "^0.3.2",
    "jsonwebtoken": "^8.2.1",
    "lodash": "^4.17.11",
    "morgan": "^1.9.1",
    "node-jose": "^1.1.0",
    "nonce": "^1.0.4",
    "pug": "^2.0.0-rc.4",
    "querystring": "^0.2.0",
    "request": "^2.88.2",
    "request-promise": "^4.2.5",
    "serve-favicon": "^2.5.0",
    "superagent": "^3.1.0",
    "superagent-bluebird-promise": "^4.1.0",
    "urlsafe-base64": "^1.0.0"
  }
}

app.js

let express = require("express");
let path = require("path");
let logger = require("morgan");
let cookieParser = require("cookie-parser");
let bodyParser = require("body-parser");
let cors = require("cors");
let routes = require("./routes/index");
let app = express();

app.use(cors());

// view engine setup
app.use(logger("dev"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

app.use("/", routes);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  var err = new Error("Not Found");
  err.status = 404;
  next(err);
});

// error handlers
app.use(function (err, req, res, next) {
  console.log("err", err);
  res.status(err.status || 500);
});

module.exports = app;

config/config.js

process.env.TEST_PUBLIC_CERT = "./ssl/stg-demoapp-client-privatekey-2018.pem";
process.env.TEST_PRIVATE_KEY = "./ssl/staging_myinfo_public_cert.cer";
process.env.PROD_PUBLIC_CERT = "";
process.env.PROD_PRIVATE_KEY = "";
process.env.ENV = "test"; //sandbox, production

const config = {
  test: {
    attributes:
      "vehicles.vehicleno,vehicles.make,vehicles.model,cpfemployers,passtype,passstatus,passexpirydate,uinfin,name,sex,race,nationality,dob,email,mobileno,regadd,housingtype,hdbtype,marital,edulevel,noa-basic,ownerprivate,cpfcontributions,cpfbalances",
    key: {
      PRIVATE: process.env.TEST_PUBLIC_CERT,
      PUBLIC: process.env.TEST_PRIVATE_KEY,
    },
    secret: {
      CLIENT_ID: "clientId",
      CLIENT_SECRET: "clientSecret",
      REDIRECT_URL: "callbackurl",
    },
    api: {
      //TEST ENVIRONMENT (with PKI digital signature)
      AUTHORISE: https://test.api.myinfo.gov.sg/com/v3/authorise",
      TOKEN: "https://test.api.myinfo.gov.sg/com/v3/token",
      PERSON: "https://test.api.myinfo.gov.sg/com/v3/person",
      AUTH_LEVEL: "L2",
    },
    server: {
      PORT: 4200,
    },
  },
};

module.exports = config;

bin/www

#!/usr/bin/env node

/**
 * Module dependencies.
 */

let app = require("../app");
let debug = require("debug")("example-oauth-client:server");
let http = require("http");
const config = require("./../config/config");

/**
 * Get port from environment and store in Express.
 */

let port = normalizePort(config[process.env.ENV].server.PORT || "4200");
app.set("port", port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on("error", onError);
server.on("listening", onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== "listen") {
    throw error;
  }

  var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case "EACCES":
      console.error(bind + " requires elevated privileges");
      process.exit(1);
      break;
    case "EADDRINUSE":
      console.error(bind + " is already in use");
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  let addr = server.address();
  let bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
  debug("Listening on " + bind);
}

lib/security/security.js

const _ = require("lodash");
const path = require("path");
const fs = require("fs");
const nonce = require("nonce")();
const crypto = require("crypto");
const qs = require("querystring");
const jwt = require("jsonwebtoken");
const jose = require("node-jose");
const URLSafeBase64 = require("urlsafe-base64");
const colors = require("colors");

let security = {};

// Sorts a JSON object based on the key value in alphabetical order
function sortJSON(json) {
  if (_.isNil(json)) {
    return json;
  }

  let newJSON = {};
  let keys = Object.keys(json);
  keys.sort();

  for (key in keys) {
    newJSON[keys[key]] = json[keys[key]];
  }

  return newJSON;
}

/**
 * @param url Full API URL
 * @param params JSON object of params sent, key/value pair.
 * @param method
 * @param appId ClientId
 * @param keyCertContent Private Key Certificate content
 * @param keyCertPassphrase Private Key Certificate Passphrase
 * @returns {string}
 */
function generateSHA256withRSAHeader(
  url,
  params,
  method,
  strContentType,
  appId,
  keyCertContent,
  keyCertPassphrase
) {
  const nonceValue = nonce();
  const timestamp = new Date().getTime();

  // A) Construct the Authorisation Token
  let defaultApexHeaders = {
    app_id: appId, // App ID assigned to your application
    nonce: nonceValue, // secure random number
    signature_method: "RS256",
    timestamp: timestamp, // Unix epoch time
  };

  // Remove params unless Content-Type is "application/x-www-form-urlencoded"
  if (
    method == "POST" &&
    strContentType != "application/x-www-form-urlencoded"
  ) {
    params = {};
  }

  // B) Forming the Signature Base String

  // i) Normalize request parameters
  let baseParams = sortJSON(_.merge(defaultApexHeaders, params));

  let baseParamsStr = qs.stringify(baseParams);
  baseParamsStr = qs.unescape(baseParamsStr);

  // ii) construct request URL ---> url is passed in to this function

  // iii) concatenate request elements
  let baseString = method.toUpperCase() + "&" + url + "&" + baseParamsStr;

  console.log("Formulated Base String".green);
  console.log(
    "Base String generated by your application that will be signed using your private key."
      .grey
  );
  console.log(baseString);

  // C) Signing Base String to get Digital Signature
  let signWith = {
    key: fs.readFileSync(keyCertContent, "utf8"),
  };

  if (!_.isUndefined(keyCertPassphrase) && !_.isEmpty(keyCertPassphrase))
    _.set(signWith, "passphrase", keyCertPassphrase);

  // Load pem file containing the x509 cert & private key & sign the base string with it.
  let signature = crypto
    .createSign("RSA-SHA256")
    .update(baseString)
    .sign(signWith, "base64");

  console.log("Digital Signature:".green);
  console.log(
    "Signature produced by signing the above Base String with your private key."
      .grey
  );
  console.log(signature);

  // D) Assembling the Header
  let strApexHeader =
    'PKI_SIGN timestamp="' +
    timestamp +
    '",nonce="' +
    nonceValue +
    '",app_id="' +
    appId +
    '",signature_method="RS256"' +
    ',signature="' +
    signature +
    '"';

  return strApexHeader;
}

/**
 * @param url API URL
 * @param params JSON object of params sent, key/value pair.
 * @param method
 * @param appId API ClientId
 * @param passphrase API Secret or certificate passphrase
 * @returns {string}
 */
security.generateAuthorizationHeader = function (
  url,
  params,
  method,
  strContentType,
  authType,
  appId,
  keyCertContent,
  passphrase
) {
  if (authType == "L2") {
    return generateSHA256withRSAHeader(
      url,
      params,
      method,
      strContentType,
      appId,
      keyCertContent,
      passphrase
    );
  } else {
    return "";
  }
};

// Verify & Decode JWS or JWT
security.verifyJWS = function verifyJWS(jws, publicCert) {
  // verify token
  // ignore notbefore check because it gives errors sometimes if the call is too fast.
  try {
    let decoded = jwt.verify(jws, fs.readFileSync(publicCert, "utf8"), {
      algorithms: ["RS256"],
      ignoreNotBefore: true,
    });
    return decoded;
  } catch (error) {
    console.error("Error with verifying and decoding JWS: %s".red, error);
    throw "Error with verifying and decoding JWS";
  }
};

// Decrypt JWE using private key
security.decryptJWE = function decryptJWE(
  header,
  encryptedKey,
  iv,
  cipherText,
  tag,
  privateKey
) {
  console.log(
    "Decrypting JWE".green +
      " (Format: " +
      "header".red +
      "." +
      "encryptedKey".cyan +
      "." +
      "iv".green +
      "." +
      "cipherText".magenta +
      "." +
      "tag".yellow +
      ")"
  );
  console.log(
    header.red +
      "." +
      encryptedKey.cyan +
      "." +
      iv.green +
      "." +
      cipherText.magenta +
      "." +
      tag.yellow
  );
  return new Promise((resolve, reject) => {
    let keystore = jose.JWK.createKeyStore();

    console.log(Buffer.from(header, "base64").toString());

    let data = {
      type: "compact",
      ciphertext: cipherText,
      protected: header,
      encrypted_key: encryptedKey,
      tag: tag,
      iv: iv,
      header: JSON.parse(jose.util.base64url.decode(header).toString()),
    };
    keystore
      .add(fs.readFileSync(privateKey, "utf8"), "pem")
      .then(function (jweKey) {
        // {result} is a jose.JWK.Key
        jose.JWE.createDecrypt(jweKey)
          .decrypt(data)
          .then(function (result) {
            resolve(JSON.parse(result.payload.toString()));
          })
          .catch(function (error) {
            reject(error);
          });
      });
  }).catch((error) => {
    console.error("Error with decrypting JWE: %s".red, error);
    throw "Error with decrypting JWE";
  });
};

module.exports = security;

You need to store public and private key in ssl folder

\ssl\stg-demoapp-client-privatekey-2018.pem,
\ssl\stg-demoapp-client-publiccert-2018.pem

Routes file: routes/index.js

const express = require("express");
const router = express.Router();
const config = require("./../config/config");
const restClient = require("superagent-bluebird-promise");
const path = require("path");
const url = require("url");
const util = require("util");
const Promise = require("bluebird");
const _ = require("lodash");
const querystring = require("querystring");
const securityHelper = require("../lib/security/security");
const crypto = require("crypto");
const colors = require("colors");

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const environment = process.env.ENV || "test";

// public key from MyInfo Consent Platform given to you during onboarding for RSA digital signature
const _publicCertContent = config[environment].key.PUBLIC;
// your private key for RSA digital signature
const _privateKeyContent = config[environment].key.PRIVATE;
// your client_id provided to you during onboarding
const _clientId = config[environment].secret.CLIENT_ID;
// your client_secret provided to you during onboarding
const _clientSecret = config[environment].secret.CLIENT_SECRET;
// redirect URL for your web application
const _redirectUrl = config[environment].secret.REDIRECT_URL;

// URLs for MyInfo APIs
const _authLevel = config[environment].api.AUTH_LEVEL;
const _authApiUrl = config[environment].api.AUTHORISE;
const _tokenApiUrl = config[environment].api.TOKEN;
const _personApiUrl = config[environment].api.PERSON;
const _attributes = config[environment].attributes;

// function for getting environment variables to the frontend
router.get("/getEnv", function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  if (_clientId == undefined || _clientId == null)
    res.jsonp({
      status: "ERROR",
      msg: "client_id not found",
    });
  else
    res.jsonp({
      status: "OK",
      clientId: _clientId,
      redirectUrl: _redirectUrl,
      authApiUrl: _authApiUrl,
      attributes: _attributes,
      authLevel: _authLevel,
    });
});

// function for frontend to call backend
router.post("/getPersonData", function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  let code = req.body.code;
  let request;

  // **** CALL TOKEN API ****
  request = createTokenRequest(code);
  request.buffer(true).end(function (callErr, callRes) {
    if (callErr) {
      console.error("Token Call Error: ", callErr.status);
      console.error(callErr.response.req.res.text);
      res.jsonp({
        status: "ERROR",
        msg: callErr,
      });
    } else {
      let data = {
        body: callRes.body,
        text: callRes.text,
      };
      console.log("Response from Token API:".green);
      console.log(JSON.stringify(data.body));

      let accessToken = data.body.access_token;
      if (accessToken == undefined || accessToken == null) {
        res.jsonp({
          status: "ERROR",
          msg: "ACCESS TOKEN NOT FOUND",
        });
      }
      // everything ok, call person API
      callPersonAPI(accessToken, res);
    }
  });
});

function callPersonAPI(accessToken, res) {
  console.log("AUTH_LEVEL:".green, _authLevel);

  // validate and decode token to get SUB
  let decoded = securityHelper.verifyJWS(accessToken, _publicCertContent);
  if (decoded == undefined || decoded == null) {
    res.jsonp({
      status: "ERROR",
      msg: "INVALID TOKEN",
    });
  }

  console.log("Decoded Access Token:".green);
  console.log(JSON.stringify(decoded));

  let sub = decoded.sub;
  if (sub == undefined || sub == null) {
    res.jsonp({
      status: "ERROR",
      msg: "SUB NOT FOUND",
    });
  }

  // **** CALL PERSON API ****
  let request = createPersonRequest(sub, accessToken);

  // Invoke asynchronous call
  request.buffer(true).end(function (callErr, callRes) {
    if (callErr) {
      console.error("Person Call Error: ", callErr.status);
      console.error(callErr.response.req.res.text);
      res.jsonp({
        status: "ERROR",
        msg: callErr,
      });
    } else {
      let data = {
        body: callRes.body,
        text: callRes.text,
      };
      let personData = data.text;
      if (personData == undefined || personData == null) {
        res.jsonp({
          status: "ERROR",
          msg: "PERSON DATA NOT FOUND",
        });
      } else {
        if (_authLevel == "L0") {
          console.log("Person Data:".green);
          console.log(personData);
          personData = JSON.parse(personData);
          // personData = securityHelper.verifyJWS(personData, _publicCertContent);
          if (personData == undefined || personData == null) {
            res.jsonp({
              status: "ERROR",
              msg: "INVALID DATA OR SIGNATURE FOR PERSON DATA",
            });
          }
          // successful. return data back to frontend
          res.jsonp({
            status: "OK",
            text: personData,
          });
        } else if (_authLevel == "L2") {
          console.log("Person Data (JWE):".green);
          console.log(personData);

          let jweParts = personData.split("."); // header.encryptedKey.iv.ciphertext.tag
          securityHelper
            .decryptJWE(
              jweParts[0],
              jweParts[1],
              jweParts[2],
              jweParts[3],
              jweParts[4],
              _privateKeyContent
            )
            .then((personDataJWS) => {
              if (personDataJWS == undefined || personDataJWS == null) {
                res.jsonp({
                  status: "ERROR",
                  msg: "INVALID DATA OR SIGNATURE FOR PERSON DATA",
                });
              }
              console.log("Person Data (JWS):".green);
              console.log(JSON.stringify(personDataJWS));

              let decodedPersonData = securityHelper.verifyJWS(
                personDataJWS,
                _publicCertContent
              );
              if (decodedPersonData == undefined || decodedPersonData == null) {
                res.jsonp({
                  status: "ERROR",
                  msg: "INVALID DATA OR SIGNATURE FOR PERSON DATA",
                });
              }

              console.log("Person Data (Decoded):".green);
              console.log(JSON.stringify(decodedPersonData));
              // successful. return data back to frontend
              res.jsonp({
                status: "OK",
                text: decodedPersonData,
              });
            })
            .catch((error) => {
              console.error("Error with decrypting JWE: %s".red, error);
            });
        } else {
          throw new Error("Unknown Auth Level");
        }
      } // end else
    }
  }); //end asynchronous call
}

// function to prepare request for TOKEN API
function createTokenRequest(code) {
  let cacheCtl = "no-cache";
  let contentType = "application/x-www-form-urlencoded";
  let method = "POST";

  // assemble params for Token API
  let strParams =
    "grant_type=authorization_code" +
    "&code=" +
    code +
    "&redirect_uri=" +
    _redirectUrl +
    "&client_id=" +
    _clientId +
    "&client_secret=" +
    _clientSecret;
  let params = querystring.parse(strParams);

  // assemble headers for Token API
  let strHeaders = "Content-Type=" + contentType + "&Cache-Control=" + cacheCtl;
  let headers = querystring.parse(strHeaders);

  console.log("headers", headers);

  // Add Authorisation headers for connecting to API Gateway
  let authHeaders = null;
  if (_authLevel == "L0") {
    // No headers
  } else if (_authLevel == "L2") {
    authHeaders = securityHelper.generateAuthorizationHeader(
      _tokenApiUrl,
      params,
      method,
      contentType,
      _authLevel,
      _clientId,
      _privateKeyContent,
      _clientSecret
    );
    console.log("authHeaders", authHeaders);
  } else {
    throw new Error("Unknown Auth Level");
  }

  if (!_.isEmpty(authHeaders)) {
    _.set(headers, "Authorization", authHeaders);
  }

  console.log("Request Header for Token API:".green);
  console.log(JSON.stringify(headers));

  let request = restClient.post(_tokenApiUrl);

  // Set headers
  if (!_.isUndefined(headers) && !_.isEmpty(headers)) request.set(headers);

  // Set Params
  if (!_.isUndefined(params) && !_.isEmpty(params)) request.send(params);

  return request;
}

// function to prepare request for PERSON API
function createPersonRequest(sub, validToken) {
  let url = _personApiUrl + "/" + sub + "/";
  let cacheCtl = "no-cache";
  let method = "GET";

  // assemble params for Person API
  let strParams = "client_id=" + _clientId + "&attributes=" + _attributes;

  let params = querystring.parse(strParams);

  // assemble headers for Person API
  let strHeaders = "Cache-Control=" + cacheCtl;
  let headers = querystring.parse(strHeaders);

  // Add Authorisation headers for connecting to API Gateway
  let authHeaders = securityHelper.generateAuthorizationHeader(
    url,
    params,
    method,
    "", // no content type needed for GET
    _authLevel,
    _clientId,
    _privateKeyContent,
    _clientSecret
  );

  // NOTE: include access token in Authorization header as "Bearer " (with space behind)
  if (!_.isEmpty(authHeaders)) {
    _.set(headers, "Authorization", authHeaders + ",Bearer " + validToken);
  } else {
    _.set(headers, "Authorization", "Bearer " + validToken);
  }

  console.log("Request Header for Person API:".green);
  console.log(JSON.stringify(headers));
  // invoke person API
  let request = restClient.get(url);

  // Set headers
  if (!_.isUndefined(headers) && !_.isEmpty(headers)) request.set(headers);

  // Set Params
  if (!_.isUndefined(params) && !_.isEmpty(params)) request.query(params);

  return request;
}

router.get("/test", (req, res, next) => {
  return res.json("test");
});

module.exports = router;

once all file setup is done, it can be stared using the command npm install

api url is http://localhost:4200

Developing Front End using the Angular

package.json

{
  "name": "myinfoprototype",
  "version": "0.0.1",
  "author": "seoinfotech",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --port 3001",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~9.1.0",
    "@angular/common": "~9.1.0",
    "@angular/compiler": "~9.1.0",
    "@angular/core": "~9.1.0",
    "@angular/forms": "~9.1.0",
    "@angular/platform-browser": "~9.1.0",
    "@angular/platform-browser-dynamic": "~9.1.0",
    "@angular/router": "~9.1.0",
    "bootstrap": "^4.4.1",
    "jquery": "^3.4.1",
    "ngx-spinner": "^9.0.2",
    "popper.js": "^1.16.1",
    "rxjs": "~6.5.4",
    "tslib": "^1.10.0",
    "zone.js": "~0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.901.0",
    "@angular/cli": "~9.1.0",
    "@angular/compiler-cli": "~9.1.0",
    "@angular/language-service": "~9.1.0",
    "@types/node": "^12.11.1",
    "@types/jasmine": "~3.5.0",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "^5.1.2",
    "jasmine-core": "~3.5.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.4.1",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "~2.1.0",
    "karma-jasmine": "~3.0.1",
    "karma-jasmine-html-reporter": "^1.4.2",
    "protractor": "~5.4.3",
    "ts-node": "~8.3.0",
    "tslint": "~6.1.0",
    "typescript": "~3.8.3"
  }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { MyinfoComponent } from './myinfo/myinfo.component';
import { NgxSpinnerModule } from 'ngx-spinner';

const appRoutes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'myinfo' },
  { path: 'myinfo', component: MyinfoComponent },
  { path: 'callback', component: MyinfoComponent },
  { path: '404', component: PageNotFoundComponent },
  { path: '**', redirectTo: '/404' },
];

@NgModule({
  declarations: [
    AppComponent,
    PageNotFoundComponent,
    MyinfoComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    RouterModule.forRoot(
      appRoutes
      //{ enableTracing: true } // <-- debugging purposes only
    ),
    BrowserAnimationsModule,
    NgxSpinnerModule,
  ],
  providers: [],
  exports: [RouterModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

api.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
const API_URI = 'http://localhost:4200';

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  constructor(private http: HttpClient) {}
  //private API_KEY = "";

  public getEnv(): Observable<any> {
    return this.http.get<any>(API_URI + '/getEnv');
  }

  public getPersonData(data: any = {}): Observable<any> {
    return this.http.post<any>(API_URI + '/getPersonData', data);
    //.pipe(catchError(this.handleError('getPerson', data)));
  } 

}

myinfo.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { NgxSpinnerService } from 'ngx-spinner';

@Component({
  selector: 'app-myinfo',
  templateUrl: './myinfo.component.html',
  styleUrls: ['./myinfo.component.css'],
})
export class MyinfoComponent implements OnInit {
  constructor(
    private apiService: ApiService,
    private route: ActivatedRoute,
    private router: Router,
    private SpinnerService: NgxSpinnerService
  ) {}
  public prod: String = '';
  public readonly: Boolean = true;

  // Variables for API #1 - Authorise API
  public authApiUrl; // URL for authorise API
  public clientId; // your app_id/client_id provided to you during onboarding
  public redirectUrl; //callback url for your application
  public attributes; // the attributes you are retrieving for your application to fill the form
  public authLevel; // the auth level, determines the flow
  // the purpose of your data retrieval
  public purpose = 'demonstrating MyInfo APIs';
  // randomly generated state
  public state = Math.floor(Math.random() * 100 + 10);

  public form = new FormGroup({
    uinfin: new FormControl('', [Validators.required]),
    name: new FormControl('', [Validators.required]),
    sex: new FormControl('', [Validators.required]),
    race: new FormControl(''),
    nationality: new FormControl(''),
    dob: new FormControl(''),
    email: new FormControl(''),
    mobileno: new FormControl(''),
    regadd: new FormControl({ value: '', disabled: true }),
    housingtype: new FormControl(''),
    marital: new FormControl(''),
    edulevel: new FormControl(''),
    assessableincome: new FormControl(''),
  });

  public productForm = new FormGroup({
    prod: new FormControl(''),
  });

  ngOnInit(): void {
    this.SpinnerService.show();
    this.apiService.getEnv().subscribe((data) => {
      console.log(data);
      if (data.status == 'OK') {
        // successful
        // fill up the application form
        this.clientId = data.clientId;
        this.redirectUrl = data.redirectUrl;
        this.authApiUrl = data.authApiUrl;
        this.attributes = data.attributes;
        this.authLevel = data.authLevel;

        this.callbackHndler();
      } else {
        // error occured
        alert('ERROR:' + JSON.stringify(data.msg));
        this.SpinnerService.hide();
      }
    });
  }

  private callbackHndler(): void {
    // CALLBACK HANDLER
    const currPath = this.route.snapshot.url[0].path;
    const error = this.route.snapshot.queryParamMap.get('error');
    if (currPath === 'callback' && error) {
      const description =
        this.route.snapshot.queryParamMap.get('error-description') || '';
      // error occured
      console.log('callbackHndler', error, description, new Date());
      alert(error + ' : ' + description);
      this.SpinnerService.hide();
      this.router.navigate(['/myinfo']);
    } else if (
      currPath === 'callback' &&
      this.route.snapshot.queryParamMap.get('code')
    ) {
      // call the backend server APIs
      this.callServerAPIs();
      // this.SpinnerService.hide();
    } else {
      //do nothing
      this.SpinnerService.hide();
    }
  }

  // Function for calling API #1 - Authorise
  public callAuthoriseApi(): void {
    this.readonly = true;

    const authoriseUrl =
      this.authApiUrl +
      '?client_id=' +
      this.clientId +
      '&attributes=' +
      this.attributes +
      '&purpose=' +
      this.purpose +
      '&state=' +
      encodeURIComponent(this.state) +
      '&redirect_uri=' +
      this.redirectUrl;
    console.log('authoriseUrl', authoriseUrl, new Date());

    window.location.href = authoriseUrl;
  }

  public callServerAPIs(): void {
    const authCode = this.route.snapshot.queryParamMap.get('code');
    const params = { code: authCode };
    this.apiService.getPersonData(params).subscribe((data) => {
      if (data.status == 'OK') {
        console.log('data', data);
        this.prefillForm(data.text);
      } else {
        // error occured
        console.log('error persondata', data.msg, new Date());
        alert('ERROR:' + JSON.stringify(data.msg));
        this.router.navigate(['/home']);
      }
    });
  }

  public manualEntry(): void {
    this.readonly = false;
    this.form.controls['regadd'].enable();
  }

  // Prefill Online Form with MyInfo data
  public prefillForm(data: any) {
    //TODO: make it dynamic

    let noaData = '';
    let address = '';
    if (data['noa-basic']) {
      noaData = this.str(data['noa-basic'].amount)
        ? this.formatMoney(this.str(data['noa-basic'].amount), 2, '.', ',')
        : '';
    }
    if (data.regadd.type == 'SG') {
      address =
        this.str(data.regadd.country) == ''
          ? ''
          : this.str(data.regadd.block) +
            ' ' +
            this.str(data.regadd.building) +
            ' \n' +
            '#' +
            this.str(data.regadd.floor) +
            '-' +
            this.str(data.regadd.unit) +
            ' ' +
            this.str(data.regadd.street) +
            ' \n' +
            'Singapore ' +
            this.str(data.regadd.postal);
    } else if (data.regadd.type == 'Unformatted') {
      address =
        this.str(data.regadd.line1) + '\n' + this.str(data.regadd.line2);
    }

    this.form.setValue({
      uinfin: this.str(data.uinfin),
      name: this.str(data.name),
      sex: this.str(data.sex),
      race: this.str(data.race),
      nationality: this.str(data.nationality),
      dob: this.str(data.dob),
      email: this.str(data.email),
      marital: this.str(data.marital),
      edulevel: this.str(data.edulevel),
      assessableincome: noaData,
      mobileno:
        this.str(data.mobileno.prefix) +
        this.str(data.mobileno.areacode) +
        ' ' +
        this.str(data.mobileno.nbr),
      housingtype:
        this.str(data.housingtype) == ''
          ? this.str(data.hdbtype)
          : this.str(data.housingtype),
      regadd: address,
    });

    this.prod = 'motor';
    this.SpinnerService.hide();
  }

  // used to output data items with value or desc
  private str(data): any {
    if (!data) return null;
    if (data.value) return data.value;
    else if (data.desc) return data.desc;
    else if (typeof data == 'string') return data;
    else return '';
  }

  private formatMoney(n: any, c: any, d: any, t: any) {
    var c = isNaN((c = Math.abs(c))) ? 2 : c,
      d = d == undefined ? '.' : d,
      t = t == undefined ? ',' : t,
      s = n < 0 ? '-' : '',
      i: any = String(parseInt((n = Math.abs(Number(n) || 0).toFixed(c)))),
      j = (j = i.length) > 3 ? j % 3 : 0;

    return (
      s +
      (j ? i.substr(0, j) + t : '') +
      i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + t) +
      (c
        ? d +
          Math.abs(n - i)
            .toFixed(c)
            .slice(2)
        : '')
    );
  }

  public submit() {
    console.log(this.form.value);
    alert('Form submitted successfully');
  }
}

myinfo.component.html

<div class="container">

  <div class="dropdown">
    <select [(ngModel)]="prod" class="form-control">
      <option selected="selected" value="">Select</option>
      <option value="myform">My Form</option>
    </select>
  </div>

  <h1>Selected: {{ prod || "" }}</h1>

  <div *ngIf="prod" class="card">
    <div class="card-header">
      Registration
    </div>
    <div class="card-body">
      <h5 class="card-title">Fill the form to register</h5>
      <p class="card-text">
        or fill the form using your NRIC / Singpass
      </p>
      <button class="btn btn-primary" (click)="callAuthoriseApi()">
        MyInfo</button
      >&nbsp;
      <button class="btn btn-success" (click)="manualEntry()">
        Fill Manually
      </button>
    </div>
  </div>
  <form [formGroup]="form" (ngSubmit)="submit()">
    <div *ngIf="prod" class="card">
      <div class="card-header">
        User Form
      </div>
      <div class="card-body">
        <div class="row justify-content-around">
          <div class="col-md-12 col-lg-12 form-box mb-4">
            <h3>Personal Information</h3>
            <hr />
            <div class="form-group">
              <label>NRIC</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="uinfin"
                  id="uinfin"
                  [required]="true"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Full Name</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="name"
                  id="name"
                  [required]="true"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Sex</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="sex"
                  id="sex"
                  [required]="true"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Race</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="race"
                  id="race"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Nationality</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="nationality"
                  id="nationality"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Date Of Birth</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="dob"
                  id="dob"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Email</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="email"
                  id="email"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Mobile Number</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="mobileno"
                  id="mobileno"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Registered Address</label>
              <div class="input-group">
                <textarea
                  cols="50"
                  rows="3"
                  [disabled]="true"
                  formControlName="regadd"
                  id="regadd"
                ></textarea>
              </div>
            </div>
            <div class="form-group">
              <label>Housing Type</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="housingtype"
                  id="housingtype"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Marital Status</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="marital"
                  id="marital"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label>Highest Education Level</label>
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="edulevel"
                  id="edulevel"
                  [readonly]="readonly"
                />
              </div>
            </div>
            <div class="form-group">
              <label
                >Notice of Assessment - Latest Assessable Income (SGD)</label
              >
              <div class="input-group">
                <input
                  type="text"
                  class="form-control"
                  formControlName="assessableincome"
                  id="assessableincome"
                  [readonly]="readonly"
                />
              </div>
            </div>
          </div>

          <div class="col-md-12 text-center">
            <button
              class="btn btn-primary"
              type="submit"
              [disabled]="!form.valid"
            >
              Submit
            </button>
          </div>
        </div>
      </div>
    </div>
  </form>
</div>

<ngx-spinner
  bdColor="rgba(51, 51, 51, 0.8)"
  size="default"
  type="ball-spin-clockwise"
>
</ngx-spinner>

Once files setup is done. Start the front end application using npm start, open localhost:3001/

Please comment us in case if you are looking the full code or anything is not working.

Similar Posts