Sign in with Dazr Identity: developer docs
Standard OpenID Connect 1.0 with the authorization code flow and PKCE. No SDK needed: any certified OpenID Connect library works, and so does plain code.
People can already use their Google or Microsoft account inside Dazr Identity, so your app does not need separate Google and Microsoft buttons next to it: one button instead of three.
Discovery and endpoints
Everything is described in the discovery document. Point your library at the issuer and it finds the rest.
| What | Address |
|---|---|
| Issuer | https://identity.dazr.eu |
| Discovery | https://identity.dazr.eu/.well-known/openid-configuration |
| Authorization | https://identity.dazr.eu/oauth/authorize |
| Token | https://identity.dazr.eu/oauth/token |
| User info | https://identity.dazr.eu/oauth/userinfo |
| Public keys (JWKS) | https://identity.dazr.eu/oauth/jwks |
| Revocation (RFC 7009) | https://identity.dazr.eu/oauth/revoke |
| Introspection (RFC 7662), web server apps only | https://identity.dazr.eu/oauth/introspect |
| Sign-out (RP-initiated logout) | https://identity.dazr.eu/oauth/logout |
Register an app
- Apps belong to an organisation. Owners and admins of the organisation register them in Developers.
- Web server apps (confidential clients) get a client secret, shown once. Authenticate to the token endpoint with HTTP Basic (
client_secret_basic) or in the form body (client_secret_post). - Single-page and mobile apps (public clients) get no secret. They send
client_idand rely on PKCE. - Redirect URIs match exactly. They must be
https://. While the app is in testing,http://localhostandhttp://127.0.0.1on any port also work. - A privacy policy address, a support email and a one-line purpose are required: people see them on the consent screen.
Integration guides
Every stack below uses its own standard OpenID Connect support. You need three values from the console: the client ID, the client secret (web server apps only) and the redirect URI you registered. The rest comes from the discovery document.
- Auth.js / NextAuth
- Next.js App Router
- WordPress
- Laravel
- Supabase
- Django
- Node.js (openid-client)
- Any OpenID Connect library
sub claim is pairwise: the same for every app of your organisation, different for every other organisation. It never changes for a person, while an email address can. Store sub as the account key and treat email as contact data.
Auth.js / NextAuth
Auth.js (NextAuth.js v5) takes a custom OpenID Connect provider object. Register a web server app, set AUTH_SECRET, AUTH_DAZR_ID and AUTH_DAZR_SECRET, and use this callback URL as the redirect URI: https://app.example.eu/api/auth/callback/dazr.
// auth.ts
import NextAuth from "next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
{
id: "dazr",
name: "Dazr Identity",
type: "oidc",
issuer: "https://identity.dazr.eu",
clientId: process.env.AUTH_DAZR_ID,
clientSecret: process.env.AUTH_DAZR_SECRET,
authorization: { params: { scope: "openid profile email" } },
checks: ["pkce", "state", "nonce"],
client: { id_token_signed_response_alg: "ES256" },
},
],
})
NextAuth.js v4 uses the same idea with type: "oauth" and the discovery URL:
// pages/api/auth/[...nextauth].ts (NextAuth.js v4), in providers: [ ... ]
{
id: "dazr",
name: "Dazr Identity",
type: "oauth",
wellKnown: "https://identity.dazr.eu/.well-known/openid-configuration",
clientId: process.env.DAZR_CLIENT_ID,
clientSecret: process.env.DAZR_CLIENT_SECRET,
authorization: { params: { scope: "openid profile email" } },
idToken: true,
checks: ["pkce", "state"],
client: { id_token_signed_response_alg: "ES256" },
profile(profile) {
return { id: profile.sub, name: profile.name ?? null, email: profile.email }
},
}
Next.js App Router
With the auth.ts file above, add the route handler and a sign-in button that runs a server action.
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
// app/page.tsx
import { auth, signIn, signOut } from "@/auth"
export default async function Page() {
const session = await auth()
if (session?.user) {
return (
<form action={async () => { "use server"; await signOut() }}>
<p>Signed in as {session.user.email}</p>
<button type="submit">Sign out</button>
</form>
)
}
return (
<form action={async () => { "use server"; await signIn("dazr") }}>
<button type="submit" className="dazr-signin">Sign in with Dazr Identity</button>
</form>
)
}
Style the button as shown under The button. In callbacks, the Dazr sub is account.providerAccountId.
WordPress
Use a generic OpenID Connect client plugin, such as OpenID Connect Generic Client. Register a web server app and fill in the plugin settings with the values below. The plugin shows the redirect URI to register on its settings page; by default it is https://example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize.
| Setting | Value |
|---|---|
| Client ID | your client ID |
| Client secret | your client secret |
| Scope | openid profile email |
| Login (authorization) endpoint | https://identity.dazr.eu/oauth/authorize |
| Token endpoint | https://identity.dazr.eu/oauth/token |
| User info endpoint | https://identity.dazr.eu/oauth/userinfo |
| End session endpoint | https://identity.dazr.eu/oauth/logout |
| Identity key | sub |
| PKCE | on (S256) |
Setting names differ a little between plugins. If a plugin has no PKCE option, choose another one: Dazr Identity rejects sign-ins without PKCE.
Laravel
Laravel Socialite has no generic OpenID Connect driver built in, so add a small custom driver. It sends people to Dazr with PKCE and reads the user from the user info endpoint. Socialite does not read the discovery document, so the endpoints are written out. Register a web server app with the redirect URI https://app.example.eu/auth/dazr/callback.
<?php
// app/Socialite/DazrProvider.php
namespace App\Socialite;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\User;
class DazrProvider extends AbstractProvider
{
protected $scopes = ['openid', 'profile', 'email'];
protected $scopeSeparator = ' ';
protected $usesPKCE = true;
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase('https://identity.dazr.eu/oauth/authorize', $state);
}
protected function getTokenUrl()
{
return 'https://identity.dazr.eu/oauth/token';
}
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get('https://identity.dazr.eu/oauth/userinfo', [
'headers' => ['Authorization' => 'Bearer '.$token],
]);
return json_decode((string) $response->getBody(), true);
}
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['sub'],
'name' => $user['name'] ?? null,
'email' => $user['email'] ?? null,
]);
}
}
// config/services.php
'dazr' => [
'client_id' => env('DAZR_CLIENT_ID'),
'client_secret' => env('DAZR_CLIENT_SECRET'),
'redirect' => env('DAZR_REDIRECT_URI'),
],
// app/Providers/AppServiceProvider.php, in boot()
use Laravel\Socialite\Facades\Socialite;
use App\Socialite\DazrProvider;
Socialite::extend('dazr', function ($app) {
return Socialite::buildProvider(DazrProvider::class, config('services.dazr'));
});
// routes/web.php
Route::get('/auth/dazr', fn () => Socialite::driver('dazr')->redirect());
Route::get('/auth/dazr/callback', function () {
$dazr = Socialite::driver('dazr')->user();
$user = \App\Models\User::updateOrCreate(
['dazr_sub' => $dazr->getId()],
['name' => $dazr->getName() ?? $dazr->getEmail(), 'email' => $dazr->getEmail()],
);
Auth::login($user);
return redirect('/');
});
Supabase
Supabase Auth has no generic OpenID Connect provider that you can point at any issuer: its sign-in providers and third-party auth integrations are fixed lists. So you cannot add Dazr Identity in the Supabase dashboard today. If Supabase adds generic OpenID Connect support, use the values from the checklist below.
The alternative: sign people in with Dazr Identity in your own server (with one of the guides on this page), store the Dazr sub in your users table, and talk to Supabase from that server with the service role key, checking access in your own code. Never send the service role key to a browser.
Django
Use mozilla-django-oidc. Register a web server app with the redirect URI https://app.example.eu/oidc/callback/. The library does not read the discovery document, so the endpoints are written out.
# settings.py
import os
INSTALLED_APPS += ["mozilla_django_oidc"] # after django.contrib.auth
AUTHENTICATION_BACKENDS = [
"mozilla_django_oidc.auth.OIDCAuthenticationBackend",
"django.contrib.auth.backends.ModelBackend",
]
OIDC_RP_CLIENT_ID = os.environ["DAZR_CLIENT_ID"]
OIDC_RP_CLIENT_SECRET = os.environ["DAZR_CLIENT_SECRET"]
OIDC_RP_SCOPES = "openid profile email"
OIDC_RP_SIGN_ALGO = "ES256"
OIDC_OP_JWKS_ENDPOINT = "https://identity.dazr.eu/oauth/jwks"
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://identity.dazr.eu/oauth/authorize"
OIDC_OP_TOKEN_ENDPOINT = "https://identity.dazr.eu/oauth/token"
OIDC_OP_USER_ENDPOINT = "https://identity.dazr.eu/oauth/userinfo"
OIDC_USE_PKCE = True
OIDC_PKCE_CODE_CHALLENGE_METHOD = "S256"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
# urls.py
from django.urls import include, path
urlpatterns += [path("oidc/", include("mozilla_django_oidc.urls"))]
# template: start the sign-in
# <a href="{% url 'oidc_authentication_init' %}">Sign in with Dazr Identity</a>
By default the backend matches users by email. To key accounts on sub, subclass OIDCAuthenticationBackend and override filter_users_by_claims and create_user. Use a current release: older releases cannot verify ES256 signatures.
Node.js (openid-client)
openid-client (version 6) reads the discovery document and checks PKCE, state, nonce and the ID token for you.
import * as client from 'openid-client';
const config = await client.discovery(
new URL('https://identity.dazr.eu'),
process.env.DAZR_CLIENT_ID,
process.env.DAZR_CLIENT_SECRET, // public client: pass undefined here and client.None() as the 4th argument
);
const redirect_uri = 'https://app.example.eu/auth/dazr/callback';
// 1. Start: keep the three random values in the user's session.
export async function start(session) {
session.verifier = client.randomPKCECodeVerifier();
session.state = client.randomState();
session.nonce = client.randomNonce();
return client.buildAuthorizationUrl(config, {
redirect_uri,
scope: 'openid profile email',
code_challenge: await client.calculatePKCECodeChallenge(session.verifier),
code_challenge_method: 'S256',
state: session.state,
nonce: session.nonce,
});
}
// 2. Callback: pass the full callback URL (a URL object).
export async function callback(session, currentUrl) {
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: session.verifier,
expectedState: session.state,
expectedNonce: session.nonce,
});
const claims = tokens.claims(); // sub, email, name
return { sub: claims.sub, email: claims.email, name: claims.name };
}
Any OpenID Connect library
If your stack is not listed, any OpenID Connect client works. Check these settings:
- Issuer
https://identity.dazr.eu, discovery URLhttps://identity.dazr.eu/.well-known/openid-configuration. - Response type
code(authorization code flow). No implicit or hybrid flow. - PKCE is required, with
code_challenge_method=S256. - Scopes:
openidplus only what you need fromprofile,email,address,organisationsandoffline_access. Your app must be registered for each scope it asks for. - Redirect URI: exactly as registered in the console, including the path and any query string.
- Client authentication:
client_secret_basicorclient_secret_postfor web server apps,nonefor single-page and mobile apps. - ID token signature: ES256. Some libraries assume RS256: set the algorithm to ES256.
- Use
subas the account key. It is pairwise, per organisation. - With
offline_access, store the new refresh token after every refresh: they rotate.
The flow
- Create a random
code_verifier(43 to 128 characters) and itscode_challenge= base64url(SHA-256(verifier)). Create a randomstateandnonce. Keep all three in the user's session. - Redirect to the authorization endpoint with
response_type=code,client_id,redirect_uri,scope(always includingopenid),state,nonce,code_challengeandcode_challenge_method=S256. - The person signs in if needed and sees the consent screen with the actual data. When they allow it, they come back to your redirect URI with
code,stateandiss. Check thatstatematches andissishttps://identity.dazr.eu. - On your server, POST to the token endpoint with
grant_type=authorization_code,code, the sameredirect_uriand thecode_verifier. - Verify the ID token: ES256 signature with a key from the JWKS (match the
kid),iss,aud= your client ID,expand yournonce. Usesubas the account key.
The prompt parameter accepts none (answer at once with login_required or consent_required if the person would have to act), login (ask the person to sign in again) and consent (show the consent screen even when it was approved before). max_age is supported. Remembered consent: if the person approved the same scopes before, they go straight back to your app. If you ask for more later, the consent screen lists only what is new.
Scopes and claims
Ask only for what you need. Nothing outside this table exists: no contacts, no files, no passwords.
| Scope | Claims |
|---|---|
openid (required) | sub: a random ID for the person, the same for every app of your organisation and different for every other organisation |
profile | name (if set) and locale |
email | email and email_verified (always true) |
address | address (formatted, street_address, locality, region, postal_code, country), only if the person saved one |
organisations | organisations: the organisations the person ticks on the consent screen, each with id, name, country, register_number, vat, vat_verified (true only when the EU VIES service confirmed the VAT number), verified, verification (see Verified organisations) and role |
offline_access | no claims; you also receive a refresh token |
Claims appear in the ID token for the scopes the person approved. The user info endpoint returns the same set, with current values.
Verified organisations
Dazr verifies an organisation when its director, or another person who can sign for it, signs a short declaration with a qualified electronic signature (QES). It is instant and works in every EU country. Without a qualified signature, the representative can send a company register extract and an ID instead; Dazr reviews those by hand. Every organisation in the organisations claim carries its current verification:
| verification.status | Meaning |
|---|---|
unverified | Not verified, or a verification failed, expired or was revoked (for example after the legal name or company number changed). |
pending | Verification is in progress: the representative has been asked to sign, or Dazr is reviewing documents. |
verified | Verified. method is qes, qes_org (the certificate itself names the organisation, by VAT or register number), documents or extract, and verified_at is a Unix time. |
The boolean verified stays for compatibility and equals verification.status === 'verified'. Each organisation also has an id, the same one webhooks use.
"organisations": [{
"id": "org_3k9x2m0q8w1v5t7a", "name": "Conti Logistica S.r.l.", "country": "IT",
"register_number": "REA MI1234567", "vat": "IT12345678903", "vat_verified": true,
"verified": false, "verification": { "status": "pending", "method": "qes" }, "role": "owner"
}]
Require a verified organisation
In the console, set Require a verified organisation to Verification started or Verified, or ask per request with acr_values=urn:dazr:org:verification-started or acr_values=urn:dazr:org:verified (the stricter of the two wins). Your app must also request the organisations scope.
Someone without a qualifying organisation is taken through it in the same sign-in: their name, the organisation (with the VAT number checked in VIES), then the verification. They can sign right away, have the declaration emailed to themselves, or name someone else who signs. They then tick the organisation on the consent screen (only qualifying organisations can be ticked) and return to your redirect URI.
If someone else still has to sign, the person comes back with status pending, also when you asked for Verified: you receive a code, the claim says pending, and the ID token's acr is urn:dazr:org:verification-started instead of urn:dazr:org:verified. Always gate on verification.status, never on the fact that sign-in succeeded. With prompt=none you get interaction_required when the requirement isn't met.
Changes to the setting apply from the next sign-in. People already signed in are not signed out: refresh responses, the user info endpoint and webhooks always carry the current status, so use those to react for existing sessions.
Webhooks
Add a webhook URL (https) in the console. You see the signing secret once; you can rotate it. Dazr sends organisation.verification.updated when an organisation that one of your users shared with your app becomes pending, verified, failed or unverified. Apps that never received an organisation through consent never hear about it.
POST /your/webhook
Content-Type: application/json
Dazr-Event: organisation.verification.updated
Dazr-Delivery: dlv_8Fq2…
Dazr-Signature: t=1791021600,v1=5e0c…
{ "id": "evt_…", "type": "organisation.verification.updated", "created": 1791021600,
"data": { "organisation": { "id": "org_3k9x2m0q8w1v5t7a", "name": "Conti Logistica S.r.l.", "country": "IT" },
"status": "verified", "method": "qes_org", "verified_at": 1791021598,
"subjects": ["Xo1c…"] } }
status is pending, verified, failed or unverified. subjects are the sub values, at your app, of the people who shared this organisation with it. Check the signature before you trust the body:
import crypto from 'node:crypto';
// raw = the request body exactly as received (a string), header = the Dazr-Signature header
function verifyDazrSignature(raw, header, secret) {
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; // reject old deliveries
const want = crypto.createHmac('sha256', secret).update(m[1] + '.' + raw).digest('hex');
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}
Answer with any 2xx within 8 seconds. Otherwise Dazr retries after about 5 minutes, 30 minutes, 2, 6, 12 and 24 hours, then marks the delivery failed. Every attempt has the same Dazr-Delivery ID, so ignore duplicates. The console lists recent deliveries, can resend one and can send a test event (webhook.test). Polling the user info endpoint works too.
Tokens and lifetimes
| Token | Lifetime and rules |
|---|---|
| Authorization code | 60 seconds, single use, bound to your client, redirect URI and PKCE challenge. Using a code twice revokes the tokens it issued. |
| Access token | 10 minutes. A signed JWT (ES256, typ at+jwt) with aud = your client ID. Send it as Authorization: Bearer. |
| ID token | 10 minutes. ES256, with iss, sub, aud, exp, iat, auth_time, nonce and amr where known. |
| Refresh token | Only with offline_access. Rotates on every use: always store the new one. Sending an old refresh token again revokes the whole sign-in. Expires after 30 days without use and 180 days at most. |
Signing keys rotate. Always pick the key by kid from the JWKS and refresh the JWKS when you see an unknown kid.
Errors
As long as your client ID and redirect URI are valid, errors come back to your redirect URI as error, error_description, state and iss: for example invalid_request (such as missing or plain PKCE), invalid_scope, unsupported_response_type, access_denied (the person cancelled, or the app is in testing and the person is not a member), login_required, consent_required and interaction_required (with prompt=none, when a required verified organisation is missing). With an unknown client or an unregistered redirect URI, Dazr shows an error page and never redirects. The token endpoint answers with JSON errors such as invalid_client, invalid_grant and unsupported_grant_type.
Sign-out
To sign someone out, send them to the sign-out endpoint with id_token_hint (or client_id), an optional post_logout_redirect_uri registered in your app settings, and state. Dazr asks whether they also want to sign out of Dazr Identity on that device, then sends them back. Revoke refresh tokens you no longer need at the revocation endpoint.
Testing and going live
A new app is in testing: only members of your organisation can sign in, and the consent screen says so. To go live, get your organisation verified in Dazr Identity, add an https:// redirect URI and request a live review in the console. Changing the name, purpose, privacy policy or data of a live app needs a new review.
The button
Use the wording "Sign in with Dazr Identity" and the Dazr logo as below. You may change the size, not the logo, colours or wording. Link it to your own route that starts the flow.
<a href="/auth/dazr" style="display:inline-flex;align-items:center;gap:10px;height:44px;padding:0 18px;border-radius:22px;background:#18203a;color:#fff;font:600 15px/1 system-ui,sans-serif;text-decoration:none">
<svg width="20" height="20" viewBox="0 0 100 100" aria-hidden="true"><path d="M50 4 89 16v31c0 25.5-16.8 42.5-39 50C27.8 89.5 11 72.5 11 47V16z" fill="#fff"/><path d="M50 15.5 79 24.5v22.8c0 19.5-12.4 32.6-29 38.7C33.4 79.9 21 66.8 21 47.3V24.5z" fill="#18203a"/><path d="M50 24 71 30.6v16.9c0 14.5-9 24.4-21 29.2-12-4.8-21-14.7-21-29.2V30.6z" fill="#fff"/></svg>
Sign in with Dazr Identity
</a>
A minimal Node.js example
Node 18 or later, no dependencies. A web server app with a client secret; for a public client, drop the Authorization header and send client_id in the body.
import crypto from 'node:crypto';
const issuer = 'https://identity.dazr.eu';
const clientId = process.env.DAZR_CLIENT_ID;
const clientSecret = process.env.DAZR_CLIENT_SECRET;
const redirectUri = 'https://app.example.eu/auth/dazr/callback';
const b64url = (buf) => Buffer.from(buf).toString('base64url');
// 1. Start: keep verifier, state and nonce in the user's session.
export function start(session) {
session.verifier = b64url(crypto.randomBytes(32));
session.state = b64url(crypto.randomBytes(16));
session.nonce = b64url(crypto.randomBytes(16));
const challenge = b64url(crypto.createHash('sha256').update(session.verifier).digest());
return issuer + '/oauth/authorize?' + new URLSearchParams({
response_type: 'code', client_id: clientId, redirect_uri: redirectUri,
scope: 'openid profile email', state: session.state, nonce: session.nonce,
code_challenge: challenge, code_challenge_method: 'S256',
});
}
// 2. Callback: check state, exchange the code, verify the ID token.
export async function callback(session, query) {
if (query.error) throw new Error('sign-in failed: ' + query.error);
if (query.state !== session.state || query.iss !== issuer) throw new Error('state or issuer mismatch');
const res = await fetch(issuer + '/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + Buffer.from(clientId + ':' + clientSecret).toString('base64'),
},
body: new URLSearchParams({ grant_type: 'authorization_code', code: query.code, redirect_uri: redirectUri, code_verifier: session.verifier }),
});
const tokens = await res.json();
if (!res.ok) throw new Error(tokens.error);
const claims = await verifyIdToken(tokens.id_token, session.nonce);
return { sub: claims.sub, email: claims.email, name: claims.name, tokens };
}
async function verifyIdToken(idToken, nonce) {
const [h, p, s] = idToken.split('.');
const header = JSON.parse(Buffer.from(h, 'base64url'));
const { keys } = await (await fetch(issuer + '/oauth/jwks')).json();
const jwk = keys.find((k) => k.kid === header.kid);
if (!jwk || header.alg !== 'ES256') throw new Error('unknown signing key');
const ok = crypto.verify('sha256', Buffer.from(h + '.' + p),
{ key: crypto.createPublicKey({ key: jwk, format: 'jwk' }), dsaEncoding: 'ieee-p1363' }, Buffer.from(s, 'base64url'));
const c = JSON.parse(Buffer.from(p, 'base64url'));
const now = Math.floor(Date.now() / 1000);
if (!ok || c.iss !== issuer || c.aud !== clientId || c.exp < now - 30 || c.nonce !== nonce) throw new Error('invalid ID token');
return c;
}
Limits
The authorization and token endpoints are rate limited. If you get HTTP 429, wait and try again; do not retry in a tight loop.
Your obligations
Your organisation is an independent controller for the data it receives. The Dazr Identity Developer Terms set out what you may do with it, how to keep it secure and how to report breaches. Questions: hello@dazr.eu.