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.
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.
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
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:
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:
- 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
>
<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.