Skip to main content

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:

  • ES512 for signing JSON Web Tokens (JWT) to verify the identity of the client and secure data exchanges.
  • JWE (JSON Web Encryption) using ECDH-ES+A256KW for 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

  1. PKCE → improve implementation with code_challenge and code_verifier

  2. State → generate random state with secure random number generator

caution

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

  1. Check that the state defined at login is valid

  2. Check that code_challenge is not manipulated by using code_verifier

  3. 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 sha256 hash format and serves to identify the key used to sign the token.

  1. 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)
}
caution

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_token is used to authenticate other API calls

  • id_token contains information about user data and its scope

  • refresh_token is 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.