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.

FieldWhat to enter
Application typeWeb / SPA (public client, PKCE) or Backend service (confidential, private_key_jwt)
Client IDA unique identifier for the app, e.g. acme-web. Globally unique; used as the aud of issued ID tokens.
Display nameOptional human-readable label shown in the console.
Client authenticationPKCE (default) or private_key_jwt with a registered Ed25519 public key
Redirect URIsYour callback URLs. Exact-match, https only (except http on localhost/loopback).
Post-logout redirect URIsWhere Hawcx may return the browser after logout (see Single Logout).
Back-channel logout URIOptional. Endpoint Hawcx POSTs a logout_token to when the user signs out (see Single Logout).
Require sid in logout tokenToggle (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/callback
curl -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/callback

The 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_method must be S256; the code_verifier is checked at the token endpoint for both public and confidential clients.
  • Validate state. Store it with the request and reject any callback whose state doesn't match — this binds the callback to the browser that started the flow (CSRF defense).
  • Validate nonce. Bind a nonce into 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 accept none or let the token header choose the algorithm (alg-confusion defense).
  • Verify iss, aud, and expiry. iss must equal the issuer, aud your client_id, and exp/nbf/iat must be valid within a small clock-skew tolerance.
  • Exact-match redirect URIs. Only pre-registered, exact-match https redirect URIs are honored (loopback http excepted).
  • Codes are single-use and short-lived; redeem immediately and never log them.
  • Dedupe back-channel logout tokens on jti and reject any that carry a nonce. See Single Logout.

Next steps