Getting Started: Integration Guide
Initial Requirements
Items Prepared by Relying Party or Client
To integrate SSO with INApas, the client must prepare the following:
1. Name
Provide a valid and registered name of the entity or business owner.
2. Application Name
Specify the application name to be used as the client_id during the authentication process.
3. Business Information
Prepare the following details for the application:
-
App Icon or Logo
An application logo or icon with a minimum size of 256x256 pixels for visual identification. -
Application Description
A brief summary of the application's functionality and purpose. -
Official Website or App Store Link
Provide a link to the official website or the app's listing in the Play Store/App Store. -
Terms of Service (ToS) Link
Include a link to the terms of service governing the application's use. -
Privacy Policy Link
Provide a link to the privacy policy that outlines how user data is processed and protected.
4. Basic Client Information
Provide the following technical details:
a. Redirect URL
-
Define the URL where users will be redirected after authentication.
Example:https://example.com/connect. -
Ensure this endpoint is active and capable of processing authorization codes or tokens.
b. Allowed CORS Origin
- Specify the client's domain or origin to avoid cross-origin errors.
Example:example.com/*.
c. Audience
- Define the identity of the application or service being accessed via OpenID.
Example:INAKU.
d. Callback URL
- Specify the endpoint for handling callback data from the OIDC provider.
Example:http://localhost:3000/callback.
5. Key Pair Generation
To secure communication between the client application and the authentication provider, generate a key pair using the Elliptic Curves P-512 algorithm. This method ensures a fast and secure implementation of the cryptographic process.
- Public Key: Used by OpenID as JWKS (JSON Web Key Set) to verify JWT (JSON Web Token) signatures and enable secure communication.
- Private Key: Stored securely on your server and used for signing requests. It ensures the authenticity and integrity of data exchanged between the client and server.
We utilize:
ES512for signing JSON Web Tokens (JWT) to verify the identity of the client and secure data exchanges.- JWE (JSON Web Encryption) using
ECDH-ES+A256KWfor encrypting sensitive data, ensuring confidentiality and secure transmission of user information
For example key generation, refer to:
Generate Key Example: GitHub - inadigital-inapas/kuncy.
6. Configuring OpenID on the Server
To complete the integration, send the following information to the OpenID (INApas):
- Redirect URL
- Allowed CORS Origin
- Audience
- JWKS (Public Key)
- JWKS URL: Ensure the public key is accessible via a URL, e.g.,
https://example.com/jwks. - Scope: List the scopes required by the application, such as:
name, phone, DoB, PoB, Offline Access, Offline, OpenID, Refresh, NIK.
The OpenID Server will validate and register your Application as a Client.
Authentification Check
This action is performed when clicking the login button with INA PAS.
Security Enhancement
-
PKCE → improve implementation with code_challenge and code_verifier
-
State → generate random state with secure random number generator
If your callback URL is written in NodeJS, it is recommended to use node-forge or built-in crypto if possible. Reference → Node.js URL callback.
Server-side example using Node.js
import forge from 'node-forge'
// Generate random state
const hashed = forge.md.sha256.create();
hashed.update(forge.util.bytesToHex(forge.random.getBytesSync(16)));
const state = hashed.digest().toHex();
// Generate PKCE code verifier and code challenge
const codeVerifier = forge.util.bytesToHex(forge.random.getBytesSync(32));
const codeChallengeHash = forge.md.sha256.create();
codeChallengeHash.update(codeVerifier);
const codeChallenge = forge.util.encode64(codeChallengeHash.digest().bytes()).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
// Construct the authorization request URL
const redirection = new URL(`/sso/oauth2/auth`, `https://dev-inapass-api.govtechindonesia.id`);
redirection.searchParams.set(`client_id`, `<PUT YOUR CLIENT ID HERE>`);
redirection.searchParams.set(`redirect_uri`, `<PUT YOUR CALLBACK URL HERE>`);
redirection.searchParams.set(`response_type`, `code`);
redirection.searchParams.set(`state`, state);
redirection.searchParams.set(`code_challenge_method`, `S256`);
redirection.searchParams.set(`code_challenge`, codeChallenge);
// Store state and code verifier in secure cookies
Astro.cookies.set('oidc-state', state, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
});
Astro.cookies.set('oidc-code-verifier', codeVerifier, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
});
// Redirect to the authorization endpoint
return Astro.redirect(redirection.toString());
Token Exchange
This action is performed after the approval page is approved by the INApas authentication page, and will be automatically redirected from the SSO engine.
Security Enhancement
-
Check that the state defined at login is valid
-
Check that code_challenge is not manipulated by using code_verifier
-
Use jwt_assertion when performing token exchange
-
Use a pre-generated private key to sign the data
-
Use the correct values to generate the signed JWT (JSON Web Token). This maintains data integrity and security.
-
Use the KID (Key ID) as part of the JWT header. This KID uses the
sha256hash format and serves to identify the key used to sign the token.
-
- KID is a unique identifier provided by INApas to make it easier for the system to recognize certain keys in the validation process. The KID helps direct the system to the correct key without the need to store all key information in the JWT...
{
'iss': '<YOUR CLIENT ID>',
'sub': '<YOUR CLIENT ID>',
'aud': '<PLACE THE TOKEN ISSUER URL>', // https://dev-inapass-api/sso/oauth2/token
'jti': '<PLACE UNIQUE RANDOM>',
'iat': '<DATE NOW>' // Math.floor(Date.now() / 1000)
}
Please note that the private key must be stored privately in the KMS or secret storage.
Server-side example using Astro.js
---
import crypto from 'crypto';
import * as Jose from 'jose';
const query = await Astro.url.searchParams
const code = query.get("code")
const state = query.get("state")
const scope = query.get("scope")
const clientId = "<YOUR CLIENT ID HERE>"
const redirectUri = "<YOUR REDIRECT URL HERE>"
const stateCookie = Astro.cookies.get('oidc-state')
const codeVerifierCookie = Astro.cookies.get('oidc-code-verifier')
if (state !== stateCookie.value) {
console.error('Error!', stateCookie.value, state)
// return error here
}
if (codeVerifierCookie.value == "") {
console.error('Error! security tampered!')
// return error here
}
Astro.cookies.delete('oidc-state')
Astro.cookies.delete('oidc-code-verifier')
const privateKey = `-----BEGIN PRIVATE KEY-----
<PUT YOUR PRIVATE KEY FOR SIGNING HERE>
-----END PRIVATE KEY-----`;
const privatePKCS = await Jose.importPKCS8(privateKey, 'ES512')
const jwt = await new Jose.SignJWT({
iss: clientId,
sub: clientId,
aud: `<PUT SAME URL WITH TOKEN REQUEST>`,
exp: Math.floor(Date.now() / 1000) + 60, // Expires in 1 minutes
jti: crypto.randomUUID(),
iat: Math.floor(Date.now() / 1000)
})
.setProtectedHeader({ alg: 'ES512', kid: '<PUT KID VALUE FROM JWK>' })
.sign(privatePKCS);
const tokenResponse = await fetch(`https://dev-inapass-api.govtechindonesia.id/sso/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: codeVerifierCookie.value,
scope: "openid offline_access profile",
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: jwt,
})
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json();
console.error('Error exchanging authorization code:', errorData);
return new Response('Error exchanging authorization code', { status: 500 });
}
const tokens = await tokenResponse.json();
Astro.cookies.set('login-session', JSON.stringify(tokens), {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
})
// decrypting tokens JWE
const decryptPrivateKey = `-----BEGIN PRIVATE KEY-----
<PUT YOUR PRIVATE KEY FOR DECRYPTION HERE>
-----END PRIVATE KEY-----`;
const parsedToken = Jose.decodeJwt(tokens.id_token)
const encryptedData = parsedToken["encrypted_data"]
const privateKey = await Jose.importPKCS8(decryptPrivateKey, 'ECDH-ES+A256KW');
const { plaintext } = await Jose.compactDecrypt(encryptedData.toString(), privateKey);
const plaintextStr = new TextDecoder().decode(plaintext);
const payloadObj = JSON.parse(plaintextStr);
// print decrypted data
console.log(payloadObj)
return Astro.redirect("http://localhost:3020")
Result Token
Example of exchanged result token
{
access_token: 'INPSS.AT~g7_9T689V00YUAkBwCzf2wvEZNykCzxlCApayyXzQzQ.J5vJMXyKesz5mXxo3h8yciTmdO1b8RTReeFSX5sCSWY',
expires_in: 3599,
id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMTg2YmRlLTBiNmYtNDRlMS1hZjgxLTE0YjFkZGVjYjg3NSIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJ1cm46bWFjZTppbmNvbW1vbjppYXA6c2lsdmVyIiwiYW1yIjpbInBpbiJdLCJhdF9oYXNoIjoiTlRqU0VWdlNLRVF4OHFON251Vko3ZyIsImF1ZCI6WyJpbnBzcy1ycC13aXRoLWp3a3MtaWQiXSwiYXV0aF90aW1lIjoxNzIxNjM2MzQ4LCJleHAiOjE3MjE2Mzk5NTAsImlhdCI6MTcyMTYzNjM1MCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0NDQ0IiwianRpIjoiNTY1ZmZmNGMtNjAzZC00YmViLWFmNDktOTg0MzhjZWM2MmY5IiwicHJvZmlsZSI6eyJlbWFpbCI6ImZiZkBnbWFpbC5jb20iLCJuYW1lIjoiRnVsYW4gQmluIEZ1bGFuIiwibmlrIjoiMTAwMDIwMDAzMDAwNDAwMCJ9LCJyYXQiOjE3MjE2MzYzNDIsInNpZCI6IjMwYzZiYjNjLWE5ZmMtNGVhYy04ZTE1LTY4MDMzYmNjZDQ1MyIsInN1YiI6ImluYXBhc3NfaWQtIn0.2-iEuP0Ht8TvrX0MW0DhB3FpNO9LBJM4_OMvw8AbMs8VP2ALAfryqu58z9-ms5jg8H4dqCp2-DR3fQ_LumlX0KjphHo3iUi1UiI4aH_lZTrXLWLOqL14AeE2oTuke-1DS3vbkUzhwdqEMTJa0mXGMLHsXWl1o_m4sJxz41KRo0-zZ0HWeXNsRQF97TSqG4CfAm7tZZUk7mNuc_KKz1pzgdkoqXMVPASEqKVaR4zE8qQtrQQzSDXCowdCMeh8YxceUzMF905TTRLns19H17ymJsf_WeiadugheJuTmvtXAF8WoU_FhLDRGmeu3Vx6DEuOIGGG6hjjDiwwLzUORtNPYCWgwN4Jw_lrvetPbeDMBHOuDuifZ47KVDLwDg5_4ibaoI35xqc-5mJqP9Lbt5rcT3ICI0VjHfGaVk-oA72bmq-gSu4rs5_Rs8nC31jUmBfJClFXFrc6r6CnwZN2CUiycCAhsq6pteuMCnlQgBfgK2NlwBDkC_rMJ4aCL-wJGo3fKcAIJJQkB2nXM4C-ow1jeWET1Ji6cK5dl_cYgFEIem-pWBBS7ft2_JxDFZhftXdjzpBj-noLnPEAiGQzDecJNfVME6eZZ-xjy_bAde1LnZ1dA93t7JkdDZ5I2QBtbqOJfdE9ESYKKLPdrE9lmeTQzsyE4PKKN8WHOU1ve8YPyJ4',
refresh_token: 'INPSS.RT~pppBiuwpo7k63TlSRztcoujTqhiZfEtSQNxrBbwtVIk.f8-ht7lTo6vWh7M8i2F5CwqQUtAbo-TQJGOVXfYW_M4',
scope: 'openid offline_access profile email nik',
token_type: 'bearer'
}
Core Element :
-
access_tokenis used to authenticate other API calls -
id_tokencontains information about user data and its scope -
refresh_tokenis used to re-generate access tokens that have expired
Token ID
Example of parsed id_token data
{
acr: 'urn:mace:incommon:iap:silver',
amr: [ 'pin' ],
at_hash: 'mnsqlMl62E6hesMelJOQ3A',
aud: [ 'portalku-mania' ],
auth_time: 1733299450,
exp: 1733303052,
iat: 1733299452,
iss: 'http://localhost:9000/sso',
jti: '06675ec1-53e1-4b18-8435-67c896d4e63e',
profile: {
aal: 2,
dob: '01/01/1990',
email: 'jggm@emailku.com',
name: 'JAN GABE GHIYAST MUBARIZ',
nik: '1000200030004000',
phone: '62810002000'
},
rat: 1733299435,
sid: '4a6ec125-e537-4118-bbc7-27b0245a3ce4',
sub: 'JGJE6EE0GX'
}
Refresh Token
If your access_token has expired, you can get a new access_token by redeeming the given refresh_token.
How to get a new token using refresh_token →
... todo ...
If the new token request is invalid, your app access will be revoked by the user, and the user will need to log out and log back in.