Custom Integration
You will need to do a custom integration if your app uses a language that does not have a SDK or you would like to implement your own sgID integration.
When a user tries to login on your application with sgID, you need to:
Step 1: Generate a PKCE pair
Proof Key for Code Exchange (PKCE) is an OAuth 2.0 enhancement and protects against various potential vulnerabilities such as the authorization code interception attack. A unique PKCE pair must be generated for each request and consists of a code_verifier
and a code_challenge
.
code_verifier = 'bbGcObXZC1YGBQZZtZGQH9jsyO1vypqCGqnSU_4TI5S'
code_challenge = 'zaqUHoBV3rnhBF2g0Gkz1qkpEZXHqi2OrPK1DqRi-Lk'
Code verifier
The code_verifier
should be a high-entropy cryptographic random string with an ABNF as follows
ABNF:
code-verifier = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39
Code challenge
The code_challenge
should be generated from the code_verifier
using the S256
code challenge method. The S256
transformation is described below together with the ABNF of the code_challenge
.
S256 Transformation:
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
ABNF:
code-challenge = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39
// Note that the ABNF for code-challenge is identical to the ABNF for code-verifier
The code_challenge
must be sent to the sgID authorization server in the next step while the code_verifier
must be provided while exchanging the code for the access token. Our server will use these values to ensure that the request is legitimate and not a potential attack.
Some crypto libraries you could use to generate these values could be
Node.js - pkce-challenge
Python - pkce
Step 2: Create an authorization URL to redirect to
To allow your user to login into your app with sgID, you need to create an sgID authorization URL to redirect your user's browser to so that they can authenticate with Singpass.:
https://api.id.gov.sg/v2/oauth/authorize?
response_typecode
&client_id=abc
&redirect_uri=https://example.com/callback
&scope=openid%20myinfo.name%20myinfo.passport_expiry_date%20myinfo.nric_number
&code_challenge=zaqUHoBV3rnhBF2g0Gkz1qkpEZXHqi2OrPK1DqRi-Lk
&nonce=BQO8SV3ALIYA808IZ8O7PKWRI8A8X6MI
&state=tk39drykro3
You will need to supply the following query string parameters:
response_type
Must be set to code
because sgID only supports the authorization code flow.
client_id
Provided to you during client registration.
redirect_uri
The callback URL that you provided during client registration.
scope
A URL-encoded string of the scopes your client will request for.
code_challenge
The code challenge used for PKCE. Used to prevent authorization code interception attack and CSRF.
nonce (optional)
Randomly generated string to be returned in the id_token. Used to prevent replay attacks. Refer to the OpenID Connect documentation for implementation details.
state (optional)
A unique and non-guessable value associated with each authentication request about to be initiated.
Step 3: Exchange auth code for access token and ID token
After the user authenticates with the Singpass mobile app, the user's browser will be redirected back to the callback URL you provided, together with the authorization code and a state value.
https://example.com/callback?
code=someAuthCode
&state=tk39drykro3
To exchange the code
for the access token andID token, make a POST
request to
https://api.id.gov.sg/v2/oauth/token
with the following request body parameters:
client_id
Provided to you during client registration.
client_secret
Provided to you during client registration.
code
The value returned to you as part of the callback URL.
grant_type
Must be set to authorization_code.
redirect_uri
The callback URL that you provided during client registration.
You should receive a response with the following attributes:
access_token
Access Token to be used with retrieving the encrypted payload from user info endpoint
id_token
JWT token with the associated user claims. Encodes the following:
iss (hostname)
sub (end user's unique identifier)
aud (client id)
nonce (only returned if provided in authorization url)
exp (seconds before auth request and access token expires)
iat (timestamp at which id token was issued)
Example JSON response body:
{
"access_token": "I6zGnxYTy4fZubtb7LcG48K1fHWb5b",
"id_token": "eyJhbGciOiJ...[truncated]...L6zm6LaWfkBoA",
}
Step 4: Request for user info with access token
Once you have the access token, you can use it to request information about the user corresponding to the scopes that you requested. To do so, make a GET
request to
https://api.id.gov.sg/v2/oauth/userinfo
with the access token you received in the previous step. Example request:
GET /v2/oauth/userinfo HTTP/1.1
Host: api.id.gov.sg
Authorization: Bearer I6zGnxYTy4fZubtb7LcG48K1fHWb5b
Content-Length: 57
Content-Type: application/json
You should receive a response with the following attributes:
sub
End user's unique identifier for your client - This is the same value as the sub
claim in the id_token
returned from the previous response.
Note that as part of sgID's privacy-preserving measures, each end user's unique identifier is different for each sgID client
key
An AES-128-GCM symmetric key, or a block key, that is encrypted with your client's RSA-2048 public key.
data
JSON object which contains the data you requested in your application scope. To prevent sgID from reading the data, the payload is encrypted with the block key referenced in the definition for the key
attribute in the same response body.
Refer to the following section for instructions on decrypting the payload.
Example JSON response body:
{
"sub": "abcdef",
"key": "eyJhbGcDpgYRL4chyXTjgim...[truncated]...Gxa2tO7nghnu-ewD5ZqA",
"data": {
// Note: this will contain all the scopes you requested
"myinfo.nric_number": "eyJlbmMiOiJ...[truncated]...QafqHmGERc3A",
"myinfo.name": "eyJlbmMiOi...[truncated]...UgJ9hDSTNLVw",
"myinfo.passport_expiry_date": "eyJlbmMiOi...[truncated]...UvS41pKk9VKQ",
}
}
Step 5: Decrypt the user info payload
As part of sgID's privacy-preserving measures, user data is transmitted in encrypted form, so that the sgID server is unable to read the data being transacted. The data is encrypted with a block key, which is itself encrypted with your client's public key so that only your client has access to the block key.
Therefore, to obtain the user data in plaintext, you will need to:
Decrypt the
key
received from the user info response with your client private key. This will give you the block key.Decrypt the
data
received from the user info response with the block key you have just obtained.

data
you received from the user info endpointExamples decryption:
// We use the node-jose package for working with JWEs and JWKs
// https://github.com/cisco/node-jose
import { JWE, JWK } from 'node-jose'
/**
* Decrypts data into an object of
* plaintext key-value pairs
*
* @param {string} encKey - encrypted block key
* @param {array} block - data
* @param {string} privateKeyPem - private key in pem format
* @returns {object}
*/
async function decryptData(encKey, block, privateKeyPem) {
const result = {}
// Decrypted encKey to get block key
const privateKey = await JWK.asKey(privateKeyPem, 'pem')
const key = await JWE.createDecrypt(privateKey).decrypt(encKey)
// Parse the block key
const decryptedKey = await JWK.asKey(key.plaintext, 'json')
// Decrypt data
for (const [key, value] of Object.entries(block)) {
const { plaintext } = await JWE.createDecrypt(decryptedKey).decrypt(value)
result[key] = plaintext.toString('ascii')
}
return result
}from jwcrypto import jwk, jwe
Example of decrypted data:
{
"myinfo.name": "TIMOTHY TAN CHENG GUAN",
"myinfo.nric_number": "S3000786G",
"myinfo.passport_expiry_date": "2024-01-01",
}
Last updated