Sassly Auth · Panduan integrasi

Sassly Suites · Login terpusat

Hubungkan aplikasi Anda ke login Sassly

Satu akun Sassly (akun Google) untuk semua aplikasi Sassly. Pengguna yang sudah login di satu aplikasi langsung masuk ke aplikasi lain tanpa layar login. Panduan ini untuk aplikasi PHP native; aplikasi dengan framework memakai SDK yang sama.

Mulai integrasi Unduh SDK Skill Claude Code

Cara kerja

  1. Halaman dilindungiUser membuka halaman aplikasi Anda tanpa sesi lokal → aplikasi mengarahkan ke /auth/login.
  2. Ke auth.sassly.aiSDK menyimpan state, nonce, dan PKCE di session, lalu redirect ke https://auth.sassly.ai/oauth/authorize.
  3. Login (atau SSO)Bila user sudah login di aplikasi Sassly lain di browser yang sama, auth langsung kembali tanpa layar apa pun. Bila belum, user memilih akun Google.
  4. CallbackAuth kembali ke /auth/callback membawa code. Aplikasi menukarnya server-to-server menjadi token dan memverifikasi ID token (RS256, JWKS).
  5. Tautkan user lokalAkun Sassly (sub, mis. usr_…) dipetakan ke baris user aplikasi lewat kolom sassly_sub. Role, tenant, dan status aktif tetap urusan aplikasi.
  6. RevalidasiTiap 5 menit SDK mengecek ulang sesi pusat. Logout, blokir, atau pencabutan sesi di pusat ikut mengeluarkan user dari aplikasi.

Auth hanya mengurus identitas. Siapa yang boleh masuk ke aplikasi Anda, role-nya, dan tenant-nya tetap ditentukan database aplikasi Anda.

Persiapan

  • PHP ≥ 8.1 dengan ekstensi curl, openssl, json; session PHP aktif.
  • Folder cache yang bisa ditulis user PHP-FPM (untuk cache JWKS).
  • Tabel user dengan kolom email dan penanda aktif.
  • Data untuk admin Sassly: client_id (huruf kecil, angka, tanda minus, mis. kobe), URL production aplikasi, dan URL callback.

Endpoint discovery: https://auth.sassly.ai/.well-known/openid-configuration · kunci publik: /.well-known/jwks.json.

Aturan wajib

  1. Salin SasslyAuthClient.php apa adanya. Jangan diedit per aplikasi; upgrade dengan mengganti seluruh file.
  2. Jangan mematikan verifikasi TLS. Untuk sertifikat lokal self-signed, pakai opsi ca_file.
  3. Token hanya masuk lewat handleCallback() (server-to-server). Jangan menerima token dari query string, form, atau JavaScript, dan jangan menaruh token di HTML/JS.
  4. Tautkan user lewat email hanya bila email_verified === true dan baris lokal sassly_sub IS NULL. Jangan pernah menimpa sassly_sub yang sudah terisi.
  5. Callback tidak boleh otomatis redirect ke /auth/login saat error atau saat user ditolak (redirect loop). Tampilkan halaman.
  6. Cookie session aplikasi: nama unik per aplikasi, host-only (tanpa domain), HttpOnly, SameSite=Lax, Secure di HTTPS. Bila sebelumnya memakai cookie ber-domain=.sassly.ai, pakai nama baru.
  7. Logout hanya lewat POST + CSRF.
  8. redirect_uri di config harus identik dengan yang didaftarkan (tanpa normalisasi, termasuk slash akhir).
  9. Setiap halaman, endpoint AJAX, dan unduhan file yang dilindungi memanggil sassly_require_login().
  10. Client secret disimpan di .env aplikasi: tidak di-commit, tidak ditulis ke log, tidak ditempel ke chat.

1Daftarkan aplikasi

Hubungi admin Sassly dengan client_id, redirect URI, dan URL setelah logout. Admin menjalankan di server auth:

admin
php bin/auth client:create --id=kobe --name="Kobe" --type=confidential \
  --redirect=https://kobe.sassly.ai/auth/callback --post-logout=https://kobe.sassly.ai/

client_secret (awalan sas_cs_) dicetak sekali; admin menaruhnya langsung di .env aplikasi. Redirect URI tambahan (mis. staging) didaftarkan dengan client:add-redirect. Aplikasi mobile memakai --type=public (PKCE tanpa secret, refresh token dirotasi).

2Pasang file

Unduh lalu letakkan di aplikasi (sesuaikan path bila struktur aplikasi berbeda). Jalankan php -l untuk setiap file setelah disalin.

Bootstrap Sassly di-include dari bootstrap aplikasi, sebelum output apa pun. Route template me-require bootstrap aplikasi lewat __DIR__ . '/../../bootstrap.php' — sesuaikan path itu (komentar ADAPT).

3Konfigurasi .env

.env
SASSLY_AUTH_ISSUER=https://auth.sassly.ai
SASSLY_AUTH_CLIENT_ID=kobe
# Dicetak sekali oleh admin saat client:create (jangan di-commit)
SASSLY_AUTH_CLIENT_SECRET=
# Harus identik dengan redirect URI terdaftar
SASSLY_AUTH_REDIRECT_URI=https://kobe.sassly.ai/auth/callback
SASSLY_AUTH_POST_LOGOUT_REDIRECT_URI=https://kobe.sassly.ai/
# Bisa ditulis user PHP-FPM
SASSLY_AUTH_CACHE_DIR=/var/www/kobe/storage/cache/sassly-auth
SASSLY_AUTH_REVALIDATE_INTERVAL=300
# Unik per aplikasi, host-only, belum pernah dipakai dengan Domain=.sassly.ai
APP_SESSION_NAME=kobe_sid

Lokal (auth di http://127.0.0.1:8701): tambahkan SASSLY_AUTH_ALLOW_INSECURE_HTTP=1; untuk sertifikat self-signed pakai SASSLY_AUTH_CA_FILE. Tulis komentar di baris sendiri — sebagian loader .env menganggap # … setelah nilai sebagai bagian nilai.

Di sassly_auth_bootstrap.php, hanya bagian ADAPT yang diubah:

  • sassly_auth_config() membaca dari sistem config aplikasi. PHP-FPM tidak melihat env shell — nilai harus sampai lewat loader .env aplikasi atau env[…] pool.
  • sassly_session_name() mengembalikan nama session unik aplikasi.
  • sassly_paths() berisi URL route auth yang sebenarnya.

Ganti session_start() aplikasi dengan sassly_session_start(), atau samakan parameter cookie-nya persis.

4Database

  1. Jalankan query cek di migration_users_sassly_sub.sql. Email ganda di dalam satu tenant harus dibereskan dulu.
  2. Tambahkan kolom penaut lewat mekanisme migrasi aplikasi:
sql
ALTER TABLE users
    ADD COLUMN sassly_sub VARCHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL;

-- (a) satu orang = satu baris user:
CREATE UNIQUE INDEX uq_users_sassly_sub ON users (sassly_sub);
-- (b) orang yang sama bisa punya baris di beberapa tenant (pakai halaman pilih ruang kerja):
-- CREATE INDEX ix_users_sassly_sub ON users (sassly_sub);

5Hooks aplikasi

File sassly_auth_hooks.php adalah satu-satunya tempat logika khusus aplikasi:

FungsiYang disesuaikan
sassly_app_pdo()Koneksi PDO aplikasi (ERRMODE_EXCEPTION).
sassly_find_or_link_users()Nama tabel/kolom + kondisi aktif. Cari lewat sassly_sub; tautkan lewat email hanya bila email_verified === true dan sassly_sub IS NULL; kembalikan 0, 1, atau beberapa baris aktif.
sassly_load_active_user()Harus null bila user nonaktif atau tidak tertaut lagi ke sub ini (user yang dinonaktifkan langsung kehilangan akses).
sassly_user_label()Nama tenant + role untuk halaman pilih ruang kerja.
sassly_on_login()Isi key $_SESSION yang dibaca kode aplikasi (mis. user_id, company_id, role), update login terakhir.
sassly_app_session_keys()Semua key yang diisi sassly_on_login() (dihapus saat sesi Sassly berakhir).
php
function sassly_find_or_link_users(array $identity): array
{
    $pdo = sassly_app_pdo();
    $sub = (string) $identity['sub'];
    $email = is_string($identity['email'] ?? null) ? strtolower(trim($identity['email'])) : '';

    if ($email !== '' && ($identity['email_verified'] ?? false) === true) {
        $pdo->prepare('UPDATE users SET sassly_sub = ? WHERE LOWER(email) = ? AND sassly_sub IS NULL')
            ->execute([$sub, $email]);
    }
    $st = $pdo->prepare('SELECT * FROM users WHERE sassly_sub = ? AND is_active = 1 ORDER BY id');
    $st->execute([$sub]);
    return $st->fetchAll(PDO::FETCH_ASSOC);
}

email_verified hanya true bila Google berwenang atas alamat itu (Gmail, Google Workspace) atau domainnya dipercaya di auth. Email kantor non-Google tetap bisa login ke auth tetapi tidak tertaut otomatis: halaman "Akun belum terdaftar" menampilkan ID akun Sassly (usr_…), lalu admin aplikasi mengisi sassly_sub baris user itu secara manual.

6Route login

  • login.php — membuat state/nonce/PKCE lalu redirect ke auth. Bila sudah login, langsung ke return_to.
  • callback.php — verifikasi, pemetaan user, lalu salah satu: login, halaman pilih ruang kerja, atau halaman 403 "Akun belum terdaftar" (tidak memulai sesi; tombol "Keluar / ganti akun" memakai id_token yang ditolak sebagai hint).
  • logout.php — POST + CSRF → prepareLogout() → hancurkan session aplikasi → redirect ke logout auth (mengakhiri sesi pusat di browser itu).
  • choose.php — hanya aplikasi multi-tenant; hapus bila tidak dipakai, beserta cabang count($users) > 1 di callback.

7Lindungi halaman & AJAX

php
// Di guard aplikasi (awal setiap halaman / endpoint AJAX / unduhan yang dilindungi):
$user = sassly_require_login();   // baris user lokal yang aktif, atau request berakhir di sini

sassly_require_login() sudah menangani: redirect ke login untuk halaman (GET menyimpan return_to), 401 JSON untuk AJAX (X-Requested-With: XMLHttpRequest atau Accept: application/json), dan 403 untuk user yang dinonaktifkan di aplikasi. Frontend menangani 401 di wrapper fetch global:

js
if (res.status === 401) {
    window.location = '/auth/login?return_to=' + encodeURIComponent(location.pathname + location.search);
}

Tombol keluar berupa form POST:

html
<form method="post" action="/auth/logout">
    <input type="hidden" name="csrf" value="<?= sassly_e(sassly_csrf_token()) ?>">
    <button type="submit">Keluar</button>
</form>

8API dengan access token (opsional)

Untuk endpoint yang dipanggil app mobile Sassly Suites atau aplikasi lain. Access token berlaku 15 menit, aud = sassly-api. Petakan user lewat sassly_sub saja (tanpa penautan email). Contoh lengkap: api_bearer_example.php.

php
$token = SasslyAuthClient::bearerTokenFromRequest();
try {
    $claims = sassly_auth()->verifyAccessToken((string) $token, 'sassly-api');
} catch (SasslyAuthException $e) {
    http_response_code($e->reason === 'insufficient_scope' ? 403 : 401);
    exit;
}
// $claims['sub'] → SELECT * FROM users WHERE sassly_sub = ? AND is_active = 1

Memanggil API Sassly lain dari aplikasi web: sassly_auth()->accessToken() mengembalikan access token yang masih berlaku (diperbarui otomatis).

9Pindah dari login lama

Simpan login lama di balik saklar config (AUTH_MODE=legacy|sassly) sampai integrasi terverifikasi di staging, lalu hapus kode dan secret Google OAuth lama aplikasi. Halaman login lama diarahkan ke /auth/login.

10Verifikasi

  • Halaman dilindungi → login → kembali ke halaman semula.
  • Sudah login di aplikasi Sassly lain → masuk tanpa layar login.
  • Akun tidak terdaftar → 403 "Akun belum terdaftar"; "Keluar / ganti akun" berfungsi.
  • User dinonaktifkan di DB aplikasi → request berikutnya 403, tanpa redirect loop.
  • Baris yang tertaut ke sub lain tidak diambil alih.
  • Akun dengan email_verified=false tidak tertaut otomatis; setelah sassly_sub diisi manual, akun itu bisa masuk.
  • Browser yang masih menyimpan cookie session lama aplikasi tetap bisa login.
  • Multi-tenant (bila ada) → halaman pilih ruang kerja.
  • Logout → kembali ke halaman depan aplikasi; aplikasi lain keluar ≤ revalidate_interval.
  • AJAX setelah sesi berakhir → 401 → frontend ke login.
  • /auth/login?return_to=//evil.com → mendarat di /.
  • GET logout dan POST tanpa CSRF ditolak.
  • Tidak ada warning/notice PHP di log; tidak ada token di HTML/JS; cookie session tanpa Domain.

API SDK

MethodKegunaan
new SasslyAuthClient(array $config)Validasi config; melempar SasslyAuthException('config_invalid').
buildLoginUrl($returnTo, $options)URL authorize; opsi prompt (none/login), login_hint.
redirectToLogin($returnTo, $options)Redirect ke auth.
handleCallback(?array $query)Verifikasi callback → identity (sub, email, email_verified, name, picture, sid, return_to, claims, tokens).
startSession(array $identity)Login lokal (regenerate session id).
check()NONE / VALID / ENDED; revalidasi tiap interval.
identity()Data user tanpa token.
accessToken()Access token segar untuk memanggil API Sassly.
prepareLogout($state, $idTokenHint)URL logout auth; membersihkan data Sassly lokal.
clearLocal()Hapus data Sassly lokal.
verifyAccessToken($jwt, 'sassly-api', $scope)Verifikasi bearer token di API.
SasslyAuthClient::bearerTokenFromRequest()Ambil bearer token dari request.
SasslyAuthClient::sanitizeReturnTo($path, '/')Cegah open redirect.

Semua kegagalan berupa SasslyAuthException dengan ->reason, ->oauthError, dan ->returnTo — artinya ada di Troubleshooting.

Kontrak protokol (v1)

Endpoint

EndpointPathCatatan
Discovery/.well-known/openid-configurationSDK memakai path tetap.
JWKS/.well-known/jwks.jsonCache 1 jam; diambil ulang sekali bila kid tidak dikenal.
Authorize/oauth/authorizeresponse_type=code, state, nonce, PKCE S256 wajib.
Token/oauth/tokenForm-urlencoded, HTTP Basic (confidential).
Userinfo/oauth/userinfoBearer access token.
Logout/oauth/logoutid_token_hint, post_logout_redirect_uri terdaftar persis.
Revoke/oauth/revokeRFC 7009; selalu 200.
Cabut sesi/api/v1/sessions/revokeKhusus client dengan izin (mis. HR): body sub atau email.

Waktu

ItemNilai
Authorization code60 detik, sekali pakai
ID token5 menit (hanya untuk login)
Access token15 menit
Sesi pusatidle 7 hari, maksimal 30 hari
Revalidasi SDKtiap 300 detik (minimal 30)
Toleransi auth tak terjangkau1800 detik (max_stale), lalu sesi lokal diakhiri
Toleransi jam60 detik

Token endpoint

http
POST /oauth/token   Authorization: Basic base64(urlencode(client_id) ":" urlencode(client_secret))

grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…
→ {"access_token":"…","token_type":"Bearer","expires_in":900,"id_token":"…","refresh_token":"rt_…","scope":"openid email profile"}

grant_type=refresh_token&refresh_token=rt_…
→ {"access_token":"…","token_type":"Bearer","expires_in":900,"scope":"openid email profile"}
   (client confidential tidak menerima refresh token baru; client public menerima token baru setiap kali)
HTTPerrorArti bagi aplikasi
400invalid_grantCode tidak valid/terpakai/kedaluwarsa, PKCE atau redirect_uri tidak cocok, ATAU sesi pusat sudah berakhir (logout, diblokir, dicabut, kedaluwarsa).
401invalid_clientclient_id/secret salah atau client dinonaktifkan.
400invalid_requestParameter wajib kosong, body bukan form, atau dua metode autentikasi sekaligus.
400unsupported_grant_typeGrant selain dua di atas.
5xxserver_errorGangguan di auth.

ID token & access token

KlaimIsi
issIssuer persis: https://auth.sassly.ai
subID akun Sassly, usr_ + 32 hex. Kunci penautan user — stabil selamanya untuk satu akun Google.
audID token: client_id. Access token: sassly-api (+ klaim client_id, scope, jti).
iat, exp, auth_timeInteger epoch detik.
nonce, sidNonce dari SDK; ID sesi pusat ses_ + 32 hex.
email, email_verifiedEmail Google (huruf kecil); email_verified boolean — true hanya bila Google berwenang atas alamat itu atau domainnya dipercaya.
name, pictureProfil Google, bila ada.

Header JWT: {"alg":"RS256","typ":"JWT"|"at+jwt","kid":"…"}. Kunci RSA 2048; kunci baru diterbitkan di JWKS minimal 2 jam sebelum dipakai menandatangani.

Logout & identitas

  • Dengan id_token_hint yang cocok dengan sesi di browser: logout langsung. Tanpa hint yang cocok: auth meminta konfirmasi.
  • Yang dicabut: sesi pusat di browser itu + semua refresh token sesi itu. Aplikasi lain di browser yang sama keluar pada revalidasi berikutnya.
  • Akun Google yang dihapus lalu dibuat ulang dengan email sama mendapat sub baru — jangan ditautkan otomatis ke baris yang sudah tertaut ke sub lama.
  • Auth tidak menyimpan role, tenant, atau status aktif per aplikasi.

Troubleshooting

Berdasarkan SasslyAuthException->reason

reasonPenyebab umumPerbaikan
config_invalidNilai config kosong; issuer/redirect_uri bukan https; client_id tidak cocok [a-z0-9-]; scope tanpa openid. Sering: .env tidak dimuat di PHP-FPM.Cek nilai config di request web (bukan CLI). Issuer http lokal: SASSLY_AUTH_ALLOW_INSECURE_HTTP=1.
session_not_startedSDK dipanggil sebelum session dimulai.Panggil sassly_session_start() lebih dulu.
headers_sentAda output sebelum redirect: BOM, spasi sebelum <?php, echo debug, warning PHP.Pesan error menyebut file & baris asal output; hapus output itu.
state_missing / state_mismatchCallback diulang (refresh/back), login dimulai > 10 menit lalu, tab lain, atau cookie session aplikasi tidak tersimpan.Klik "Coba lagi". Bila selalu terjadi: cek cookie session (Secure di http lokal, domain salah, nama bentrok).
iss_mismatchIssuer di config berbeda dengan issuer server (http/https, port).Samakan SASSLY_AUTH_ISSUER persis.
authorize_error + login_requiredprompt=none tanpa sesi pusat.Normal; template callback mengarahkan ke return_to.
authorize_error + invalid_requestParameter authorize ditolak (state/PKCE/nonce/prompt).Jangan bangun URL authorize sendiri; pakai buildLoginUrl().
invalid_grant (callback)Code dipakai dua kali, > 60 detik, redirect_uri config ≠ yang dipakai, atau sesi pusat dicabut di tengah login.Samakan SASSLY_AUTH_REDIRECT_URI dengan URL callback; cek jam server (NTP).
invalid_grant (check)Sesi pusat berakhir: logout di aplikasi lain, diblokir, dicabut, idle 7 hari / maks. 30 hari.Normal; SDK mengembalikan ENDED dan user diarahkan ke login.
invalid_clientSecret salah, secret lama lewat masa transisi rotasi, client dinonaktifkan.Perbarui secret di .env; hubungi admin Sassly.
network_errorAuth tidak terjangkau: DNS, firewall, timeout, TLS.Lihat pesan cURL di log. curl 60 = CA tidak dipercaya → SASSLY_AUTH_CA_FILE.
token_invalidTanda tangan/iss/aud/exp/typ salah: issuer atau client_id config salah, jam melenceng > 60 detik, ID token dipakai sebagai access token.Samakan config; aktifkan NTP; hapus cache jwks-*.json bila kunci dirotasi.
jwks_unavailableJWKS tidak bisa diambil dan tidak ada cache.Cek akses ke /.well-known/jwks.json; jangan blokir /.well-known/ di Nginx.
identity_invalidstartSession() dipanggil dengan array buatan sendiri.Berikan array hasil handleCallback() apa adanya.

Berdasarkan gejala

GejalaPenyebab & perbaikan
Redirect loop aplikasi ↔ authCallback/guard redirect otomatis ke login saat user ditolak, atau cookie session tidak tersimpan. Pakai template callback dan sassly_require_login() apa adanya; cookie host-only, SameSite=Lax, Secure hanya di https.
"Aplikasi tidak dikenal"client_id belum terdaftar atau dinonaktifkan — hubungi admin Sassly.
"redirect_uri tidak terdaftar"URI di config ≠ URI terdaftar (slash akhir, .php, http/https, port). Samakan, atau minta admin menambah redirect.
403 "Akun belum terdaftar" untuk user yang adaEmail di aplikasi ≠ email Google; baris nonaktif; atau baris sudah tertaut ke sassly_sub lain.
Email kantor non-Google selalu 403email_verified=false → tidak ada penautan otomatis. Isi sassly_sub manual dengan ID dari halaman 403.
Logout di satu aplikasi, aplikasi lain masih login beberapa menitSesuai desain: keluar pada revalidasi berikutnya (≤ revalidate_interval, minimal 30 detik).
Auth meminta konfirmasi saat logoutTidak ada id_token_hint yang cocok (session aplikasi sudah hilang). Normal.
Lancar di CLI, gagal di webConfig/.env tidak sampai ke PHP-FPM; muat lewat mekanisme config aplikasi.
AJAX mendapat HTML halaman loginKirim header X-Requested-With: XMLHttpRequest atau Accept: application/json.

Untuk Claude Code

Skill sassly-auth-integration berisi SDK, template, kontrak, dan troubleshooting di atas, sehingga Claude Code bisa mengaudit aplikasi, menyusun rencana, dan memasang integrasi. Pasang lalu minta: "integrasikan login auth.sassly.ai ke aplikasi ini".

shell
curl -fsSLO https://auth.sassly.ai/assets/sdk/sassly-auth-integration.zip
unzip -o sassly-auth-integration.zip -d ~/.claude/skills/