Introduction
We offer DN42 sign-in, third-party app authorization, self-service app management, and account settings.
Access
OIDC Discovery / Auto configuration file URL
- https://auth.iedon.net/.well-known/openid-configuration
- https://oauth.dn42/.well-known/openid-configuration
Callback URL
- You can only create/save callback URLs with "http" schema in oauth.dn42 (not clearnet auth.iedon.net)
- "https" schema check for callback URLs is enabled in clearnet version of OAuth (auth.iedon.net)
Claims
Request the dn42 scope to include DN42 network information in your tokens. The dn42 claim contains the route prefixes originated by the authenticated AS, telephony prefixes registered under the active maintainer, and session-specific identity fields tied to the maintainer used at login.
Supported scopes: openid, profile, email, dn42
TLDR
- Claim
openid,email,profile,dn42 - Use
dn42.asnto get AS (Type: number) - Use
dn42.active_mntto get exact maintainer user logged-in with - Use
dn42.active_nameto get friendly name of active maintainer - Use
dn42.routeanddn42.route6to get routes user is allowed to use - Use
dn42.telephonyto get registered telephony prefixes of active maintainer
The dn42 Claim
| Field | Type | Description |
|---|---|---|
asn |
number | AS number of the authenticated user |
route |
string[] | IPv4 prefixes originated by this AS (real-time WHOIS) |
route6 |
string[] | IPv6 prefixes originated by this AS (real-time WHOIS) |
timestamp |
number | Unix timestamp when the WHOIS query was performed |
active_mnt |
string | The maintainer used to authenticate; see About Active Maintainer bellow |
telephony |
string[] | Telephony prefixes from telephony objects maintained by active_mnt; omitted if none exist |
active_name |
string | Display name of the person associated with active_mnt (ID Token only) |
active_person |
string | NIC handle of the person associated with active_mnt (ID Token only) |
active_email |
string | Email address used to authenticate; non-empty only when the email auth method was used (ID Token only) |
auth_method |
string | Authentication method used: passkey, password, email, pgp, or ssh (ID Token only) |
mnt_by |
string[] | All maintainers listed in the AS object's mnt-by field (ID Token only) |
Fields marked ID Token only are present in the ID Token but not returned by the /userinfo endpoint. The UserInfo response for the dn42 claim contains only: asn, route, route6, timestamp, active_mnt, and telephony.
Email authentication: When the user signs in via the
active_emailis set to the verified address used during the challenge, and the top-levelactive_emailis empty and the top-level
About Active Maintainer (active_mnt)
The active maintainer is the specific mntner object the user authenticated with during login.
A single AS can be maintained by multiple maintainer objects, and each (maintainer, AS) pair represents a distinct identity. For example, if AS4242420001 has mnt-by: ALICE-MNT, BOB-MNT, Alice and Bob each produce a different token even though both authenticate as the same AS:
| Authenticated with | active_mnt |
sub |
|---|---|---|
ALICE-MNT + AS4242420001 |
ALICE-MNT |
sha256("ALICE-MNT/4242420001") |
BOB-MNT + AS4242420001 |
BOB-MNT |
sha256("BOB-MNT/4242420001") |
The sub claim is the lowercase hex-encoded SHA-256 digest of "<active_mnt>/<asn>". It stably and uniquely identifies the (maintainer, AS) pair and is guaranteed to be consistent between the ID Token and the /userinfo response.
Example ID Token Payload
Decoded payload for a login with scopes openid profile email dn42:
{
"at_hash": "dgaDDz-MiPldBFG3o22jMQ",
"aud": "3de6795a0ae65021d6b8cc3c7dfdc380",
"auth_time": 1782007350,
"dn42": {
"active_email": "",
"active_mnt": "IEDON-MNT",
"active_name": "Name of Active Maintainer",
"active_person": "IEDON-DN42",
"asn": 4242422189,
"auth_method": "password",
"mnt_by": [
"IEDON-MNT"
],
"route": [
"10.127.21.0/24",
"10.127.25.0/24",
"172.20.0.53/32",
"172.23.0.80/32",
"172.23.91.0/25",
"172.23.91.128/26"
],
"route6": [
"fd42:4242:2189::/48",
"fd42:d42:d42:54::/64",
"fd42:d42:d42:80::/64"
],
"telephony": [
"+04242189"
],
"timestamp": 1782007350
},
"email": "mail@active.maintainer.localdomain",
"exp": 1782010950,
"iat": 1782007350,
"iss": "https://auth.iedon.net",
"name": "Name of Active Maintainer",
"nonce": "zX6fNEIHT9PUdCNargvZhA",
"preferred_username": "AS4242422189",
"sub": "sha256(MNT/AS)"
}
Notice
It is recommended to use /userinfo with access token to refresh/re-get user's dn42 claim, as a server background task and run this task in a period, because a user may register or return things from and back to the DN42 registry.
Example
JavaScript
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import crypto from 'node:crypto'
// ── Configuration ──────────────────────────────────────────────
// Change these to match your auth42 backend and registered app
const PORT = 3002
const ISSUER = process.env.ISSUER || 'https://auth.iedon.net'
const CLIENT_ID = process.env.CLIENT_ID || 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`
const SCOPES = 'openid profile email dn42'
// ── PKCE helpers ───────────────────────────────────────────────
function base64url(buf) {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
function generateCodeVerifier() {
return base64url(crypto.randomBytes(32))
}
function generateCodeChallenge(verifier) {
return base64url(crypto.createHash('sha256').update(verifier).digest())
}
// In-memory state (single-user demo, good enough for testing)
let pendingState = null
let pendingVerifier = null
let tokens = null
// ── App ────────────────────────────────────────────────────────
const app = new Hono()
// Home: show status + login link
app.get('/', (c) => {
let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>OIDC Tester</title>
<style>body{font-family:system-ui;max-width:720px;margin:40px auto;padding:0 20px}
pre{background:#f4f4f4;padding:16px;border-radius:8px;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
a{color:#1677ff}h1{border-bottom:2px solid #eee;padding-bottom:8px}
.btn{display:inline-block;padding:10px 24px;background:#1677ff;color:#fff;text-decoration:none;border-radius:6px;margin:8px 4px 8px 0}
.btn.red{background:#ff4d4f}</style></head><body>`
html += `<h1>🔐 OIDC Tester</h1>`
html += `<p><strong>Issuer:</strong> ${ISSUER}</p>`
html += `<p><strong>Client ID:</strong> <code>${CLIENT_ID}</code></p>`
html += `<p><strong>Redirect URI:</strong> <code>${REDIRECT_URI}</code></p>`
if (CLIENT_ID === 'CHANGE_ME') {
html += `<p style="color:red">⚠️ Set CLIENT_ID and CLIENT_SECRET env vars or edit index.js first!
<br>Register an app in auth42, then set the values.</p>`
}
if (tokens) {
html += `<h2>✅ Authenticated</h2>`
html += `<p><a class="btn" href="/userinfo">Fetch /userinfo</a>`
html += `<a class="btn" href="/refresh">Refresh Token</a>`
html += `<a class="btn red" href="/logout">Logout (RP-Initiated)</a></p>`
html += `<h3>Access Token</h3><pre>${tokens.access_token}</pre>`
if (tokens.id_token) {
html += `<h3>ID Token</h3><pre>${tokens.id_token}</pre>`
try {
const payload = JSON.parse(Buffer.from(tokens.id_token.split('.')[1], 'base64').toString())
html += `<h3>ID Token Claims</h3><pre>${JSON.stringify(payload, null, 2)}</pre>`
} catch {}
}
if (tokens.refresh_token) {
html += `<h3>Refresh Token</h3><pre>${tokens.refresh_token}</pre>`
}
html += `<h3>Full Token Response</h3><pre>${JSON.stringify(tokens, null, 2)}</pre>`
} else {
html += `<p><a class="btn" href="/login">Login with OIDC</a></p>`
}
html += `</body></html>`
return c.html(html)
})
// Step 1: Redirect to authorization endpoint
app.get('/login', (c) => {
pendingState = base64url(crypto.randomBytes(16))
pendingVerifier = generateCodeVerifier()
const challenge = generateCodeChallenge(pendingVerifier)
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: SCOPES,
state: pendingState,
code_challenge: challenge,
code_challenge_method: 'S256',
nonce: base64url(crypto.randomBytes(16)),
})
return c.redirect(`${ISSUER}/authorize?${params}`)
})
// Step 2: Handle callback — exchange code for tokens
app.get('/callback', async (c) => {
const code = c.req.query('code')
const state = c.req.query('state')
const error = c.req.query('error')
if (error) {
return c.html(`<h1>Error</h1><p>${error}: ${c.req.query('error_description') || ''}</p><p><a href="/">Back</a></p>`)
}
if (!code || !state) {
return c.html(`<h1>Error</h1><p>Missing code or state</p><p><a href="/">Back</a></p>`)
}
if (state !== pendingState) {
return c.html(`<h1>Error</h1><p>State mismatch</p><p><a href="/">Back</a></p>`)
}
// Exchange authorization code for tokens
try {
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: pendingVerifier,
})
const resp = await fetch(`${ISSUER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
if (!resp.ok) {
const errBody = await resp.text()
return c.html(`<h1>Token Error (${resp.status})</h1><pre>${errBody}</pre><p><a href="/">Back</a></p>`)
}
tokens = await resp.json()
pendingState = null
pendingVerifier = null
return c.redirect('/')
} catch (err) {
return c.html(`<h1>Error</h1><pre>${err.message}</pre><p><a href="/">Back</a></p>`)
}
})
// Fetch userinfo
app.get('/userinfo', async (c) => {
if (!tokens?.access_token) return c.redirect('/')
try {
const resp = await fetch(`${ISSUER}/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
})
const data = await resp.json()
let html = `<h1>UserInfo Response (${resp.status})</h1>`
html += `<pre>${JSON.stringify(data, null, 2)}</pre>`
html += `<p><a href="/">Back</a></p>`
return c.html(html)
} catch (err) {
return c.html(`<h1>Error</h1><pre>${err.message}</pre><p><a href="/">Back</a></p>`)
}
})
// Refresh token
app.get('/refresh', async (c) => {
if (!tokens?.refresh_token) {
return c.html(`<h1>No Refresh Token</h1><p><a href="/">Back</a></p>`)
}
try {
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: tokens.refresh_token,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
})
const resp = await fetch(`${ISSUER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
const data = await resp.json()
if (resp.ok) {
tokens = { ...tokens, ...data }
}
let html = `<h1>Refresh Response (${resp.status})</h1>`
html += `<pre>${JSON.stringify(data, null, 2)}</pre>`
html += `<p><a href="/">Back</a></p>`
return c.html(html)
} catch (err) {
return c.html(`<h1>Error</h1><pre>${err.message}</pre><p><a href="/">Back</a></p>`)
}
})
// RP-Initiated Logout
app.get('/logout', (c) => {
const params = new URLSearchParams()
if (tokens?.id_token) {
params.set('id_token_hint', tokens.id_token)
}
params.set('post_logout_redirect_uri', `http://127.0.0.1:${PORT}/logged-out`)
params.set('client_id', CLIENT_ID)
tokens = null
return c.redirect(`${ISSUER}/logout?${params}`)
})
app.get('/logged-out', (c) => {
return c.html(`<h1>Logged Out</h1><p>Session ended.</p><p><a href="/">Back to Home</a></p>`)
})
// ── Start ──────────────────────────────────────────────────────
serve({ fetch: app.fetch, port: PORT }, () => {
console.log(`OIDC Tester running at http://127.0.0.1:${PORT}`)
console.log(`Issuer: ${ISSUER}`)
console.log(`Client ID: ${CLIENT_ID}`)
if (CLIENT_ID === 'CHANGE_ME') {
console.log('\n⚠️ Set CLIENT_ID and CLIENT_SECRET before testing!')
console.log(' Register an app in the auth42 admin, then:')
console.log(` CLIENT_ID=xxx CLIENT_SECRET=yyy node index.js\n`)
}
})