npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@starasia/sso-api

v3.6.0

Published

Validasi access token SSO Starasia (OIDC resource server), token server-ke-server, login halaman sendiri, dan BFF untuk backend Node.js/Express

Readme

@starasia/sso-api

Validasi access token SSO Starasia untuk backend Node.js/Express (OAuth 2.0 resource server).

Untuk aplikasi yang login memakai @starasia/sso (OIDC authorization code + PKCE). Backend tidak menukar token atau menyimpan client secret aplikasi FE — cukup memvalidasi access token yang dikirim frontend.

Sejak 3.1.0 juga untuk server-ke-server: createServiceClient mengambilkan token layanan (client_credentials) untuk memanggil API layanan lain, dan requireAuth({ services }) membatasi endpoint hanya untuk layanan tertentu. Lihat Server-ke-server.

Sejak 3.2.0 juga untuk aplikasi internal yang punya halaman login sendiri: createPasswordLoginClient dan createLoginRouter. Lihat Halaman login sendiri.

Sejak 3.6.0 juga untuk pola BFF: login tetap lewat halaman SSO, tetapi backend yang menukar kode login dan menyimpan token di cookie terenkripsi, jadi browser tidak memegang token: createBff. Lihat BFF.

v3 menghapus API lama (config(), auth(), AUTH_TYPE, rute /auth/*) dan memperketat validasi token. Lihat CHANGELOG untuk daftar breaking change dan langkah migrasi.

npm install @starasia/sso-api@^3
import express from 'express'
import { createResourceServer, errorHandler, RequestWithAuth } from '@starasia/sso-api'

const sso = createResourceServer({
  issuer: 'https://sso.starasia.tech',         // = `iss` token (issuer di discovery)
  audiences: ['APP-xxxxxxxx', 'APP-yyyyyyyy'], // client ID app FE yang boleh memanggil backend ini
})

const app = express()
app.use('/api', sso.requireAuth())                          // wajib login
app.get('/api/me', (req, res) => res.json((req as RequestWithAuth).auth))
app.get('/api/me/email', sso.requireAuth({ scopes: ['email'] }), handler) // wajib scope OAuth
app.use(errorHandler)

Contoh lengkap: examples/resource-server.ts.

Yang diverifikasi di setiap request

  1. Signature RS256 dengan kunci dari JWKS (<issuer>/.well-known/jwks.json), dipilih berdasarkan kid. Bila JWK menyebut alg, nilainya wajib sama dengan alg header token (RFC 8725 §3.1). kid baru (rotasi kunci) memicu pengambilan ulang JWKS, dibatasi sekali per 30 detik. JWKS di-cache 10 menit (jwksCacheTtl).
  2. Header typ = at+jwt (RFC 9068) — selalu dicek, tidak bisa dimatikan. ID token (typ: JWT) yang dikirim sebagai bearer ditolak.
  3. Klaim wajib access token: sub (string tidak kosong), client_id, exp, iat. Token tanpa client_id (mis. ID token) ditolak walau semua pengecekan lain lolos.
  4. iss = issuer, aud ∈ audiences, exp/nbf (toleransi 5 detik).
  5. Opsional: status token ke /introspect (lihat Introspeksi).

req.auth (AuthContext): sub, clientId, isService (token layanan: sub = clientId), scopes, jti, issuedAt, expiresAt, authTime, sid (ID sesi SSO), claims, token.

Scope ≠ role user

requireAuth({ scopes }) mengecek scope OAuth yang didelegasikan user ke aplikasi (client), bukan role atau izin user. SSO hanya pernah memberikan openid, profile, email, dan offline_access; scope lain yang diminta (mis. admin) dibuang oleh SSO, sehingga requireAuth({ scopes: ['admin'] }) tidak pernah terpenuhi — dan seandainya bisa pun, scope tidak menyatakan hak akses user.

Otorisasi user (admin, owner, anggota organisasi, dst.) wajib dicek di backend dari data entitlement server-side berdasarkan req.auth.sub:

import { AppError, ERROR_MESSAGES, HTTP_STATUS } from '@starasia/sso-api'

app.delete('/api/items/:id', sso.requireAuth(), async (req, res, next) => {
  const { sub } = (req as RequestWithAuth).auth
  if (!(await entitlements.isAdmin(sub))) {
    return next(new AppError(HTTP_STATUS.FORBIDDEN, ERROR_MESSAGES.ACCESS_DENIED))
  }
  // ...
})

Token belum membawa klaim organisasi/role — cek keanggotaan organisasi di backend (mis. dari header x-organization-id) sendiri.

Audience: client ID atau identifier API (RFC 8707)

Secara default aud access token = client ID app FE, sehingga backend mendaftar client ID setiap app FE yang boleh memanggilnya di audiences.

Untuk API yang dipakai beberapa app, gunakan resource indicator (RFC 8707) — token diterbitkan khusus untuk API Anda dengan aud = identifier API:

  1. Admin dashboard: daftarkan identifier API (URI absolut, mis. https://api.example.com/hris) di "Allowed Resources" setiap client FE yang boleh memanggil API ini.

  2. Frontend (@starasia/sso): kirim resource ke /authorize lewat opsi resource:

    setupSSO({
      authority: 'https://sso.starasia.tech',
      clientId: 'APP-xxxxxxxx',
      resource: 'https://api.example.com/hris',
    })

    (extraQueryParams: { resource: 'https://api.example.com/hris' } juga tetap bisa dipakai.) resource yang tidak ada di allowlist client ditolak SSO (error=invalid_target, login gagal) — tidak jatuh balik ke aud = client ID. Tanpa resource, aud tetap client ID.

  3. Backend: set audiences ke identifier yang sama persis dengan yang tampil di admin dashboard — aud dicocokkan apa adanya. SSO menyimpan identifier dalam bentuk baku: origin tanpa path mendapat garis miring di akhir (https://api.example.com → https://api.example.com/), jadi pakai identifier ber-path seperti contoh ini atau tulis lengkap dengan /.

    const sso = createResourceServer({
      issuer: 'https://sso.starasia.tech',
      audiences: ['https://api.example.com/hris'],
    })

    req.auth.clientId tetap berisi app FE pemohon (client_id).

Introspeksi (opsional)

JWT bersifat mandiri: tanpa introspeksi, token yang sudah diterbitkan tetap diterima sampai exp. Dengan introspection aktif, backend menanyakan status token ke POST <issuer>/introspect (RFC 7662) dan menolak token dalam jendela cache (default 30 detik) setelah:

  • user logout (sesi SSO diakhiri),
  • reset password atau user dinonaktifkan (semua token user dicabut),
  • user dihapus / tidak aktif.

Introspeksi wajib confidential client: daftarkan client dengan secret di admin dashboard, lalu isi clientId + clientSecret (dikirim sebagai client_secret_basic). Tanpa keduanya createResourceServer langsung melempar error.

const sso = createResourceServer({
  issuer: 'https://sso.starasia.tech',
  audiences: ['APP-xxxxxxxx'],
  introspection: {
    clientId: process.env.SSO_INTROSPECTION_CLIENT_ID!,
    clientSecret: process.env.SSO_INTROSPECTION_CLIENT_SECRET!,
    // cacheTtl: 30, // detik
  },
})

Respons introspeksi divalidasi: active === true, token_type (bila ada) = access_token, sub sama dengan sub JWT, client_id (bila ada) sama dengan client_id JWT. Hasil aktif maupun tidak aktif di-cache per token; error tidak di-cache. Request serentak untuk token yang sama digabung jadi satu.

Rate limit SSO: 1200 request/menit per client. Saat SSO membalas 429/503 dengan Retry-After, selama jendela itu introspeksi langsung gagal 503 (dengan header Retry-After) tanpa request ke SSO; token yang hasilnya masih ter-cache tetap dilayani.

Server-ke-server

Untuk layanan yang memanggil API layanan lain tanpa user (job terjadwal, sinkronisasi, antar-backend) — pengganti kunci statis bersama (x-guard-key, x-admin-key). Butuh sso-api yang mendukung grant client_credentials.

Persiapan (admin dashboard): daftarkan layanan pemanggil sebagai Confidential client, isi Allowed Resources dengan identifier API tujuan, simpan Client Secret (muncul sekali) di secret manager layanan pemanggil.

Layanan pemanggil (A) — token diambil, di-cache sampai mendekati kedaluwarsa, dan request bersamaan digabung jadi satu request ke SSO:

import { createServiceClient } from '@starasia/sso-api'

const hris = createServiceClient({
  issuer: 'https://sso.starasia.tech',
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET!, // dari secret manager
  resource: 'https://api.starasia.tech/hris',   // identifier API tujuan (Allowed Resources)
})

// fetch global + Authorization: Bearer; 401 dari tujuan → token baru, coba sekali lagi
const res = await hris.fetch(`${HRIS_URL}/internal/employees`)

// HTTP client lain (axios, dll.)
await axios.get(url, { headers: { Authorization: await hris.authorizationHeader() } })

| Method | Keterangan | |---|---| | getToken() | Access token layanan (cache; diperbarui refreshSkew detik sebelum kedaluwarsa) | | authorizationHeader() | Bearer <token> | | fetch(input, init?) | fetch global dengan token; 401 → ambil token baru & coba sekali lagi (body stream tidak dicoba ulang) | | invalidate(token?) | Buang token dari cache |

Opsi: issuer, clientId, clientSecret, resource (wajib); tokenEndpoint (default ${issuer}/token), refreshSkew (default 60 detik), timeoutMs (default 5000). Error: SSO menolak (secret salah, client nonaktif, resource tidak diizinkan) → AppError 500 SSO rejected the service token request (<kode OAuth>); SSO tidak tersedia / 429 / 503 → 503 SSO unavailable (Retry-After dihormati, selama jendela itu tanpa request ke SSO).

Layanan tujuan (B) — audiences = identifier-nya sendiri; services = client ID layanan yang boleh memanggil endpoint ini (token user dan layanan lain → 403):

const sso = createResourceServer({
  issuer: 'https://sso.starasia.tech',
  audiences: ['https://api.starasia.tech/hris'],
})

app.get('/internal/employees', sso.requireAuth({ services: [process.env.PAYROLL_CLIENT_ID!] }), handler)

Token layanan tidak membawa scope — otorisasinya berdasarkan layanan mana yang memanggil (req.auth.clientId, req.auth.isService === true). Untuk endpoint yang menerima user maupun layanan, pakai requireAuth() lalu periksa req.auth.isService sendiri.

BFF: login lewat halaman SSO, token di server

Pola Backend For Frontend: user login di halaman SSO (single sign-on tetap berjalan), tetapi kode login dikirim ke backend aplikasi. Backend menukarnya dengan token memakai Client Secret, lalu menyimpan token di cookie HttpOnly yang terenkripsi. JavaScript di browser tidak pernah memegang token, jadi XSS tidak bisa mencurinya. Cocok bila frontend dan backend satu origin (mis. Express yang juga menyajikan SPA, atau server-rendered).

Persiapan (admin dashboard): daftarkan sekali sebagai tipe Server:

| Isian | Contoh | |---|---| | Redirect URIs | https://app.example.com/auth/callback (path tempat router dipasang + /callback) | | Post-Logout Redirect URIs | https://app.example.com/ |

Client Secret dan SESSION_SECRET (acak, ≥ 32 karakter, mis. openssl rand -base64 48) disimpan di secret manager.

import express from 'express'
import { createBff, errorHandler, RequestWithAuth } from '@starasia/sso-api'

const bff = createBff({
  issuer: 'https://sso.starasia.tech',
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET!,
  redirectUri: 'https://app.example.com/auth/callback',
  sessionSecret: process.env.SESSION_SECRET!,
})

const app = express()
app.use('/auth', bff.router())
app.use('/api', bff.requireAuth())                 // req.auth seperti createResourceServer
app.get('/api/me', (req, res) => res.json((req as RequestWithAuth).auth))
app.use(errorHandler)

| Rute | Hasil | |---|---| | GET /auth/login?returnTo=/halaman | Redirect ke halaman login SSO (PKCE S256, state, nonce) | | GET /auth/callback | Dari SSO: tukar kode, pasang cookie sesi, redirect ke returnTo (hanya path di aplikasi ini) | | GET /auth/session | { user, expiresAt }, atau 401 bila belum login | | POST /auth/logout | { redirectTo } — refresh token dicabut, cookie dihapus; arahkan browser ke redirectTo (end_session SSO) agar sesi SSO ikut berakhir |

Di frontend cukup fetch biasa — cookie ikut otomatis, tanpa header Authorization:

const res = await fetch('/api/me')
if (res.status === 401) location.assign(`/auth/login?returnTo=${encodeURIComponent(location.pathname)}`)

// Logout
const { data } = await (await fetch('/auth/logout', { method: 'POST' })).json()
location.assign(data.redirectTo)

Yang diurus createBff:

  • Cookie sesi sa_session; Path=/; HttpOnly; Secure; SameSite=Lax, isinya access token, refresh token, dan profil, dienkripsi AES-256-GCM dengan sessionSecret. sessionSecret berupa array untuk rotasi kunci (yang pertama dipakai mengenkripsi).
  • Perpanjangan otomatis: access token yang tersisa < 60 detik diperpanjang dengan refresh token saat request masuk, dan cookie baru dikirim. Request bersamaan memakai satu perpanjangan. Sesi yang sudah berakhir di SSO (logout, reset password, nonaktif) → 401 dan cookie dihapus.
  • CSRF: request POST/PUT/PATCH/DELETE dari origin lain ditolak 403 (Sec-Fetch-Site, lalu Origin). Frontend di origin lain yang sah → trustedOrigins.
  • Verifikasi token sama dengan createResourceServer (aud = clientId, atau resource bila diisi). introspection: true membuat token yang dicabut langsung ditolak, tanpa menunggu access token kedaluwarsa.

Memanggil API lain atas nama user: const token = await bff.getAccessToken(req, res).

| Opsi | Default | Keterangan | |---|---|---| | issuer, clientId, clientSecret, redirectUri, sessionSecret | — (wajib) | | | postLogoutRedirectUri | origin redirectUri + / | Harus terdaftar di Post-Logout Redirect URIs | | scope | openid profile email offline_access | | | resource | — | Identifier API (RFC 8707) → aud access token | | audiences | [resource ?? clientId] | aud yang diterima requireAuth | | introspection | false | true / { cacheTtl } — cek pencabutan token ke SSO | | trustedOrigins | — | Origin lain yang boleh mengirim request pengubah data | | cookie | { name: 'sa_session', secure: true, sameSite: 'Lax', maxAgeSec: 2592000 } | secure: false hanya untuk lokal tanpa HTTPS | | onLoginError | halaman HTML sederhana | (error, req, res) — BffLoginError.code: login_cancelled, login_expired, login_failed | | timeoutMs | 5000 | |

Satu aplikasi BFF = satu cookie sesi per host. Aplikasi lain di host yang sama (beda port atau path) harus memakai cookie.name yang berbeda.

Halaman login sendiri

Untuk aplikasi internal yang tidak mau redirect ke halaman login SSO: backend aplikasi mengirim username (email atau NRP) + password user ke SSO (grant password) dan menerima token biasa. Butuh sso-api yang mendukung grant ini; panduan lengkap di sso-api docs/panduan-login-halaman-sendiri.md.

Bukan single sign-on. Akun dan sub sama dengan aplikasi lain, tetapi user yang sudah login di aplikasi lain tetap harus mengetik password di aplikasi ini (dan sebaliknya). Reset password, penonaktifan, dan force logout admin tetap mengeluarkan user dari aplikasi ini.

Persiapan (admin dashboard): aplikasi Confidential client, superadmin menyalakan Login dengan halaman sendiri (allowPasswordGrant), opsional IP server di passwordGrantIps. Client Secret di secret manager.

Cara cepat — router siap pakai (pola BFF): refresh token disimpan di cookie HttpOnly milik aplikasi, frontend hanya menerima access token (simpan di memori).

import express from 'express'
import { createPasswordLoginClient, createLoginRouter } from '@starasia/sso-api'

const sso = createPasswordLoginClient({
  issuer: 'https://sso.starasia.tech',
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET!, // dari secret manager
})

const app = express()
app.set('trust proxy', 1) // agar req.ip = IP user di belakang proxy (untuk audit SSO)
app.use(express.json())
app.use('/auth', createLoginRouter(sso))

| Rute | Body (JSON) | Hasil | |---|---|---| | POST /auth/login | { username, password } | { accessToken, expiresIn, user } + cookie refresh token | | POST /auth/refresh | — | { accessToken, expiresIn, user } + cookie baru | | POST /auth/logout | — | 204, token dicabut di SSO, cookie dihapus | | POST /auth/activate-nrp | { nrp, email, password, name? } | 204 — aktivasi akun NRP (password = NRP) | | POST /auth/resend-verification | { email } | 204 | | POST /auth/forgot-password | { email } | 204 | | POST /auth/reset-password | { token, password } | 204 — token = resetToken dari error login, link email, atau /verify-email | | POST /auth/verify-email | { token } | { resetToken? } — token dari link email; resetToken ada → user harus set password baru |

Semua rute wajib Content-Type: application/json (pertahanan CSRF) dan membalas Cache-Control: no-store. Cookie default sa_refresh; Path=<mount>; HttpOnly; Secure; SameSite=Strict; Max-Age=30 hari — atur lewat createLoginRouter(sso, { cookie: { name, secure, sameSite, path, domain, maxAgeSec }, endUserIp, refreshTokenIn }). Rute lain diteruskan ke next().

Aplikasi native (mobile/desktop) — form login native, backend aplikasi tetap memegang Client Secret. Aplikasi native tidak cocok dengan cookie, jadi refresh token lewat body JSON dan disimpan aplikasi di Keychain/Keystore (sejak 3.4.0):

app.use('/auth', createLoginRouter(sso)) // frontend web: cookie
app.use('/mobile/auth', createLoginRouter(sso, { refreshTokenIn: 'body' })) // aplikasi native

| Rute (refreshTokenIn: 'body') | Body (JSON) | Hasil | |---|---|---| | POST /mobile/auth/login | { username, password } | { accessToken, expiresIn, refreshToken, user } | | POST /mobile/auth/refresh | { refreshToken } | { accessToken, expiresIn, refreshToken, user } — simpan refreshToken yang baru | | POST /mobile/auth/logout | { refreshToken } | 204, token dicabut di SSO |

Rute pendukung dan format error sama. Tidak ada Set-Cookie; opsi cookie diabaikan. Jangan pakai mode ini untuk frontend web — refresh token jadi terbaca JavaScript.

Refresh bersamaan dengan refresh token yang sama (mis. beberapa tab) digabung jadi satu request ke SSO. IP user (endUserIp, default req.ip) ikut ke SSO di login dan endpoint pendukung, sehingga rate limit SSO menghitung per user, bukan per IP server aplikasi.

Halaman reset password & verifikasi email sendiri — isi URL Halaman Reset Password dan URL Halaman Verifikasi Email aplikasi di Admin Dashboard (butuh sso-api yang mendukungnya). Email yang diminta lewat klien/router ini lalu berisi link ke halaman aplikasi dengan ?token=:

| Halaman aplikasi | Yang dilakukan | |---|---| | Reset password (?token=) | Form password baru → POST /auth/reset-password { token, password } | | Verifikasi email (?token=) | POST /auth/verify-email { token }; ada resetToken → form password baru → /auth/reset-password dengan resetToken; tidak ada → user login |

Email yang diminta tanpa kredensial aplikasi (mis. lewat halaman SSO) tetap berisi link ke halaman SSO.

Error untuk frontend — code menentukan layar:

{ "success": false, "error": { "message": "...", "code": "password_change_required", "statusCode": 403, "resetToken": "..." } }

| code | HTTP | Layar di aplikasi | |---|---|---| | invalid_grant | 401 | Username/password salah; di /refresh = sesi berakhir, login ulang | | nrp_activation_required | 403 | Form aktivasi NRP → /auth/activate-nrp | | email_not_verified | 403 | Info + tombol kirim ulang → /auth/resend-verification | | account_inactive | 403 | Akun nonaktif, hubungi admin | | password_change_required | 403 | Form password baru → /auth/reset-password dengan resetToken | | too_many_requests | 429 | Tunggu Retry-After |

Tanpa code: 500 SSO rejected the password login client (<kode OAuth>) = konfigurasi aplikasi salah (secret, flag belum dinyalakan, IP); 503 SSO unavailable; 4xx lain dari endpoint pendukung diteruskan dengan pesan SSO (mis. NRP tidak ditemukan).

Tanpa router — pakai klien langsung, mis. untuk sesi server sendiri:

try {
  const { accessToken, refreshToken, user } = await sso.login({ username, password, endUserIp: req.ip })
} catch (err) {
  if (err instanceof PasswordLoginError) {
    // err.code, err.resetToken, err.statusCode, err.retryAfter
  }
  throw err
}
const next = await sso.refresh(refreshToken) // refresh token berputar: simpan yang baru
await sso.logout(refreshToken)

| Method | Keterangan | |---|---| | login({ username, password, endUserIp?, scope? }) | Token + user (sub, name, email, emailVerified) dari ID token | | refresh(refreshToken) | Token baru; refresh token lama tidak berlaku lagi | | logout(refreshToken) | Cabut token & akhiri sesi aplikasi ini di SSO | | activateNrp, resendVerification, forgotPassword, resetPassword, verifyEmail | Endpoint /auth/* SSO, dengan clientId dan kredensial aplikasi; opsi endUserIp → X-End-User-IP |

Opsi: issuer, clientId, clientSecret (wajib); scope (default openid profile email offline_access), resource, tokenEndpoint, revocationEndpoint, timeoutMs (default 5000).

API aplikasi tetap memvalidasi access token dengan createResourceServer seperti biasa — audiences memuat client ID aplikasi ini.

Fail closed

Bila keabsahan token tidak bisa dipastikan, request ditolak — tidak pernah diloloskan.

| Kondisi | Respons | |---|---| | JWKS gagal diambil, kunci lama belum lewat jwksCacheTtl + jwksMaxStale | kunci lama dipakai | | JWKS belum pernah berhasil diambil / kunci lama lewat jwksMaxStale (default 24 jam) | 503 SSO unavailable + Retry-After | | Gagal ambil JWKS berulang | backoff 1 dtk → 2 → 4 … maks. 60 dtk; selama backoff tidak ada request ke SSO | | JWKS sah tapi keys: [] | kunci lama dibuang → token 401 | | Introspeksi 429/503 | 503 + Retry-After; jendela dihormati | | Introspeksi 5xx lain / timeout / jaringan | 503 SSO unavailable | | Introspeksi 401 (invalid_client) | 500 — salah konfigurasi clientId/clientSecret, bukan kesalahan user |

Opsi

| Opsi | Default | Keterangan | |---|---|---| | issuer | — (wajib) | Issuer SSO | | audiences | — (wajib) | string \| string[] — client ID app FE dan/atau identifier API (RFC 8707) | | jwksUri | ${issuer}/.well-known/jwks.json | | | jwksCacheTtl | 600 | detik — JWKS dianggap segar | | jwksMaxStale | 86400 | detik — batas kunci lama dipakai setelah TTL bila SSO tidak terjangkau | | algorithms | ['RS256'] | | | clockTolerance | 5 | detik | | requiredTokenType | 'at+jwt' | nilai typ kustom (string); tidak bisa dimatikan | | introspection | — | { clientId, clientSecret, endpoint?, cacheTtl? } — clientId/clientSecret wajib | | timeoutMs | 5000 | request ke SSO |

Hasil createResourceServer: requireAuth({ scopes?, services? }), optionalAuth(), verify(token) (tanpa Express), refreshKeys() (ambil ulang JWKS sekarang; gagal → 503), clearIntrospectionCache().

Error

Semua error berupa AppError (statusCode, message, retryAfter?) yang diteruskan ke next(err). errorHandler paket ini membalas JSON standar dan memasang Retry-After bila ada.

| Status | Pesan | Kapan | |---|---|---| | 401 | Authorization token is required | Header Authorization: Bearer … tidak ada | | 401 | Invalid or expired token / Token has expired | Token rusak, klaim wajib tidak ada, kedaluwarsa | | 401 | Invalid token signature / issuer / audience | Signature, kid/alg, iss, aud tidak cocok | | 401 | Wrong token type for this endpoint | Bukan access token (typ, client_id) | | 401 | Token has been revoked | Introspeksi active: false | | 403 | Insufficient scope for this endpoint | Scope OAuth kurang (insufficient_scope) | | 403 | This endpoint only accepts tokens from allowed services | requireAuth({ services }): bukan token layanan yang diizinkan | | 500 | SSO rejected the introspection client credentials | Kredensial introspeksi salah | | 500 | SSO rejected the service token request (<kode>) | createServiceClient: secret salah, client nonaktif, resource tidak diizinkan | | 500 | SSO rejected the password login client (<kode>) | createPasswordLoginClient: secret salah, flag belum dinyalakan, IP tidak diizinkan | | 401 | Sign-in required | createBff: belum login atau sesi berakhir | | 403 | Cross-site request rejected | createBff: request pengubah data dari origin lain | | 500 | SSO rejected the BFF client (<kode>) | createBff: secret salah, Redirect URI tidak terdaftar | | 503 | SSO unavailable | JWKS/introspeksi tidak tersedia (fail closed) |

401 membawa WWW-Authenticate: Bearer error="invalid_token", 403 membawa error="insufficient_scope" (RFC 6750).

{
  "success": false,
  "error": {
    "message": "Invalid or expired token",
    "statusCode": 401
  }
}

Helper Express lain yang tetap diekspor: errorHandler, notFoundHandler, asyncWrapper, successResponse, errorResponse, AppError, ERROR_MESSAGES, HTTP_STATUS. PasswordLoginError (turunan AppError) untuk status akun di Halaman login sendiri, BffLoginError untuk login BFF yang tidak selesai.

Migrasi dari v1 (config() + auth())

API v1 sudah dihapus di v3.

| v1 | v3 | |---|---| | config({ clientId, clientSecret, baseUrl, redisUrl, publicKeyUrl }) | createResourceServer({ issuer, audiences }) | | Public key tunggal dari /auth/public-key-test | JWKS by kid | | Klaim type === 'ACCESS' | Header typ: at+jwt + klaim client_id | | aud = client backend | aud ∈ client app FE atau identifier API (audiences) | | Cek Redis sso:token:* | Opsional /introspect (confidential client) | | req.tokenPayload | req.auth | | Rute /auth/* (login, callback, refresh, logout) | Tidak perlu — FE bicara langsung ke SSO |

Requirements

  • Node.js >= 18.0.0
  • Express.js >= 4.18.0

Development

pnpm install
pnpm test        # jest
pnpm run build   # tsc → lib/
pnpm run lint

License

MIT © Starasia

Support

For issues and questions, please open an issue on GitHub.