Lewati ke konten utama

Memulai: Panduan Integrasi

Persyaratan Awal

Hal-hal yang Disiapkan oleh Pihak yang Mengandalkan atau Klien (RP)

Untuk mengintegrasikan SSO dengan INApas, klien harus menyiapkan hal-hal berikut ini:

1. Nama

Berikan nama entitas atau pemilik bisnis yang valid dan terdaftar.


2. Nama Aplikasi

Tentukan nama aplikasi yang akan digunakan sebagai client_id selama proses autentikasi.


3. Informasi Bisnis

Siapkan detail berikut untuk aplikasi:

  • Ikon atau Logo Aplikasi
    Logo atau ikon aplikasi dengan ukuran minimum 256x256 piksel untuk identifikasi visual.

  • Deskripsi Aplikasi Ringkasan singkat tentang fungsi dan tujuan aplikasi.

  • Tautan Situs Web Resmi atau App Store
    Berikan tautan ke situs web resmi atau daftar aplikasi di Play Store/App Store.

  • Tautan Persyaratan Layanan (ToS)
    Sertakan tautan ke ketentuan layanan yang mengatur penggunaan aplikasi.

  • Tautan Kebijakan Privasi
    Cantumkan tautan ke kebijakan privasi yang menguraikan bagaimana data pengguna diproses dan dilindungi.


4. Informasi Klien Dasar

Berikan detail teknis berikut:

a. Redirect URL

  • Tentukan URL ke mana pengguna akan dialihkan setelah autentikasi.
    Contoh: https://example.com/connect.

  • Pastikan titik akhir ini aktif dan mampu memproses kode otorisasi atau token.

b. CORS Origin yang Diizinkan

  • Tentukan domain atau asal klien untuk menghindari kesalahan lintas asal.
    Contoh: example.com/*.

c. Audiens

  • Tentukan identitas aplikasi atau layanan yang diakses melalui OpenID.
    Contoh: INAKU.

d. Callback URL

  • Tentukan titik akhir untuk menangani data panggilan balik dari penyedia OIDC.
    Contoh: http://localhost:3000/callback.

5. Pembuatan Pasangan Kunci

Untuk mengamankan komunikasi antara aplikasi klien dan penyedia autentikasi, buatlah pasangan kunci menggunakan algoritme Eliptic Curves P-512. Metode ini memastikan implementasi proses kriptografi yang cepat dan aman.

  • Kunci Publik: Digunakan oleh OpenID sebagai JWKS (JSON Web Key Set) untuk memverifikasi tanda tangan JWT (JSON Web Token) dan memungkinkan komunikasi yang aman.
  • Kunci Pribadi: Disimpan dengan aman di server Anda dan digunakan untuk menandatangani permintaan. Ini memastikan keaslian dan integritas data yang dipertukarkan antara klien dan server.

Kami menggunakan:

  • ES512 untuk menandatangani JSON Web Tokens (JWT) untuk memverifikasi identitas klien dan mengamankan pertukaran data.
  • JWE (JSON Web Encryption) menggunakan ECDH-ES + A256KW untuk mengenkripsi data sensitif, memastikan kerahasiaan dan transmisi informasi pengguna yang aman

Untuk contoh pembuatan kunci, lihat:
Contoh Pembuatan Kunci: GitHub - inadigital-inapas/kuncy.


6. Mengonfigurasi OpenID pada Server

Untuk menyelesaikan integrasi, kirimkan informasi berikut ini ke OpenID (INApas):

  • Redirect URL
  • Asal CORS yang Diizinkan
  • Audiens
  • JWKS (Kunci Publik)
  • URL JWKS: Pastikan kunci publik dapat diakses melalui URL, misalnya, https://example.com/jwks.
  • Cakupan**: Cantumkan cakupan yang diperlukan oleh aplikasi, seperti:
    nama, telepon, DoB, PoB, Akses Offline, Offline, OpenID, Refresh, NIK.

Server OpenID akan memvalidasi dan mendaftarkan Aplikasi Anda sebagai Klien.


Pemeriksaan Autentifikasi

Tindakan ini dilakukan ketika mengklik tombol login dengan INA PAS.

Peningkatan Keamanan

  1. PKCE → meningkatkan implementasi dengan code_challenge dan code_verifier

  2. State → menghasilkan state acak dengan generator nomor acak yang aman

waspada

Jika URL callback Anda ditulis di NodeJS, disarankan untuk menggunakan node-forge atau crypto bawaan jika memungkinkan. Referensi → Pemanggilan balik URL Node.js.

Contoh sisi server menggunakan 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());

Pertukaran Token

Tindakan ini dilakukan setelah halaman persetujuan disetujui oleh halaman autentikasi INApas dan akan secara otomatis dialihkan dari mesin SSO.

Peningkatan Keamanan

  1. Periksa apakah status yang ditetapkan pada saat login sudah valid

  2. Pastikan code_challenge tidak dimanipulasi dengan menggunakan code_verifier

  3. Gunakan jwt_assertion saat melakukan pertukaran token

    • Gunakan kunci pribadi yang dibuat sebelumnya untuk menandatangi data

    • Gunakan nilai-nilai yang benar untuk menghasilkan JWT (JSON Web Token) yang ditandatangani. Hal ini menjaga integritas dan keamanan data.

    • Gunakan KID (Key ID) sebagai bagian dari header JWT. KID ini menggunakan format hash sha256 dan berfungsi untuk mengidentifikasi kunci yang digunakan untuk menandatangani token.

  4. KID adalah identifier unik yang disediakan oleh INApas untuk memudahkan sistem mengenali kunci tertentu dalam proses validasi. KID membantu mengarahkan sistem ke kunci yang benar tanpa perlu menyimpan semua informasi kunci dalam 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)
}
waspada

Harap diperhatikan, private key harus disimpan secara pribadi di dalam KMS atau penyimpanan rahasia.

Contoh sisi server menggunakan 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")

Token Hasil

Contoh token hasil yang dipertukarkan

{
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'
}

Elemen Inti :

  • access_token digunakan untuk mengautentikasi panggilan API lainnya

  • id_token berisi informasi tentang data pengguna dan cakupannya

  • refresh_token digunakan untuk membuat ulang token akses yang telah kedaluwarsa

Token ID

Contoh data id_token yang diuraikan

{
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

Jika access_token anda sudah kedaluwarsa, Anda bisa mendapatkan access_token baru dengan menukarkan refresh_token yang sudah diberikan.

Cara mendapatkan token baru menggunakan refresh_token

... todo ...

Jika permintaan token baru tidak valid, akses aplikasi Anda akan dicabut oleh pengguna, dan pengguna harus logout lalu login kembali.