Ringkasan
SSO UNRI memakai SAML 2.0. Aplikasi Anda tidak pernah menerima atau menyimpan password. Pengguna mengetik password di halaman SSO, lalu SSO mengirim bukti login bertanda tangan ke aplikasi. Aplikasi apa pun bisa terhubung selama bahasanya punya library SAML 2.0 Service Provider. Yang tidak punya bisa memakai mod_auth_mellon di depan aplikasinya.
- Pengguna membuka aplikasi dan menekan tombol masuk.
- Aplikasi mengalihkan browser ke SSO dengan AuthnRequest yang ditandatangani kunci aplikasi.
- Pengguna login di SSO. Bila sudah login sebelumnya, langkah ini dilewati.
- SSO mengirim SAML Response bertanda tangan lewat POST ke ACS aplikasi.
- Aplikasi memeriksa tanda tangan, lalu membuat sesinya sendiri.
Alamat SSO Asli
Alamat di bawah adalah alamat SSO ini, bukan contoh. Salin apa adanya.
| Entity ID | https://sso.dev-app.unri.ac.id/saml/metadata |
|---|---|
| Metadata | https://sso.dev-app.unri.ac.id/saml/metadata |
| Single Sign-On | https://sso.dev-app.unri.ac.id/saml/sso, HTTP-Redirect |
| Single Logout | https://sso.dev-app.unri.ac.id/saml/slo, HTTP-Redirect |
| Respons ke aplikasi | HTTP-POST ke ACS yang terdaftar |
| Tanda tangan | RSA-SHA256, wajib di AuthnRequest dan pesan logout dari aplikasi |
| NameID | urn:oasis:names:tc:SAML:2.0:nameid-format:persistent |
| Masa berlaku | Assertion 5 menit, toleransi jam 120 detik |
Sertifikat SSO
Sertifikat yang sama ada di metadata. Simpan sebagai idp.crt di server aplikasi.
-----BEGIN CERTIFICATE-----
MIIEdzCCAt+gAwIBAgIIMznLIWgqGJQwDQYJKoZIhvcNAQELBQAwUTESMBAGA1UE
AwwJMTI3LjAuMC4xMRkwFwYDVQQKDBBVbml2ZXJzaXRhcyBSaWF1MQswCQYDVQQG
EwJJRDETMBEGA1UECAwKU29tZS1TdGF0ZTAeFw0yNjExMDIwOTQxNTdaFw0zNjEw
MzAwOTQxNTdaMFExEjAQBgNVBAMMCTEyNy4wLjAuMTEZMBcGA1UECgwQVW5pdmVy
c2l0YXMgUmlhdTELMAkGA1UEBhMCSUQxEzARBgNVBAgMClNvbWUtU3RhdGUwggGi
MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCS0dP6yhMOGqphaMT0oJqYOod4
7WV1qtjsZOWeCPSIy+QKtJs1I3jV5yMtH6WMneFAwI3kqOkKmZB+ISkfUUGefMh5
Lytbw5TuRdyGLb/UTuMpe7Kz8B6/9/xQzjrylDgO/X2wMtxsURRCP9eftpVbkMMd
IOrEZN2tq1n+/GsGzAIt89MAXGnZKfOMkNnaDyjhnwjeNyjOzLb3p3teYqZxoYci
Ep7P6W9tLzwIUtlcK/Y1aQTNHPBIG8pV9/94Zzwa8DMc9ZiumwjgdsOpFNeV8py7
7xuyjJyS63JSJtwxqRhte1TFAjeUlIUoDYeTe8FAS0m1SC7lUHVJ4dAUIxmBJt1D
KRe2S3eohGhCAucUj4bDHKyPViXrHWF3SEP/VhDvYWf34bMLEGAC/sdX8Mer50Yt
5IeZrVRJW+50alHVEQd2c9ckuZKFY5M44H6wMrpyR99trXUfPH5cTx+KZmOyBPws
1AXsXqMK/OwaYCK6Vw5r0uj1De+CV6J+0McoxsMCAwEAAaNTMFEwHQYDVR0OBBYE
FHkeAbYQaVgD7A3AhmE5L+7iUtRQMB8GA1UdIwQYMBaAFHkeAbYQaVgD7A3AhmE5
L+7iUtRQMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggGBAGc/B1af
WBCP/XSAXn5wpWgeBJVguvDWXCcXqOUZqGNStzVP9BVpzReJflYRhpAgFn8rVX5H
1xECN6CqCgOKV9ZwcDbbfJsF1+bYQ90u/M3HgQjSN2PxbQAd0vye0TH00TsTV80S
yKuX1QKvUViWUNezraVRkxZZKdMKA0tVRjd4HDTm3VjKfMOv6zUJm2m2eBnXEmsS
CZREFcCvbGzqo4cG30ekEjlt3/aANCSRl8L64GzHKV2c8UEo0AihwgnWGiMwqkxO
5or3z8b5y04HF5l/xxTxcQlNlRuj4qM3S+a4do7XANN0K8oTAoUd3+6N/kjQ4LWu
29qRFnVSoyBE+NiRi1T8NdL/eXgR/GoQnzjC1qHaL/V8KAKUB+fTnJTiCJs5UC+x
T54lKn/e8wY0WI/lX93jCVFe1yaIIx2bzAbkoofcf55OUezxv2EtfOddLf2nLDrv
iqeSOD1uKi7lQ4bksT7DCh9eGgBME4Zq+SiJU2pmxyOqUsFxlcRrZDHgbA==
-----END CERTIFICATE-----
Data pengguna yang diterima
| Nama | Isi |
|---|---|
NameID | pegawai:<id> atau mahasiswa:<NIM>. Identitas tetap; pakai ini sebagai kunci pengguna. |
account_type | pegawai atau mahasiswa |
name | Nama lengkap |
registration_number | NIP atau NIM. Bisa tidak ada untuk pegawai tanpa NIP. |
email | Email pegawai atau email kampus mahasiswa. Bisa tidak ada. |
AuthnContextClassRef berisi urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken
bila pengguna memakai kode authenticator, dan PasswordProtectedTransport bila hanya password.
Aplikasi yang punya 2FA sendiri bisa melewatinya bila nilainya TimeSyncToken.
Aplikasi tidak menerima password. Bila perlu tabel pengguna lokal, simpan NameID dan atribut di atas tanpa kolom password.
Langkah integrasi
-
Buat kunci aplikasi
openssl req -x509 -newkey rsa:3072 -nodes -days 3650 -subj "/CN=nama-aplikasi" \ -keyout sp.key -out sp.crt chmod 600 sp.keynama-aplikasiadalah contoh; isi dengan nama aplikasi Anda, misalnyaportal-wifi.sp.keytetap di server aplikasi dan jangan masuk ke git.sp.crtdiserahkan ke pengelola SSO. -
Ambil sertifikat SSO
Salin sertifikat di bagian Alamat SSO ke
idp.crt, atau arahkan library ke URL metadata. -
Pasang library dan route
Pilih bahasa di Contoh kode. Ganti
https://app.unri.ac.iddengan alamat aplikasi Anda. -
Minta pendaftaran ke pengelola SSO
SSO menolak aplikasi yang belum terdaftar. Kirim data berikut:
Data Keterangan Contoh Nama dan deskripsi Tampil di portal SSO Portal Wifi, Akses internet kampus Entity ID Biasanya URL metadata aplikasi https://app.unri.ac.id/saml/metadataACS URL Alamat yang menerima POST dari SSO https://app.unri.ac.id/saml/acsSLO URL Alamat logout. Boleh kosong bila tidak mendukung Single Logout https://app.unri.ac.id/saml/sloatau kosongLogin URL Halaman yang memulai login, dipakai kartu di portal SSO. Boleh kosong bila aplikasi tidak perlu tampil di portal https://app.unri.ac.id/saml/loginatau kosongJenis akun Pegawai, mahasiswa, atau keduanya Pegawai dan mahasiswa Sertifikat Isi sp.crtBerkas yang diawali -----BEGIN CERTIFICATE-----Kolom Contoh hanya ilustrasi. Alamat yang benar mengikuti alamat aplikasi Anda dan route bawaan library; lihat tabel di bawah tiap contoh kode.
-
Uji
Login, logout dari aplikasi, lalu buka SSO: SSO harus meminta login lagi. Coba juga akun dengan jenis yang tidak diizinkan; SSO harus menolaknya.
Prompt untuk asisten AI
Salin teks di bawah ke asisten AI pilihan Anda, misalnya ChatGPT, Claude, Gemini, atau Copilot.
Isi dulu bagian [dalam kurung siku] dengan data aplikasi Anda.
Alamat SSO di dalamnya sudah alamat asli server ini.
Bantu saya mengintegrasikan aplikasi saya ke SSO UNRI, sebuah SAML 2.0 Identity Provider. Ikuti spesifikasi di bawah dengan tepat. Jangan menebak nilai yang tidak tercantum; tanyakan dulu ke saya.
== TENTANG APLIKASI SAYA (isi dulu bagian dalam kurung siku) ==
- Bahasa dan framework: [CONTOH: Laravel 11 / Django 5 / Express / Spring Boot 3 / ASP.NET 8 / Go / Rails]
- Alamat publik aplikasi, HTTPS: [CONTOH: https://app.unri.ac.id]
- Jenis akun yang boleh masuk: [pegawai / mahasiswa / keduanya]
- Cara aplikasi menyimpan pengguna saat ini: [CONTOH: tabel users dengan kolom email]
- Berjalan di balik reverse proxy: [ya / tidak]
== IDENTITY PROVIDER (nilai asli, jangan diubah) ==
- Entity ID: https://sso.dev-app.unri.ac.id/saml/metadata
- Metadata: https://sso.dev-app.unri.ac.id/saml/metadata
- Single Sign-On: https://sso.dev-app.unri.ac.id/saml/sso (hanya HTTP-Redirect)
- Single Logout: https://sso.dev-app.unri.ac.id/saml/slo (hanya HTTP-Redirect)
- Sertifikat penandatangan IdP ada di metadata (KeyDescriptor use="signing"). Simpan sebagai idp.crt.
- Hanya alur SP-initiated. IdP-initiated tidak didukung.
- Panduan lengkap dan contoh kode: https://sso.dev-app.unri.ac.id/panduan
== ATURAN PROTOKOL ==
1. AuthnRequest dikirim lewat HTTP-Redirect dan WAJIB ditandatangani RSA-SHA256 dengan kunci privat SP (parameter SigAlg dan Signature di query string). AuthnRequest lewat HTTP-POST ditolak.
2. Issuer AuthnRequest harus sama persis dengan Entity ID SP yang didaftarkan. Bila AssertionConsumerServiceURL diisi, nilainya harus sama persis dengan ACS URL terdaftar. Bila ProtocolBinding diisi, nilainya harus HTTP-POST.
3. SAML Response datang lewat HTTP-POST ke ACS. Response dan Assertion ditandatangani RSA-SHA256. Validasi tanda tangan, Issuer, Audience (= Entity ID SP), Destination dan Recipient (= ACS URL), InResponseTo (= ID AuthnRequest yang disimpan), serta NotBefore dan NotOnOrAfter. Assertion berlaku 300 detik dengan toleransi jam 120 detik.
4. Pakai mode strict. Jangan mematikan validasi apa pun, termasuk saat uji coba.
5. RelayState maksimal 1024 byte.
6. Jangan mengirim RequestedAuthnContext.
== DATA PENGGUNA ==
- NameID, format persistent: "pegawai:<id>" atau "mahasiswa:<NIM>". Jadikan ini kunci pengguna. JANGAN memakai email sebagai identitas, karena mahasiswa bisa mengubah email sendiri dan sebagian email dipakai bersama.
- Atribut, nama tanpa awalan URI, masing-masing satu nilai:
account_type = pegawai | mahasiswa
name = nama lengkap
email = bisa tidak ada
registration_number = NIP atau NIM, bisa tidak ada
- AuthnContextClassRef bernilai urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken bila pengguna memakai kode authenticator, selain itu PasswordProtectedTransport. Baca dari elemen XML, bukan dari isi atribut.
- SSO tidak pernah mengirim password. Jangan membuat kolom password untuk pengguna SSO.
- Bila aplikasi butuh tabel pengguna lokal: cari atau buat berdasarkan NameID, lalu perbarui name dan email dari atribut setiap kali login.
== LOGOUT ==
- Bawaan: logout di aplikasi hanya menghapus sesi lokal. Jangan mengirim LogoutRequest ke SSO dan jangan membuat endpoint SLO, kecuali saya meminta Single Logout.
- Bila saya meminta Single Logout: endpoint SLO menerima LogoutRequest bertanda tangan dari SSO lewat HTTP-Redirect, mengakhiri sesi lokal, lalu membalas LogoutResponse bertanda tangan RSA-SHA256 lewat HTTP-Redirect.
== KEAMANAN APLIKASI ==
- Kecualikan ACS dari proteksi CSRF.
- Buat ulang session ID setelah login berhasil.
- Pakai HTTPS. Simpan sp.key di luar git dengan izin baca hanya untuk aplikasi.
- Bila aplikasi dan SSO beda situs (tidak sama-sama di *.unri.ac.id), cookie sesi SameSite=Lax tidak ikut pada POST ke ACS. Simpan ID AuthnRequest di cookie terpisah SameSite=None; Secure, atau di cache server yang dikunci oleh cookie itu, supaya InResponseTo tetap bisa diperiksa.
- Di balik reverse proxy, bentuk URL dari alamat publik yang dikonfigurasi, bukan dari header request.
- Catat alasan kegagalan validasi di log server. Ke pengguna cukup tampilkan pesan umum.
== YANG SAYA MINTA ==
1. Pilih library SAML 2.0 Service Provider yang masih terawat untuk stack saya, sebutkan versi minimalnya, dan jelaskan alasannya.
2. Perintah membuat kunci SP:
openssl req -x509 -newkey rsa:3072 -nodes -days 3650 -subj "/CN=<nama-aplikasi>" -keyout sp.key -out sp.crt
3. Kode lengkap: konfigurasi, route mulai login, ACS, metadata SP, logout lokal, dan pemetaan pengguna dari NameID.
4. Daftar nilai yang harus saya kirim ke pengelola SSO: Entity ID, ACS URL, SLO URL (kosong bila tanpa Single Logout), Login URL (boleh kosong), jenis akun, dan isi sp.crt.
5. Langkah uji: login berhasil; login ulang tanpa password selama sesi SSO masih aktif; akun dengan jenis yang tidak diizinkan ditolak SSO; respons yang dikirim ulang ditolak aplikasi.
Tandai dengan jelas setiap nilai contoh yang harus saya ganti.
Periksa hasil dari AI dengan Aturan wajib dan Contoh kode sebelum dipakai. AI bisa keliru menyebut nama fungsi atau opsi library.
Contoh kode
Setiap contoh lengkap: memulai login, menerima respons, logout, menerima logout dari SSO, dan metadata.
Bedakan alamat contoh dan alamat asli.
- Contoh
https://app.unri.ac.id,127.0.0.1:8000, dan lokasi file sepertisaml/sp.keyatau/etc/app/samlhanya contoh. Ganti dengan alamat dan lokasi milik aplikasi Anda. - Asli
https://sso.dev-app.unri.ac.idadalah alamat SSO ini. Pakai apa adanya.
Di dalam kode, alamat contoh ditandai komentar CONTOH.
PHP
onelogin/php-saml 4.3.2 ke atas
<?php
// composer require onelogin/php-saml:^4.3.2
use OneLogin\Saml2\Auth;
require __DIR__.'/vendor/autoload.php';
const APP = 'https://app.unri.ac.id'; // CONTOH: ganti dengan alamat aplikasi Anda
const SSO = 'https://sso.dev-app.unri.ac.id'; // alamat SSO ini, jangan diubah
const PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent';
session_start();
$auth = new Auth([
'strict' => true,
'sp' => [
'entityId' => APP.'/saml/metadata',
'assertionConsumerService' => ['url' => APP.'/saml/acs'],
'singleLogoutService' => ['url' => APP.'/saml/slo'],
'NameIDFormat' => PERSISTENT,
'x509cert' => file_get_contents(__DIR__.'/saml/sp.crt'),
'privateKey' => file_get_contents(__DIR__.'/saml/sp.key'),
],
'idp' => [
'entityId' => SSO.'/saml/metadata',
'singleSignOnService' => ['url' => SSO.'/saml/sso'],
'singleLogoutService' => ['url' => SSO.'/saml/slo'],
'x509cert' => file_get_contents(__DIR__.'/saml/idp.crt'),
],
'security' => [
'authnRequestsSigned' => true,
'logoutRequestSigned' => true,
'logoutResponseSigned' => true,
'wantAssertionsSigned' => true,
'wantMessagesSigned' => true,
'requestedAuthnContext' => false,
'rejectUnsolicitedResponsesWithInResponseTo' => true,
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
],
]);
switch (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) {
case '/saml/login':
$url = $auth->login(APP.'/', [], false, false, true);
$_SESSION['saml_request_id'] = $auth->getLastRequestID();
header('Location: '.$url);
exit;
case '/saml/acs':
$auth->processResponse($_SESSION['saml_request_id'] ?? null);
unset($_SESSION['saml_request_id']);
if ($auth->getErrors() || ! $auth->isAuthenticated()) {
error_log('Login SSO gagal: '.$auth->getLastErrorReason());
http_response_code(403);
exit('Login SSO gagal.');
}
session_regenerate_id(true);
$_SESSION['user'] = [
'id' => $auth->getNameId(), // pegawai:123 atau mahasiswa:2107110001
'type' => $auth->getAttribute('account_type')[0], // pegawai atau mahasiswa
'name' => $auth->getAttribute('name')[0] ?? null,
'email' => $auth->getAttribute('email')[0] ?? null,
];
$_SESSION['saml'] = ['name_id' => $auth->getNameId(), 'session_index' => $auth->getSessionIndex()];
header('Location: /');
exit;
case '/saml/logout':
$saml = $_SESSION['saml'] ?? [];
$url = $auth->logout(APP.'/', [], $saml['name_id'] ?? null, $saml['session_index'] ?? null, true, PERSISTENT);
$_SESSION['saml_logout_id'] = $auth->getLastRequestID();
header('Location: '.$url);
exit;
case '/saml/slo':
// Menangani dua hal: SSO meminta logout, atau SSO menjawab logout dari aplikasi ini.
$url = $auth->processSLO(false, $_SESSION['saml_logout_id'] ?? null, false, fn () => session_destroy(), true);
header('Location: '.($url ?: '/'));
exit;
case '/saml/metadata':
header('Content-Type: application/xml');
echo $auth->getSettings()->getSPMetadata();
exit;
}
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/saml/acs |
| SLO URL | https://app.unri.ac.id/saml/slo |
| Login URL | https://app.unri.ac.id/saml/login |
- Versi 4.3.1 ke bawah bermasalah bila terpasang bersama xmlseclibs 4.
- Di Laravel, pindahkan tiap case ke route dan kecualikan saml/acs dari CSRF.
Python
python3-saml (contoh dengan Flask)
# pip install python3-saml flask
import os
from urllib.parse import urlparse
from flask import Flask, abort, make_response, redirect, request, session
from onelogin.saml2.auth import OneLogin_Saml2_Auth
APP = "https://app.unri.ac.id" # CONTOH: ganti dengan alamat aplikasi Anda
SSO = "https://sso.dev-app.unri.ac.id" # alamat SSO ini, jangan diubah
PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
def read(path):
with open(path) as f:
return f.read()
SETTINGS = {
"strict": True,
"sp": {
"entityId": f"{APP}/saml/metadata",
"assertionConsumerService": {"url": f"{APP}/saml/acs"},
"singleLogoutService": {"url": f"{APP}/saml/slo"},
"NameIDFormat": PERSISTENT,
"x509cert": read("saml/sp.crt"),
"privateKey": read("saml/sp.key"),
},
"idp": {
"entityId": f"{SSO}/saml/metadata",
"singleSignOnService": {"url": f"{SSO}/saml/sso"},
"singleLogoutService": {"url": f"{SSO}/saml/slo"},
"x509cert": read("saml/idp.crt"),
},
"security": {
"authnRequestsSigned": True,
"logoutRequestSigned": True,
"logoutResponseSigned": True,
"wantAssertionsSigned": True,
"wantMessagesSigned": True,
"requestedAuthnContext": False,
"rejectUnsolicitedResponsesWithInResponseTo": True,
"signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
},
}
app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
def saml_auth():
# Host diambil dari APP, bukan dari request, supaya tetap benar di balik proxy.
return OneLogin_Saml2_Auth({
"https": "on",
"http_host": urlparse(APP).netloc,
"script_name": request.path,
"get_data": request.args.copy(),
"post_data": request.form.copy(),
}, SETTINGS)
@app.get("/saml/login")
def login():
auth = saml_auth()
url = auth.login(return_to=f"{APP}/")
session["saml_request_id"] = auth.get_last_request_id()
return redirect(url)
@app.post("/saml/acs")
def acs():
auth = saml_auth()
auth.process_response(request_id=session.pop("saml_request_id", None))
if auth.get_errors() or not auth.is_authenticated():
app.logger.warning("Login SSO gagal: %s", auth.get_last_error_reason())
abort(403)
attrs = auth.get_attributes()
session.clear()
session["user"] = {
"id": auth.get_nameid(), # pegawai:123 atau mahasiswa:2107110001
"type": attrs["account_type"][0], # pegawai atau mahasiswa
"name": attrs.get("name", [None])[0],
"email": attrs.get("email", [None])[0],
}
session["saml"] = {"name_id": auth.get_nameid(), "session_index": auth.get_session_index()}
return redirect("/")
@app.get("/saml/logout")
def logout():
auth = saml_auth()
saml = session.get("saml", {})
url = auth.logout(
return_to=f"{APP}/",
name_id=saml.get("name_id"),
session_index=saml.get("session_index"),
name_id_format=PERSISTENT,
)
session["saml_logout_id"] = auth.get_last_request_id()
return redirect(url)
# Menangani dua hal: SSO meminta logout, atau SSO menjawab logout dari aplikasi ini.
@app.get("/saml/slo")
def slo():
auth = saml_auth()
url = auth.process_slo(request_id=session.get("saml_logout_id"), delete_session_cb=session.clear)
if auth.get_errors():
abort(400)
return redirect(url or "/")
@app.get("/saml/metadata")
def metadata():
response = make_response(saml_auth().get_settings().get_sp_metadata())
response.headers["Content-Type"] = "application/xml"
return response
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/saml/acs |
| SLO URL | https://app.unri.ac.id/saml/slo |
| Login URL | https://app.unri.ac.id/saml/login |
- Butuh pustaka sistem xmlsec: apt install libxmlsec1-dev pkg-config.
- Untuk Django, pengaturannya sama; bedanya hanya cara membaca request dan sesi.
Node.js
@node-saml/node-saml 5 (contoh dengan Express)
// npm i express express-session @node-saml/node-saml
import fs from "node:fs";
import express from "express";
import session from "express-session";
import { SAML, ValidateInResponseTo } from "@node-saml/node-saml";
const APP = "https://app.unri.ac.id"; // CONTOH: ganti dengan alamat aplikasi Anda
const SSO = "https://sso.dev-app.unri.ac.id"; // alamat SSO ini, jangan diubah
const spCert = fs.readFileSync("saml/sp.crt", "utf8");
const saml = new SAML({
issuer: `${APP}/saml/metadata`,
audience: `${APP}/saml/metadata`,
callbackUrl: `${APP}/saml/acs`,
logoutCallbackUrl: `${APP}/saml/slo`,
entryPoint: `${SSO}/saml/sso`,
logoutUrl: `${SSO}/saml/slo`,
idpIssuer: `${SSO}/saml/metadata`,
idpCert: fs.readFileSync("saml/idp.crt", "utf8"),
privateKey: fs.readFileSync("saml/sp.key", "utf8"),
publicCert: spCert,
identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
wantAssertionsSigned: true,
wantAuthnResponseSigned: true,
validateInResponseTo: ValidateInResponseTo.always,
disableRequestedAuthnContext: true,
});
const app = express();
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true },
}));
app.get("/saml/login", async (req, res, next) => {
try {
res.redirect(await saml.getAuthorizeUrlAsync("", undefined, {}));
} catch (err) {
next(err);
}
});
app.post("/saml/acs", async (req, res) => {
try {
const { profile } = await saml.validatePostResponseAsync(req.body);
req.session.regenerate(() => {
req.session.user = {
id: profile.nameID, // pegawai:123 atau mahasiswa:2107110001
type: profile.attributes.account_type, // pegawai atau mahasiswa
name: profile.attributes.name,
email: profile.attributes.email,
};
req.session.saml = {
issuer: profile.issuer,
nameID: profile.nameID,
nameIDFormat: profile.nameIDFormat,
sessionIndex: profile.sessionIndex,
};
res.redirect("/");
});
} catch (err) {
console.warn("Login SSO gagal:", err.message);
res.status(403).send("Login SSO gagal.");
}
});
app.get("/saml/logout", async (req, res, next) => {
try {
if (!req.session.saml) return req.session.destroy(() => res.redirect("/"));
res.redirect(await saml.getLogoutUrlAsync(req.session.saml, "", {}));
} catch (err) {
next(err);
}
});
// Menangani dua hal: SSO meminta logout, atau SSO menjawab logout dari aplikasi ini.
app.get("/saml/slo", async (req, res) => {
try {
const originalQuery = req.originalUrl.split("?")[1] ?? "";
const { profile } = await saml.validateRedirectAsync(req.query, originalQuery);
if (req.query.SAMLRequest) {
const url = await saml.getLogoutResponseUrlAsync(profile, req.query.RelayState ?? "", {}, true);
return req.session.destroy(() => res.redirect(url));
}
req.session.destroy(() => res.redirect("/"));
} catch (err) {
console.warn("Logout SSO gagal:", err.message);
res.status(400).send("Logout SSO gagal.");
}
});
app.get("/saml/metadata", (req, res) => {
res.type("application/xml").send(saml.generateServiceProviderMetadata(null, spCert));
});
app.listen(3000);
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/saml/acs |
| SLO URL | https://app.unri.ac.id/saml/slo |
| Login URL | https://app.unri.ac.id/saml/login |
- ID permintaan login disimpan di memori proses. Bila aplikasi berjalan lebih dari satu instance, isi opsi cacheProvider dengan Redis.
- Untuk Passport, @node-saml/passport-saml menerima opsi yang sama.
Java
Spring Security SAML2 Service Provider 6.1 ke atas
// Spring Boot 3 + Spring Security 6.1 ke atas.
// Dependensi: spring-boot-starter-security dan spring-security-saml2-service-provider.
// OpenSAML diambil dari repositori Shibboleth: https://build.shibboleth.net/maven/releases/
# ---- src/main/resources/application.yml ----
spring:
security:
saml2:
relyingparty:
registration:
unri:
# CONTOH: ganti app.unri.ac.id dengan alamat aplikasi Anda
entity-id: "https://app.unri.ac.id/saml2/metadata/unri"
signing:
credentials:
# CONTOH: lokasi kunci di server aplikasi Anda
- private-key-location: "file:/etc/app/saml/sp.key"
certificate-location: "file:/etc/app/saml/sp.crt"
singlelogout:
url: "{baseUrl}/logout/saml2/slo"
binding: REDIRECT
assertingparty:
metadata-uri: "https://sso.dev-app.unri.ac.id/saml/metadata" # alamat SSO ini, jangan diubah
// ---- SecurityConfig.java ----
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.saml2Login(Customizer.withDefaults())
.saml2Logout(Customizer.withDefaults())
.saml2Metadata(Customizer.withDefaults());
return http.build();
}
}
// ---- HomeController.java ----
@RestController
public class HomeController {
@GetMapping("/")
public Map<String, Object> home(@AuthenticationPrincipal Saml2AuthenticatedPrincipal user) {
return Map.of(
"id", user.getName(), // pegawai:123 atau mahasiswa:2107110001
"type", user.getFirstAttribute("account_type"), // pegawai atau mahasiswa
"name", Objects.toString(user.getFirstAttribute("name"), ""),
"email", Objects.toString(user.getFirstAttribute("email"), "")
);
}
}
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml2/metadata/unri |
|---|---|
| ACS URL | https://app.unri.ac.id/login/saml2/sso/unri |
| SLO URL | https://app.unri.ac.id/logout/saml2/slo |
| Login URL | https://app.unri.ac.id/saml2/authenticate/unri |
- Kunci SP harus PKCS#8 (BEGIN PRIVATE KEY). Perintah openssl di langkah 1 sudah menghasilkan format ini.
- Logout dengan POST /logout; Spring meneruskannya ke SSO.
.NET
Sustainsys.Saml2.AspNetCore2
// dotnet add package Sustainsys.Saml2.AspNetCore2
// Kunci SP diubah ke PFX dulu:
// openssl pkcs12 -export -in sp.crt -inkey sp.key -out sp.pfx
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Sustainsys.Saml2;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Metadata;
const string App = "https://app.unri.ac.id"; // CONTOH: ganti dengan alamat aplikasi Anda
const string Sso = "https://sso.dev-app.unri.ac.id"; // alamat SSO ini, jangan diubah
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = Saml2Defaults.Scheme;
})
.AddCookie()
.AddSaml2(options =>
{
options.SPOptions.EntityId = new EntityId($"{App}/Saml2");
options.SPOptions.PublicOrigin = new Uri($"{App}/");
options.SPOptions.OutboundSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
options.SPOptions.ServiceCertificates.Add(
new X509Certificate2("saml/sp.pfx", builder.Configuration["Saml:PfxPassword"]));
options.IdentityProviders.Add(new IdentityProvider(new EntityId($"{Sso}/saml/metadata"), options.SPOptions)
{
MetadataLocation = $"{Sso}/saml/metadata",
LoadMetadata = true,
});
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/login", () => Results.Challenge(
new AuthenticationProperties { RedirectUri = "/" }, [Saml2Defaults.Scheme]));
app.MapGet("/logout", () => Results.SignOut(
new AuthenticationProperties { RedirectUri = "/" },
[CookieAuthenticationDefaults.AuthenticationScheme, Saml2Defaults.Scheme]));
app.MapGet("/", (ClaimsPrincipal user) => new
{
Id = user.FindFirstValue(ClaimTypes.NameIdentifier), // pegawai:123 atau mahasiswa:2107110001
Type = user.FindFirstValue("account_type"), // pegawai atau mahasiswa
Name = user.FindFirstValue("name"),
Email = user.FindFirstValue("email"),
}).RequireAuthorization();
app.Run();
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/Saml2 |
|---|---|
| ACS URL | https://app.unri.ac.id/Saml2/Acs |
| SLO URL | https://app.unri.ac.id/Saml2/Logout |
| Login URL | https://app.unri.ac.id/login |
- Metadata IdP dibaca saat aplikasi mulai. Restart aplikasi bila sertifikat SSO diganti.
Go
github.com/crewjam/saml (samlsp)
// go get github.com/crewjam/saml@latest
package main
import (
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"github.com/crewjam/saml/samlsp"
)
const (
appURL = "https://app.unri.ac.id" // CONTOH: ganti dengan alamat aplikasi Anda
ssoURL = "https://sso.dev-app.unri.ac.id" // alamat SSO ini, jangan diubah
)
func main() {
keyPair, err := tls.LoadX509KeyPair("saml/sp.crt", "saml/sp.key")
if err != nil {
panic(err)
}
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
panic(err)
}
metadataURL, _ := url.Parse(ssoURL + "/saml/metadata")
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *metadataURL)
if err != nil {
panic(err)
}
rootURL, _ := url.Parse(appURL)
sp, err := samlsp.New(samlsp.Options{
URL: *rootURL,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
IDPMetadata: idpMetadata,
SignRequest: true,
CookieSameSite: http.SameSiteNoneMode, // wajib HTTPS
})
if err != nil {
panic(err)
}
// /saml/metadata dan /saml/acs disediakan oleh middleware.
http.Handle("/saml/", sp)
http.Handle("/", sp.RequireAccount(http.HandlerFunc(home)))
http.ListenAndServe(":8000", nil)
}
func home(w http.ResponseWriter, r *http.Request) {
claims := samlsp.SessionFromContext(r.Context()).(samlsp.JWTSessionClaims)
id := claims.Subject // pegawai:123 atau mahasiswa:2107110001
accountType := samlsp.AttributeFromContext(r.Context(), "account_type") // pegawai atau mahasiswa
name := samlsp.AttributeFromContext(r.Context(), "name")
fmt.Fprintf(w, "%s (%s) %s", name, accountType, id)
}
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/saml/acs |
| SLO URL | Tidak ada |
| Login URL | https://app.unri.ac.id/ |
- samlsp tidak menangani Single Logout. Daftarkan tanpa SLO URL; logout cukup menghapus sesi lokal dengan sp.Session.DeleteSession.
Ruby
ruby-saml 1.18.0 ke atas (contoh dengan Rails)
# Gemfile: gem "ruby-saml", ">= 1.18.0"
#
# config/routes.rb:
# get "saml/login", to: "saml#login"
# post "saml/acs", to: "saml#acs"
# get "saml/logout", to: "saml#logout"
# get "saml/slo", to: "saml#slo"
# get "saml/metadata", to: "saml#metadata"
class SamlController < ApplicationController
skip_forgery_protection only: :acs
APP = "https://app.unri.ac.id" # CONTOH: ganti dengan alamat aplikasi Anda
SSO = "https://sso.dev-app.unri.ac.id" # alamat SSO ini, jangan diubah
def login
auth_request = OneLogin::RubySaml::Authrequest.new
url = auth_request.create(saml_settings)
session[:saml_request_id] = auth_request.uuid
redirect_to url, allow_other_host: true
end
def acs
response = OneLogin::RubySaml::Response.new(
params[:SAMLResponse],
settings: saml_settings,
matches_request_id: session.delete(:saml_request_id)
)
unless response.is_valid?
Rails.logger.warn("Login SSO gagal: #{response.errors.join(', ')}")
return head :forbidden
end
reset_session
session[:user] = {
"id" => response.nameid, # pegawai:123 atau mahasiswa:2107110001
"type" => response.attributes["account_type"], # pegawai atau mahasiswa
"name" => response.attributes["name"],
"email" => response.attributes["email"]
}
session[:saml] = { "name_id" => response.nameid, "session_index" => response.sessionindex }
redirect_to "/"
end
def logout
settings = saml_settings
settings.name_identifier_value = session.dig(:saml, "name_id")
settings.sessionindex = session.dig(:saml, "session_index")
logout_request = OneLogin::RubySaml::Logoutrequest.new
url = logout_request.create(settings)
session[:saml_logout_id] = logout_request.uuid
redirect_to url, allow_other_host: true
end
# Menangani dua hal: SSO meminta logout, atau SSO menjawab logout dari aplikasi ini.
def slo
if params[:SAMLRequest]
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(
params[:SAMLRequest], settings: saml_settings, get_params: request.query_parameters
)
return head :bad_request unless logout_request.is_valid?
reset_session
url = OneLogin::RubySaml::SloLogoutresponse.new.create(
saml_settings, logout_request.id, nil, RelayState: params[:RelayState]
)
redirect_to url, allow_other_host: true
else
logout_response = OneLogin::RubySaml::Logoutresponse.new(
params[:SAMLResponse], saml_settings,
matches_request_id: session[:saml_logout_id], get_params: request.query_parameters
)
return head :bad_request unless logout_response.validate && logout_response.success?
reset_session
redirect_to "/"
end
end
def metadata
render xml: OneLogin::RubySaml::Metadata.new.generate(saml_settings)
end
private
def saml_settings
OneLogin::RubySaml::Settings.new.tap do |s|
s.sp_entity_id = "#{APP}/saml/metadata"
s.assertion_consumer_service_url = "#{APP}/saml/acs"
s.single_logout_service_url = "#{APP}/saml/slo"
s.idp_entity_id = "#{SSO}/saml/metadata"
s.idp_sso_service_url = "#{SSO}/saml/sso"
s.idp_slo_service_url = "#{SSO}/saml/slo"
s.idp_cert = File.read(Rails.root.join("config/saml/idp.crt"))
s.certificate = File.read(Rails.root.join("config/saml/sp.crt"))
s.private_key = File.read(Rails.root.join("config/saml/sp.key"))
s.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
s.security[:authn_requests_signed] = true
s.security[:logout_requests_signed] = true
s.security[:logout_responses_signed] = true
s.security[:want_assertions_signed] = true
s.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
s.security[:digest_method] = XMLSecurity::Document::SHA256
end
end
end
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/saml/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/saml/acs |
| SLO URL | https://app.unri.ac.id/saml/slo |
| Login URL | https://app.unri.ac.id/saml/login |
- Versi sebelum 1.18.0 punya celah pemalsuan tanda tangan (CVE-2025-25291 dan CVE-2025-25292).
Tanpa ubah kode
Apache mod_auth_mellon di depan aplikasi apa pun
# apt install libapache2-mod-auth-mellon
# curl -o /etc/apache2/saml/idp-metadata.xml https://sso.dev-app.unri.ac.id/saml/metadata
<VirtualHost *:443>
# CONTOH: ganti app.unri.ac.id dan 127.0.0.1:8000 dengan alamat aplikasi Anda
ServerName app.unri.ac.id
<Location />
MellonEnable info
MellonSPentityId "https://app.unri.ac.id/mellon/metadata"
MellonSPPrivateKeyFile /etc/apache2/saml/sp.key
MellonSPCertFile /etc/apache2/saml/sp.crt
MellonIdPMetadataFile /etc/apache2/saml/idp-metadata.xml
MellonEndpointPath /mellon
MellonSignatureMethod rsa-sha256
MellonSecureCookie On
MellonCookieSameSite None
</Location>
# Bagian yang wajib login.
<Location /app>
AuthType Mellon
MellonEnable auth
Require valid-user
# Diteruskan ke aplikasi di belakangnya, apa pun bahasanya.
RequestHeader set X-SSO-Id "%{MELLON_NAME_ID}e"
RequestHeader set X-SSO-Type "%{MELLON_account_type}e"
RequestHeader set X-SSO-Name "%{MELLON_name}e"
</Location>
ProxyPass /app http://127.0.0.1:8000/app
ProxyPassReverse /app http://127.0.0.1:8000/app
</VirtualHost>
Alamat yang didaftarkan Contoh
Ganti https://app.unri.ac.id dengan alamat aplikasi Anda. Path di belakangnya adalah bawaan library ini.
| Entity ID | https://app.unri.ac.id/mellon/metadata |
|---|---|
| ACS URL | https://app.unri.ac.id/mellon/postResponse |
| SLO URL | https://app.unri.ac.id/mellon/logout |
| Login URL | https://app.unri.ac.id/app |
- Cocok untuk aplikasi lama atau bahasa yang tidak punya library SAML. Aplikasi cukup membaca header X-SSO-Id dan X-SSO-Type.
- Aplikasi di belakangnya hanya boleh bisa dijangkau lewat Apache. Bila bisa diakses langsung, header itu bisa dipalsukan.
Bahasa lain
Library SAML 2.0 Service Provider apa pun bisa dipakai bila mendukung:
- AuthnRequest lewat HTTP-Redirect yang ditandatangani RSA-SHA256 (parameter
SigAlgdanSignature). - Menerima SAML Response lewat HTTP-POST dan memeriksa tanda tangan Response maupun Assertion.
- Memeriksa
InResponseTo,Audience,Destination, dan masa berlaku. - Opsional: Single Logout lewat HTTP-Redirect, juga bertanda tangan.
Aturan wajib
- Kenali pengguna dari NameID, bukan dari email. Mahasiswa bisa mengubah email sendiri, dan sebagian email dipakai bersama.
- Wajibkan tanda tangan dan jangan menonaktifkan validasi, termasuk saat uji coba.
- Periksa
InResponseTo. Simpan ID AuthnRequest saat login dimulai, lalu cocokkan di ACS. Tanpa ini respons curian bisa dipakai ulang. - Kecualikan ACS dari proteksi CSRF. POST datang dari SSO, bukan dari form aplikasi.
- Buat sesi baru setelah login (regenerate session ID).
- Pakai HTTPS untuk aplikasi.
- Simpan
sp.keydi luar git dengan izin baca hanya untuk aplikasi.
Cookie dan SameSite
Bila alamat aplikasi dan SSO sama-sama di *.unri.ac.id, keduanya dianggap satu situs dan cookie sesi biasa sudah cukup.
Bila beda situs, browser tidak mengirim cookie SameSite=Lax pada POST dari SSO, sehingga ID AuthnRequest yang disimpan di sesi
tidak terbaca dan validasi InResponseTo gagal. Simpan ID itu di cookie terpisah dengan SameSite=None; Secure,
atau set cookie sesi ke SameSite=None; Secure.
Masalah umum
| Gejala | Penyebab |
|---|---|
| "Aplikasi ini belum terdaftar di SSO UNRI" | Issuer di AuthnRequest tidak sama persis dengan Entity ID yang didaftarkan. |
| SSO menolak permintaan login | Tanda tangan AuthnRequest tidak ada atau tidak cocok dengan sertifikat terdaftar, ACS di permintaan berbeda dari yang didaftarkan, atau jam server melenceng lebih dari 120 detik. |
| Aplikasi menolak respons karena tanda tangan | idp.crt di aplikasi bukan sertifikat SSO yang sedang dipakai. |
Aplikasi menolak respons karena InResponseTo | ID AuthnRequest hilang dari sesi. Lihat Cookie dan SameSite. |
| Aplikasi menolak respons karena Destination atau Audience | Alamat aplikasi di konfigurasi berbeda dari alamat publiknya, biasanya karena berjalan di balik reverse proxy. |
| "Akun Anda tidak memiliki akses ke ..." | Jenis akun pengguna tidak termasuk yang diizinkan untuk aplikasi ini. |