Integrate an Application
Register an OIDC client and run the Authorization Code + PKCE flow to sign users in with Hawcx.
This guide integrates one application as an OIDC relying party (RP). The flow is the standard
OAuth 2.0 Authorization Code grant with PKCE (RFC 7636),
and every endpoint — including authorization_endpoint — is resolvable from the discovery document,
so stock OIDC libraries can be configured from the issuer alone.
Prerequisites
A Hawcx project (see Create a project). Your project's
config_id is shown under Settings → General; you'll create the OIDC client_id below.
Register an OIDC client
In the Admin Console, open your project's Settings → OAuth tab and choose Add client.
| Field | What to enter |
|---|---|
| Application type | Web / SPA (public client, PKCE) or Backend service (confidential, private_key_jwt) |
| Client ID | A unique identifier for the app, e.g. acme-web. Globally unique; used as the aud of issued ID tokens. |
| Display name | Optional human-readable label shown in the console. |
| Client authentication | PKCE (default) or private_key_jwt with a registered Ed25519 public key |
| Redirect URIs | Your callback URLs. Exact-match, https only (except http on localhost/loopback). |
| Post-logout redirect URIs | Where Hawcx may return the browser after logout (see Single Logout). |
| Back-channel logout URI | Optional. Endpoint Hawcx POSTs a logout_token to when the user signs out (see Single Logout). |
Require sid in logout token | Toggle (default on). Keep on so logout tokens include a sid and you can honor session-scoped logout; see below. |
Adding more clients to the same project puts them in one identity domain, so users sign in once across all of them.
private_key_jwt keys
For a backend service, generate the Ed25519 keypair in the console (the private key is downloaded in your browser and never sent to Hawcx) or paste your own public JWK. Only the public key is registered.
Start sign-in (authorization request)
Generate a PKCE code_verifier/code_challenge and a state (an opaque random value that binds
the callback to this request and defends against CSRF), store them in the user's session, and
redirect the browser to the authorization endpoint:
https://api.hawcx.com/authorize
?response_type=code
&client_id=acme-web
&redirect_uri=https://app.acme.com/callback
&scope=openid%20email
&state=<opaque>
&code_challenge=<S256(code_verifier)>
&code_challenge_method=S256
&nonce=<opaque>scope must include openid. code_challenge_method must be S256. Hawcx renders its hosted
passwordless sign-in, then redirects the browser back to your redirect_uri with code and
state.
Silent SSO
If the user already has a live session in this project (they signed in to a sibling app), the
same request returns a code immediately with no sign-in screen. To require a fresh login add
prompt=login; to only check for an existing session without ever showing UI add prompt=none
(you'll get error=login_required back if there's no session).
Exchange the code for an ID token
On your redirect_uri, verify state matches, then exchange the code at the token endpoint.
PKCE (code_verifier) is required for both client types.
curl -X POST https://api.hawcx.com/oauth2/token \
-d grant_type=authorization_code \
-d code=<code> \
-d code_verifier=<code_verifier> \
-d client_id=acme-web \
-d redirect_uri=https://app.acme.com/callbackcurl -X POST https://api.hawcx.com/oauth2/token \
-d grant_type=authorization_code \
-d code=<code> \
-d code_verifier=<code_verifier> \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
-d client_assertion=<signed_ed25519_jwt> \
-d redirect_uri=https://app.acme.com/callbackThe client assertion is an EdDSA-signed JWT with iss and sub set to your client_id and
aud set to the token endpoint URL (https://api.hawcx.com/oauth2/token). For a full worked
example, see private_key_jwt authentication.
Response:
{ "id_token": "<JWS>", "token_type": "Bearer", "expires_in": 60 }Authorization codes are single-use. No access_token is issued — Hawcx has no UserInfo endpoint
or resource API, so every user claim is carried in the ID token itself.
Verify the ID token
Verify the JWS signature against the JWKS, pinning the algorithm to ES512 (the value discovery advertises in id_token_signing_alg_values_supported) — always pass an
explicit algorithm allowlist and never accept none or an algorithm the JWKS doesn't use, which
is what defeats alg-confusion attacks. Then validate the claims: iss equals the issuer, aud
equals your client_id, exp/nbf/iat are valid within a small clock-skew tolerance, and
nonce matches the value you sent. The sub claim is the stable user identifier for your app.
For language-specific verification (Python, Node, Java) and the complete claim set, see
ID Token Verification. The sid claim correlates a
login with Back-Channel Logout.
End-to-end example (Node.js)
A minimal relying party using express and jose for JWKS
verification. The authorization, token, and JWKS endpoints all come from the discovery document.
import express from "express";
import crypto from "node:crypto";
import { createRemoteJWKSet, jwtVerify } from "jose";
const ISSUER = "https://api.hawcx.com";
const CLIENT_ID = "acme-web";
const REDIRECT_URI = "https://app.acme.com/callback";
// Discovery resolves authorization_endpoint, token_endpoint, and jwks_uri.
const disco = await (await fetch(`${ISSUER}/.well-known/openid-configuration`)).json();
const jwks = createRemoteJWKSet(new URL(disco.jwks_uri));
const b64url = (b) => b.toString("base64url");
const app = express();
const store = new Map(); // demo only; use the user session in production
app.get("/login", (req, res) => {
const verifier = b64url(crypto.randomBytes(32));
const challenge = b64url(crypto.createHash("sha256").update(verifier).digest());
const state = b64url(crypto.randomBytes(16));
const nonce = b64url(crypto.randomBytes(16));
store.set(state, { verifier, nonce });
const url = new URL(disco.authorization_endpoint);
url.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "openid email",
state,
nonce,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString();
res.redirect(url.toString());
});
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
const saved = store.get(state);
if (!saved) return res.status(400).send("Invalid state");
store.delete(state);
const tokenRes = await fetch(disco.token_endpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
code_verifier: saved.verifier,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
}),
});
if (!tokenRes.ok) return res.status(401).send("Token exchange failed");
const { id_token } = await tokenRes.json();
const { payload } = await jwtVerify(id_token, jwks, {
issuer: ISSUER,
audience: CLIENT_ID,
algorithms: ["ES512"], // must match discovery's id_token_signing_alg_values_supported
});
if (payload.nonce !== saved.nonce) return res.status(401).send("Nonce mismatch");
// payload.sub is the user; payload.sid ties this login to Back-Channel Logout.
res.json({ signedInAs: payload.sub, email: payload.email });
});
app.listen(3000);Security considerations
- PKCE is required.
code_challenge_methodmust beS256; thecode_verifieris checked at the token endpoint for both public and confidential clients. - Validate
state. Store it with the request and reject any callback whosestatedoesn't match — this binds the callback to the browser that started the flow (CSRF defense). - Validate
nonce. Bind anonceinto the authorization request and confirm the ID token echoes it, to defend against token replay. - Pin the signing algorithm. Verify with an explicit
algorithms: ["ES512"]allowlist; never acceptnoneor let the token header choose the algorithm (alg-confusion defense). - Verify
iss,aud, and expiry.issmust equal the issuer,audyourclient_id, andexp/nbf/iatmust be valid within a small clock-skew tolerance. - Exact-match redirect URIs. Only pre-registered, exact-match
httpsredirect URIs are honored (loopbackhttpexcepted). - Codes are single-use and short-lived; redeem immediately and never log them.
- Dedupe back-channel logout tokens on
jtiand reject any that carry anonce. See Single Logout.
Next steps
- Single Logout: sign the user out of every app in the domain.
- Reference: endpoints, parameters, and claims.