better-auth-itsme
v0.1.2
Published
BetterAuth plugin for itsme® — Belgium's official digital identity (OIDC). Handles JWE-encrypted ID tokens, private-key JWT client authentication, and itsme®-specific claims.
Maintainers
Readme
better-auth-itsme
A BetterAuth plugin for itsme® — Belgium's official digital identity provider (OIDC).
Handles everything itsme® requires that standard OAuth/OIDC clients don't:
- Nested JWT ID tokens (
JWE(JWS(claims))): decrypts the JWE with your private encryption key and verifies the inner JWS against itsme®'s public JWKS. - Private key JWT client auth: signs a
client_assertionfor every token endpoint request using your private signing key. - Mandatory
service:*scope: automatically prepended to the scope list. - PKCE: always enabled.
- Friendly claim names: itsme®'s namespaced claim URIs mapped to camelCase properties.
Installation
npm install better-auth-itsmePeer dependencies (install if not already present):
npm install better-auth zodServer setup
// auth.ts
import { betterAuth } from 'better-auth';
import { itsme } from 'better-auth-itsme';
export const auth = betterAuth({
plugins: [
itsme({
clientId: process.env.ITSME_CLIENT_ID!,
serviceCode: process.env.ITSME_SERVICE_CODE!,
authMethod: 'private_key_jwt', // or 'client_secret'
environment: 'prd', // 'e2e' for testing
privateSigningKey: JSON.parse(process.env.ITSME_PRIVATE_SIGNING_KEY!),
privateEncryptionKey: JSON.parse(process.env.ITSME_PRIVATE_ENCRYPTION_KEY!),
}),
],
});Using client_secret instead
itsme({
clientId: process.env.ITSME_CLIENT_ID!,
serviceCode: process.env.ITSME_SERVICE_CODE!,
authMethod: 'client_secret',
clientSecret: process.env.ITSME_CLIENT_SECRET!,
})Client setup
// auth-client.ts
import { createAuthClient } from 'better-auth/client';
import { itsmeClient } from 'better-auth-itsme/client';
export const authClient = createAuthClient({
plugins: [itsmeClient()],
});// Trigger sign-in (redirects to itsme®)
await authClient.signIn.itsme({ callbackURL: '/dashboard' });
// With error redirect and explicit sign-up opt-in
await authClient.signIn.itsme({
callbackURL: '/dashboard',
errorCallbackURL: '/login?error=itsme',
requestSignUp: true, // needed when disableImplicitSignUp is set on the server
});
// Manual redirect (no automatic navigation)
const { data } = await authClient.signIn.itsme(
{ callbackURL: '/dashboard' },
{ redirect: false },
);
if (data?.url) window.location.href = data.url;Options
| Option | Type | Required | Description |
|---|---|---|---|
| clientId | string | ✅ | Your itsme® client ID |
| serviceCode | string | ✅ | Your itsme® service code (injected as service:<code> scope) |
| authMethod | 'private_key_jwt' \| 'client_secret' | ✅ | Token endpoint authentication method |
| environment | 'e2e' \| 'prd' | — | Default: 'prd' |
| privateSigningKey | JWK | When private_key_jwt | Private key for signing client_assertion |
| privateEncryptionKey | JWK | When private_key_jwt | Private key for decrypting JWE ID tokens |
| clientSecret | string | When client_secret | Shared secret for auth + ID token decryption |
| scopes | string[] | — | Extra OIDC scopes. Supported: 'profile', 'email', 'address', 'phone', 'eid'. Default: ['profile', 'email'] |
| redirectURI | string | — | Defaults to {baseURL}/itsme/callback |
| disableImplicitSignUp | boolean | — | Prevent new user creation unless client passes requestSignUp: true |
signIn.itsme() options
| Option | Type | Description |
|---|---|---|
| callbackURL | string | Where to redirect after successful sign-in |
| errorCallbackURL | string | Where to redirect on itsme® error responses |
| requestSignUp | boolean | Allow new user creation when disableImplicitSignUp is enabled on the server |
Database columns
The plugin adds two optional columns to the user table:
| Column | Type | Description |
|---|---|---|
| itsme_national_number | string | Belgian national register number |
| itsme_eid_serial_number | string | eID card serial number |
Run BetterAuth's migration after adding the plugin:
npx auth@latest migrateEndpoints
| Path | Method | Description |
|---|---|---|
| /sign-in/itsme | POST | Initiates the itsme® authorization flow |
| /itsme/callback | GET | Handles the authorization code callback |
User profile
After sign-in, the mapped user profile is available via BetterAuth's session. The ItsmeUserProfile type is exported from both better-auth-itsme and better-auth-itsme/client.
Standard OIDC fields (depending on requested scopes):
sub, name, givenName, familyName, email, emailVerified, phoneNumber, phoneNumberVerified, birthdate, gender, locale, picture, address
itsme®-specific fields (require the eid scope for most):
| Field | Description |
|---|---|
| nationalNumber | Belgian national register number |
| eidSerialNumber | eID card serial number |
| idDocumentNumber | Identity document number |
| idDocumentType | Document type |
| idIssuingCountry | Country that issued the document |
| citizenship | Citizenship |
| citizenshipIso | Citizenship as ISO code |
| placeOfBirth | Place of birth |
| birthdateAsString | Birthdate as a formatted string |
| photo | Portrait photo ({ format, value }) |
The raw itsme® claim-to-field mapping is also exported as ITSME_CLAIM_MAP if you need it.
TLS certificate requirement
itsme® requires an Organisation Validation (OV) or Extended Validation (EV) TLS certificate on your callback URL. Standard domain-validated certificates (e.g. Let's Encrypt) are not accepted. Plan accordingly for both staging and production environments.
License
MIT
