Sign in with Dazr Identity: developer docs

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.

WhatAddress
Issuerhttps://identity.dazr.eu
Discoveryhttps://identity.dazr.eu/.well-known/openid-configuration
Authorizationhttps://identity.dazr.eu/oauth/authorize
Tokenhttps://identity.dazr.eu/oauth/token
User infohttps://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 onlyhttps://identity.dazr.eu/oauth/introspect
Sign-out (RP-initiated logout)https://identity.dazr.eu/oauth/logout

Register an app

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

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.

SettingValue
Client IDyour client ID
Client secretyour client secret
Scopeopenid profile email
Login (authorization) endpointhttps://identity.dazr.eu/oauth/authorize
Token endpointhttps://identity.dazr.eu/oauth/token
User info endpointhttps://identity.dazr.eu/oauth/userinfo
End session endpointhttps://identity.dazr.eu/oauth/logout
Identity keysub
PKCEon (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:

The flow

  1. Create a random code_verifier (43 to 128 characters) and its code_challenge = base64url(SHA-256(verifier)). Create a random state and nonce. Keep all three in the user's session.
  2. Redirect to the authorization endpoint with response_type=code, client_id, redirect_uri, scope (always including openid), state, nonce, code_challenge and code_challenge_method=S256.
  3. 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, state and iss. Check that state matches and iss is https://identity.dazr.eu.
  4. On your server, POST to the token endpoint with grant_type=authorization_code, code, the same redirect_uri and the code_verifier.
  5. Verify the ID token: ES256 signature with a key from the JWKS (match the kid), iss, aud = your client ID, exp and your nonce. Use sub as 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.

ScopeClaims
openid (required)sub: a random ID for the person, the same for every app of your organisation and different for every other organisation
profilename (if set) and locale
emailemail and email_verified (always true)
addressaddress (formatted, street_address, locality, region, postal_code, country), only if the person saved one
organisationsorganisations: 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_accessno 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.statusMeaning
unverifiedNot verified, or a verification failed, expired or was revoked (for example after the legal name or company number changed).
pendingVerification is in progress: the representative has been asked to sign, or Dazr is reviewing documents.
verifiedVerified. 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

TokenLifetime and rules
Authorization code60 seconds, single use, bound to your client, redirect URI and PKCE challenge. Using a code twice revokes the tokens it issued.
Access token10 minutes. A signed JWT (ES256, typ at+jwt) with aud = your client ID. Send it as Authorization: Bearer.
ID token10 minutes. ES256, with iss, sub, aud, exp, iat, auth_time, nonce and amr where known.
Refresh tokenOnly 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.