Sign in with NobleID
Let researchers sign in to your journal, repository, conference system or app with their NobleID — and, with their permission, receive their verified profile, ORCID iD, affiliations and registered works.
What you get
A standard OpenID Connect provider. Any OIDC library works; each piece of data is a separate scope the person can see and untick on the consent screen.
Sign-in
A stable account identifier (sub) and the person’s NobleID, with no password for you to store.
Profile
Name, username, photo, website and research interests.
Verification flags
Whether the person passed NobleID’s identity check, and whether NobleID staff have marked them a verified researcher.
ORCID iD
The iD on their NobleID profile, flagged verified only when it was connected through ORCID’s own sign-in.
Affiliations
Organisation, role and start year as entered by the person.
Works
The works they registered on NobleID, each with its ARK, DOI where there is one, and version.
Everything is read-only. No scope lets an app change a NobleID account or register works on someone’s behalf. (Partner minting, works:write, is planned for a partner programme and is not available.)
Quick start
- Register your app in Dashboard → OAuth apps. Choose confidential if you have a server, public for a browser-only or native app. Add your exact redirect URI(s). Copy the client secret when it is shown — it is shown once. You need two-step login on your NobleID to register an app, edit it (including redirect URIs) or rotate its secret, and it can’t be turned off while you have an active app.
- Send people to the authorize endpoint with
response_type=code, yourclient_idandredirect_uri, the scopes you need, a randomstateandnonce, and a PKCEcode_challengewithcode_challenge_method=S256. - On your redirect URI, check
stateandiss, exchange the code at the token endpoint (form-encoded, with thecode_verifier), verify the ID token, and key the account onsub.
Codes are valid for 120 seconds, access tokens for 15 minutes. Ask for offline_access only if you need to call NobleID when the person is not present; refresh tokens last 30 days and are replaced on every use. The consent screen does not pre-tick offline_access or email (unless the person's email is public); it pre-ticks only what is already public on their profile.
Code
Copy-paste starting points. Each one uses PKCE, state and nonce and checks the issuer — do not remove those parts.
App Router, confidential client, two route handlers, jose for ID-token verification. Link your sign-in button to /auth/nobleid/start.
# .env.local (server-only — no NEXT_PUBLIC_ prefix)
NOBLEID_CLIENT_ID=nbl_...
NOBLEID_CLIENT_SECRET=...
NOBLEID_REDIRECT_URI=https://app.example.org/auth/nobleid/callback
# npm install jose// 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 works",
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: true,
sameSite: "lax",
path: "/auth/nobleid",
maxAge: 600,
})
return res
}// 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)
const q = req.nextUrl.searchParams
if (q.get("error")) return fail(q.get("error_description") ?? q.get("error")!)
if (q.get("state") !== state) return fail("state mismatch")
if (q.get("iss") !== ISSUER) return fail("issuer mismatch") // RFC 9207
// 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
}Scopes & claims
openid is required. Ask only for what you use — people see every scope and can untick the optional ones, so read the granted scope in the token response rather than assuming.
| Scope | Claims |
|---|---|
openidrequiredYour NobleID and a stable account identifier |
|
profileYour name, username, photo, website, research interests and verification badges |
|
emailYour email address |
|
orcidThe ORCID iD on your profile |
|
affiliationsThe organisations and roles listed on your profile |
|
worksThe list of works you have registered, with their ARK identifiers |
|
offline_accessStay connected when you are not using the app (refresh token, 30 days) | No claims — returns a refresh token (30 days, rotated on every use; reusing an old one revokes the whole chain). |
Works items (GET https://id.nobleid.org/oauth/works, Bearer token with scope works): nobleid, ark, url, title, type, doi, publication_date, publication_year, version, registered_at.
Endpoints
| Issuer | https://id.nobleid.org |
| Discovery | https://id.nobleid.org/.well-known/openid-configuration |
| Authorization | https://www.nobleid.org/oauth/authorize |
| Token | https://id.nobleid.org/oauth/token |
| Userinfo | https://id.nobleid.org/oauth/userinfo |
| JWKS | https://id.nobleid.org/.well-known/jwks.json |
| Revocation | https://id.nobleid.org/oauth/revoke |
| Introspection | https://id.nobleid.org/oauth/introspect |
| Pushed authorization (PAR) | https://id.nobleid.org/oauth/par |
| Works list (scope works) | https://id.nobleid.org/oauth/works?limit=&offset= |
- The authorization endpoint is the consent page on
www.nobleid.org; every other endpoint is under the issuer,id.nobleid.org. Fetch the discovery document rather than hard-coding URLs. - Only
response_type=codewith PKCES256. No implicit or hybrid flow, noplainPKCE. - The token endpoint accepts
application/x-www-form-urlencodedonly. ID tokens areES256JWTs. - The authorization response includes
iss(RFC 9207). Check it equals the issuer.
Security requirements for your integration
- Use PKCE with S256 on every request, with a fresh random verifier of at least 32 bytes.
- Send a random
stateand check it on return; send anonceand check it in the ID token. - Check
issin the authorization response, andiss,aud(your client ID) andexpin the ID token. - Verify the ID token’s ES256 signature against the JWKS on your server. Cache the key set and refetch when you see an unknown
kid. - Register exact redirect URIs. They are compared character for character; https only, except http on localhost / 127.0.0.1 / [::1], where the port may vary.
- Keep the client secret on your server. Browser and mobile apps must be public clients.
- Key accounts on
sub, never on email. Never link an existing account by email unlessemail_verifiedistrue. - Refresh tokens rotate: store the new one each time. Presenting an old one revokes the whole family and signs the person out of your app on their next refresh.
- On sign-out, revoke the refresh token at
https://id.nobleid.org/oauth/revoke.
How NobleID protects sign-in
- Authorization codes are single-use, stored hashed, and expire after 120 seconds.
- Access tokens last 15 minutes. Refresh tokens rotate on every use, and reuse of an old refresh token revokes every token in that chain.
- Consent is per app and per scope. People can remove an app at any time in Settings → Connected apps, which revokes its tokens immediately.
- The consent page cannot be framed by another site, and only redirects to URIs NobleID has validated.
- Sign-in and consent events are written to an audit log, and the endpoints are rate-limited.
- Everyone who registers or manages a partner app must have two-step login (an authenticator-app code) on their NobleID account.
- ID tokens are signed with ES256 by a key held in AWS KMS: the private key never leaves the key service, and the NobleID server can only ask it to sign. Public keys are published at the JWKS URL; when the key is rotated, the old one stays published until every token it signed has expired.
What a NobleID sign-in tells you: the person controls a NobleID account that signs in with email and password, Google or Apple, and, if they have turned it on, also entered a code from an authenticator app (two-step login). Two-step login is optional, so read amr (below) to see how this sign-in was done. identity_verified tells you separately whether the account holder passed an identity check.
How the person signed in (amr)
ID tokens from the authorization-code exchange carry amr (RFC 8176): the methods the person used to sign in to NobleID for this session, alongside auth_time.
| Value | Meaning |
|---|---|
pwd | Email and password. |
google | Sign in with Google (Google verified the account; NobleID did not see a password). |
apple | Sign in with Apple. |
privy | Legacy sign-in (Privy), kept for older accounts. |
otp | A code from the person’s authenticator app (RFC 6238 TOTP), or one of their single-use backup codes. |
mfa | Two methods were used: one of the above plus otp. |
Examples: ["pwd"], ["pwd","otp","mfa"], ["google"], ["google","otp","mfa"].
- To require two-step login, check that
amrcontainsotpand send the person back if it does not; they can turn it on in their NobleID security settings. amris omitted when NobleID does not know it: ID tokens issued from a refresh token (which also omitauth_time), and sessions that began before two-step login existed.- NobleID issues no
acrand defines no assurance levels;acr_valuesis ignored.max_ageandprompt=loginare honoured againstauth_time. - Two-step login is a time-based code from an authenticator app, with single-use backup codes. NobleID does not offer passkeys, WebAuthn security keys or SMS codes.
Branding
- Use the official button (below, or rendered by the SDK). Don’t redraw, recolour or stretch the logo.
- Wording: “Sign in with NobleID” or “Continue with NobleID”.
- Minimum height 40px. Keep clear space around the button of at least 8px.
- Place it with equal prominence next to your other sign-in options.
- Show a person’s NobleID as the full link, e.g.
https://www.nobleid.org/{nobleid}(thenobleid_uriclaim).
FAQ
- How does this relate to ORCID?
- They are complementary. ORCID identifies researchers across the scholarly record; NobleID can carry the person’s ORCID iD (scope orcid, with orcid_verified telling you whether it was connected through ORCID itself), and adds works registered on NobleID with ARK identifiers, plus the identity-check flag. Many integrations will offer both buttons.
- What is the difference between sub and nobleid?
- sub is an opaque, stable account identifier — use it as your database key. nobleid is the person’s public NobleID code, for display and linking.
- Can my app register works or edit a profile?
- No. All scopes are read-only. Partner minting (works:write) is planned for a partner programme and is not available today.
- Is there single sign-out?
- There is no end-session endpoint today. Sign the person out of your own app and revoke their refresh token.
- How does my app get the verified badge on the consent screen?
- Email info@nobleid.org with your client ID and the domains listed on the app. Staff check domain ownership by hand with a DNS TXT record. Until then people see a “not verified” notice.
Test client
Run a real sign-in against NobleID with your own client ID, and see exactly what your app would receive.
- In OAuth apps, register a public client (or edit one) and add this redirect URI exactly:
https://www.nobleid.org/developers/sign-in/callback- Paste its client ID below, pick scopes, and run the flow. You will see the real consent screen.
- On return, this page exchanges the code (no secret — public client), shows the decoded ID token and the userinfo response.
Confidential clients cannot be tested here — their code exchange needs the client secret, which must never be typed into a web page. Use your own server, or register a second, public client for testing.