Get started with Sign in with NobleID

Three steps to a working sign-in on your own computer. When it works, register your live redirect URI.

Before you start

  • A NobleID account with two-step login (a code from an authenticator app). After you turn it on, sign out and sign in again with the code.
  • The account you test with needs a first and last name on its NobleID profile. NobleID asks for it if it is missing.

Running OJS, WordPress, DSpace, Dataverse, Moodle, Keycloak, JupyterHub or Discourse? Use the platform guide instead.

Register an app

  1. Open Dashboard → OAuth apps and click New app.
  2. Choose Confidential if your code runs on a server, or Public for an app that runs only in the browser.
  3. Add a redirect URI for testing. http is allowed on localhost:
    Next.js (confidential)
    http://localhost:3000/auth/nobleid/callback
    PHP (confidential)
    http://localhost:8000/nobleid.php
    Browser only (public)
    http://localhost:8080/callback.html
  4. Keep the scopes ticked by default (OpenID, Profile). The code below asks for exactly these. Tick more only when your code asks for them.
  5. Save, then copy the client ID and, for a confidential app, the client secret. The secret is shown once.
.env.local
# .env.local  (server-only: no NEXT_PUBLIC_ prefix)
NOBLEID_CLIENT_ID=nbcl_your_client_id
NOBLEID_CLIENT_SECRET=nbcs_your_client_secret
# While you test. In production, register and use your https URL.
NOBLEID_REDIRECT_URI=http://localhost:3000/auth/nobleid/callback

# npm install jose

Add the button

The button links to a route on your server. The route creates state, nonce and a PKCE verifier, keeps them in a short-lived cookie, and sends the person to NobleID.

Your sign-in page (HTML or JSX)
<a href="/auth/nobleid/start" style="display:inline-block;line-height:0">
  <img src="https://www.nobleid.org/brand/sign-in-with-nobleid-light.svg"
       alt="Sign in with NobleID" height="40">
</a>
app/auth/nobleid/start/route.ts
// app/auth/nobleid/start/route.ts
import { NextResponse } from "next/server"
import crypto from "node:crypto"

const b64url = (buf: Buffer) => buf.toString("base64url")

export async function GET() {
  const state = b64url(crypto.randomBytes(32))
  const nonce = b64url(crypto.randomBytes(32))
  const verifier = b64url(crypto.randomBytes(32))
  const challenge = b64url(crypto.createHash("sha256").update(verifier).digest())

  const url = new URL("https://www.nobleid.org/oauth/authorize")
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: process.env.NOBLEID_CLIENT_ID!,
    redirect_uri: process.env.NOBLEID_REDIRECT_URI!,
    scope: "openid profile",
    state,
    nonce,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString()

  const res = NextResponse.redirect(url)
  // Short-lived, httpOnly, scoped to the callback path. SameSite=Lax is sent
  // on the top-level redirect back from NobleID.
  res.cookies.set("nobleid_oidc", JSON.stringify({ state, nonce, verifier }), {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production", // http://localhost while you test
    sameSite: "lax",
    path: "/auth/nobleid",
    maxAge: 600,
  })
  return res
}
Sign in with NobleIDEvery button style, in SVG and PNG. How to use them. Download button artwork

Handle the callback

NobleID sends the person back to your redirect URI with a code. Check state and iss, exchange the code, check the ID token, then key the account on sub.

app/auth/nobleid/callback/route.ts
// app/auth/nobleid/callback/route.ts
import { NextRequest, NextResponse } from "next/server"
import { createRemoteJWKSet, jwtVerify } from "jose"

const ISSUER = "https://id.nobleid.org"
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`))

function fail(msg: string) {
  const res = new NextResponse(`Sign-in failed: ${msg}`, { status: 400 })
  res.cookies.delete({ name: "nobleid_oidc", path: "/auth/nobleid" })
  return res
}

export async function GET(req: NextRequest) {
  const raw = req.cookies.get("nobleid_oidc")?.value
  if (!raw) return fail("session expired, try again")
  const { state, nonce, verifier } = JSON.parse(raw)

  // Check state, then iss, before believing anything else in the URL,
  // an error included: a forged callback can carry any error it likes.
  const q = req.nextUrl.searchParams
  if (q.get("state") !== state) return fail("state mismatch")
  if (q.get("iss") !== ISSUER) return fail("issuer mismatch") // RFC 9207
  if (q.get("error")) return fail(q.get("error_description") ?? q.get("error")!)

  // Token exchange: form-encoded, client_secret_basic.
  const id = encodeURIComponent(process.env.NOBLEID_CLIENT_ID!)
  const secret = encodeURIComponent(process.env.NOBLEID_CLIENT_SECRET!)
  const tokenRes = await fetch(`${ISSUER}/oauth/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}`,
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: q.get("code") ?? "",
      redirect_uri: process.env.NOBLEID_REDIRECT_URI!,
      code_verifier: verifier,
    }),
    cache: "no-store",
  })
  if (!tokenRes.ok) return fail("token exchange failed")
  const tokens = await tokenRes.json()

  // Verify the ID token: ES256 signature against NobleID's JWKS, iss, aud, exp.
  const { payload } = await jwtVerify(tokens.id_token, JWKS, {
    issuer: ISSUER,
    audience: process.env.NOBLEID_CLIENT_ID!,
    algorithms: ["ES256"],
  })
  if (payload.nonce !== nonce) return fail("nonce mismatch")

  // Optional: claims that are only in userinfo (affiliations, research_interests…)
  const userinfo = await fetch(`${ISSUER}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${tokens.access_token}` },
    cache: "no-store",
  }).then((r) => r.json())
  if (userinfo.sub !== payload.sub) return fail("userinfo subject mismatch")

  // Key the account on payload.sub (stable). Never link accounts by email
  // unless email_verified is true.
  // const user = await upsertUser({ nobleidSub: payload.sub, nobleid: payload.nobleid, name: userinfo.name })
  // await createYourOwnSession(user)

  const res = NextResponse.redirect(new URL("/", req.url))
  res.cookies.delete({ name: "nobleid_oidc", path: "/auth/nobleid" })
  return res
}

Run npm run dev, open http://localhost:3000 and click the button.

You will see NobleID's consent screen, then your own page with the person's sub, nobleid and name. To see every claim first, use the test client.

If it fails

What you seeWhat to do
“redirect_uri is not registered for this application”Make the redirect URI in your code and on the app identical: scheme, host, port, path and query.
invalid_scope: “Scope not available to this client”Tick that scope on your app, or stop asking for it.
“Unknown or disabled application”Check the client ID. Copy it again from Dashboard → OAuth apps.
invalid_client at the token endpointCheck the client secret. Send the request form-encoded, not as JSON.
invalid_grant at the token endpointExchange each code once, straight away, with the same redirect URI and the PKCE verifier.
The ID token fails to verifyAllow ES256. Many libraries expect RS256 unless told otherwise.
NobleID asks for your nameAdd your first and last name, then choose Continue.

Using a library? Check its settings for NobleID. To check that NobleID answers:

Terminal
curl -s https://id.nobleid.org/.well-known/openid-configuration

Next

  • Add your live https redirect URI to the app, and remove the localhost one before launch.
  • Ask for more only when you need it: email, ORCID iD, affiliations or works. See scopes and claims.
  • To fill your app's own profile with the person's name, photo, ORCID iD or institution, see Fill your app's profile from NobleID.
  • To get the verified badge on the consent screen, email info@nobleid.org with your client ID.

Platform guides: OJS · WordPress · DSpace · Dataverse · Moodle · Keycloak · JupyterHub · Discourse

    ByNobleID