Kerberos, LDAP, SAML, WebAuthn, and RADIUS are not five successive generations of login protocols replacing one another. They organize tickets, directory operations, cross-system assertions, public key credentials, and network access, respectively, and can collaborate within the same system. When comparing them, first ask: who verifies credentials, who makes the authorization decision, and which component ultimately enforces that decision?
Mechanism
Primary Objects
Trust Basis
Common Usage Location
Kerberos
principal, ticket, session key
KDC and service long-term keys
Intra-domain service authentication and SSO
LDAP
Directory entries and connection authentication state
Directory authentication methods and ACLs
Account, group, and attribute queries
SAML 2.0
Identity assertions and browser SSO
IdP metadata and signature keys trusted by the SP
Enterprise federated login
WebAuthn
Public key credentials scoped to the RP
Registered public keys and authentication transactions
User login, re-authentication
RADIUS
Network access authentication and policy attributes
NAS, authentication server, and specific authentication methods
Wi-Fi, VPN, switch access control
Whether a password is involved is only one dimension of comparison. How keys are registered and revoked, how accounts are recovered, and how established sessions are invalidated also determine system behavior.
Kerberos: Tickets Are Not Reusable Complete Login Proofs
The KDC includes two logical roles: AS and TGS. The client first obtains a TGT via AS exchange, then requests a ticket for the target service from the TGS using the TGT, and finally sends an AP request to the service. Tickets can usually be cached for use until their validity period expires; they do not require re-accessing the TGS every time a file is read.
Object
Protected/Used By
Why the Client Needs a Separate Session Key
TGT
Encrypted part protected by TGS long-term key
The client cannot derive secrets by reading the TGT; it needs the protected session key from the AS response
Service Ticket
Encrypted part protected by target service long-term key
The client uses the paired session key to construct the authenticator
Authenticator
Protected with the corresponding session key
Proves the current requester holds that key and provides freshness information
Password-based initial exchanges do not send raw passwords but use long-term keys derived from passwords and typically include pre-authentication. Kerberos also supports public key initial authentication such as PKINIT; one cannot write "all logins derive a password key only once" as a protocol law. RFC 4120, PKINIT
After the service decrypts the ticket, it must still verify the target principal, validity period, identity and time in the authenticator, and prevent reuse of the same authenticator. Timestamps only limit the acceptable window; replay is still possible within that window, so "not expired" cannot replace replay caches or other replay protection allowed by the protocol. Allowed clock skew is controlled by implementation and policy, not a universal fixed five minutes.
AP-REP can provide mutual authentication, proving the service also holds the session key. Basic verification does not require online access to the KDC, but specific deployments may still involve authorization data validation and other dependencies. Since symmetric keys can be used by the service itself, "being able to decrypt cryptographically proves only the KDC could have issued it" is also inaccurate.
Caching improves availability but introduces revocation latency: when the KDC is temporarily unreachable, already valid service tickets may continue to be used, while requests for new tickets or renewals are affected. Compromise of a service long-term key affects trust in that service's tickets; compromise of the TGS key has a wider scope. After ticket authentication is complete, the service still needs to determine whether the principal can read a specific directory or perform a specific action based on authorization data.
Why reject the second authenticator within its time window?
Deliver twice at one time: the second is replay despite valid time. A fresh authenticator keeps principalalice and can pass again. Moving receive time away triggers a separate freshness rejection.
Preparing the visual
Why reject the second authenticator within its time window?
Time bounds age; replay memory prevents reuse. One-time authenticator identity is not a one-time principal: users can authenticate again.
{"id":"kerberos-replay-time","title":"Why reject the second authenticator within its time window?","summary":"Time bounds age; replay memory prevents reuse. One-time authenticator identity is not a one-time principal: users can authenticate again.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"time\" data-i18n=\"time\"></label><output for=\"time\" id=\"time-value\"></output><input id=\"time\" type=\"range\" min=\"0\" max=\"100\" value=\"20\" step=\"1\"></div><div class=\"actions\"><button id=\"send\" data-i18n=\"send\"></button><button id=\"fresh\" data-i18n=\"fresh\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"state\"></h3><div id=\"state\"></div></section><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#state .tr{grid-template-columns:1fr 1fr}.tr>*{min-height:3.6em}.metrics strong{font-size:17px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let id=1,stamp=20,seen=new Set,last='∅';function draw(){set('time-value',$('#time').value);table('state',[['principal','alice'],['authenticator','A'+id+' @'+stamp],['already seen',seen.has(id)?'yes':'no']]);set('result',last);$('#state').dataset.snapshot=JSON.stringify({id,stamp,seen:[...seen],last});}$('#send').onclick=()=>{const valid=Math.abs(+$('input').value-stamp)<=30;if(!valid)last='REJECT: time';else if(seen.has(id))last='REJECT: replay';else{seen.add(id);last='ACCEPT'}draw()};$('#fresh').onclick=()=>{id++;stamp=+$('input').value;draw()};$('#reset').onclick=()=>{id=1;stamp=20;seen=new Set;last='∅';draw()};$('#time').oninput=draw;draw();","audio":false,"strings":{"fresh":"Fresh authenticator, same principal","heading":"Why reject the second authenticator within its time window?","intro":"Vary receive time; resend one authenticator or issue a fresh one for the same principal.","reset":"Reset","result":"Last validation result","scope":"Kerberos authenticator teaching model,±30s, ticket/cryptographic/identity checks pass. ID represents an identifiable authenticator tuple, not a new wire ID field.","scopeLabel":"Model scope and assumptions","send":"Deliver current authenticator","state":"Authenticator and replay state","time":"Receive time (s)"}}
LDAP: Directory Queries and Connection Identity Are Two Layers of State
LDAP entries have DNs, such as uid=alice,ou=people,dc=example,dc=com; search specifies base DN, scope, filter, and attribute set. The DN is a directory location identifier and can change with renaming or moving; long-term associations for directory accounts should be designed based on stable identifiers provided by the deployment. RFC 4511
The common search-then-bind flow is: the application searches for the user with limited-privilege service identity, confirms exactly one matching record, and then authenticates using the returned DN and user password. Directly concatenating user input into a DN may not conform to directory structure, and obtaining multiple search results does not allow arbitrarily taking the first one.
Bind Method
State Meaning
Boundary Conditions to Note
Empty DN, empty password simple bind
Anonymous state
Does not represent successful user authentication
Non-empty DN, empty password simple bind
If server allows, establishes unauthenticated anonymous state
Do not treat successful results as password verification for that DN
Non-empty DN, non-empty password simple bind
Username/password authentication
Depends on transport protection such as correctly verified TLS
SASL bind
Determined by the selected SASL mechanism
SASL is a framework; it does not mean every mechanism avoids sending passwords or is automatically stronger
Login interfaces should reject empty passwords from entering the username/password authentication path. When using StartTLS, confirm that the upgrade and server certificate verification are complete before submitting credentials; do not treat continuing bind after upgrade failure as an equivalent path. EXTERNAL can leverage externally established secure identity, but certificate identity to directory authorization identity still requires mapping. RFC 4513
Bind changes the connection's authentication state. Temporarily binding to a specific user on a shared search connection affects subsequent requests; authentication connections and service query connections should have clear lifecycles. Re-bind failure does not imply the original service account permissions are retained.
Filter Escape and DN Escape Are Not the Same Rules
The asterisk in (uid=alice*) has pattern meaning; if a username truly contains an asterisk, it needs to be encoded according to filter syntax. The following escapes each byte of the UTF-8 value, resulting in a verbose format but allowing direct inspection to ensure no filter structure injection; in practice, use the LDAP library's filter value encoding function.
deffilter_value(text):return''.join('\\'+format(b,'02x')forbintext.encode('utf-8'))assertfilter_value('a*(b)\\')==r'\61\2a\28\62\29\5c'assertfilter_value('中')==r'\e4\b8\ad'assertfilter_value('\0')==r'\00'query='(uid='+filter_value('alice*')+')'assertquery==r'(uid=\61\6c\69\63\65\2a)'print('Filter values encoded by byte; not used for DN or attribute names')
This function only handles attribute values and cannot be used to allow users to arbitrarily choose search base, attribute names, or filter operators. RFC 4515
SAML: Which Signature Element and Which Request Are Trusted
In common SP-initiated Web SSO, the SP saves the request ID, sends the AuthnRequest to the IdP via the browser; after the IdP completes the applicable user authentication, it typically POSTs the Response to the SP's ACS via the browser. The SP verifies the response and assertion, then creates its own session. Base64 XML carried by the browser is not inherently trusted content, nor does it imply encryption. SAML Profiles
Check
Question Answered
Trusted IdP and signature key
Is it from a pre-established trust issuer?
Signature-referenced elements
Are the identity attributes read by the program exactly the data in the verified elements?
Audience
Is the assertion applicable to this SP?
Recipient / Destination
Do the response or subject confirmation point to the expected receiving location?
InResponseTo
Is it associated with an incomplete SP request, applicable to request-response mode?
NotBefore / NotOnOrAfter
Is it within the allowed usage time range?
Assertion / Response ID and cache
Has this acceptable message already been used?
The core issue of XML Signature Wrapping is "verify signature A, but business logic reads B." Finding only one valid signature in the document is not enough; use a mature SAML library and ensure the consumed assertion and signature verification result are the same structure object. Element IDs, parsing, and allowed signature structures must also be handled according to library and profile requirements.
NameID is a subject identifier, not a one-time consumption login nonce. Its format and issuer constraints jointly affect identity mapping; persistent NameID can be used for multiple logins. Anti-replay should associate request ID, assertion/response ID, and validity period; do not permanently mark a user as "NameID used" after their first login.
IdP-initiated non-request-response logins do not have the same original SP request to match and require explicitly adopting the processing policy of that profile; do not turn off all InResponseTo checks for callbacks just for compatibility. RelayState is commonly used to restore application state and cannot directly become any arbitrary cross-site redirect address. SAML Core
The following only demonstrates one-time consumption after signature verification and other condition checks are complete; actual services must atomically check and register in shared storage and retain until the corresponding acceptance window ends.
seen=set()defconsume(issuer,assertion_id,expires,now):key=(issuer,assertion_id)ifnow>=expiresorkeyinseen:returnFalseseen.add(key)returnTrueassertconsume('idp-a','assertion-1',200,100)assertnotconsume('idp-a','assertion-1',200,101)assertconsume('idp-a','assertion-2',200,101)# Same user can log in again.
assertnotconsume('idp-a','assertion-3',200,200)print('Assertion replay and expiration boundaries passed; NameID is not consumed one-time')
WebAuthn: Credential Scope, Byte Signature, and User Verification
During registration, the RP issues a challenge, the browser and authenticator complete navigator.credentials.create(), the RP verifies the registration result, and saves the credential ID, public key, user handle, and required state associations. During authentication, navigator.credentials.get() is used; the verifier must know which local account the credential belongs to; adding new credentials also requires reliable registration authorization.
The input to the authentication signature is the raw bytes of authenticatorData || SHA256(clientDataJSON). clientDataJSON contains ceremony type, challenge, origin, etc.; authenticatorData contains RP ID hash, flags, and counter, etc. Do not parse the raw JSON, re-serialize it, and then calculate the digest, because changes in whitespace and field order will change the bytes.
The RP should check the expected challenge, allowed origin, RP ID hash, credential and account relationship, and UP/UV flags required by policy, then verify the signature according to the credential public key and algorithm. UP indicates user presence interaction, UV indicates completion of local user verification; touching the device is not always equivalent to fingerprint or PIN verification. WebAuthn Verifying Assertion
importhashlibimportjsonimportstructrp_id='example.com'raw_client_data=json.dumps({'type':'webauthn.get','challenge':'example-only','origin':'https://login.example.com'},separators=(',',':')).encode()# Minimal authenticatorData without extensions: 32 B hash + 1 B flags + 4 B counter.
auth_data=hashlib.sha256(rp_id.encode()).digest()+bytes([0x05])auth_data+=struct.pack('>I',0)signed_bytes=auth_data+hashlib.sha256(raw_client_data).digest()assertlen(auth_data)==37andlen(signed_bytes)==69assertauth_data[32]&0x01andauth_data[32]&0x04# UP, UV
asserthashlib.sha256(raw_client_data).digest()!=hashlib.sha256(json.dumps(json.loads(raw_client_data),indent=2).encode()).digest()print('Signature input 69 B; here checking encoding, not replacing real signature verification and transaction validation')
RP ID is usually a valid domain name, while origin includes protocol, host, and applicable port. login.example.com can use example.com as the RP ID when compliant, but the RP must still restrict allowed origins; do not use direct string equality between the two as the only check.
Passkeys Are Not "Trapped in One Hardware Device Forever"
Passkeys can be synchronized credentials or bound to a single device. Synchronized providers use credentials across multiple devices under their protection mechanisms, with different lifecycles than device-bound keys; WebAuthn does not require all private keys to be located in non-exportable hardware security modules. Biometrics are also not a mandatory condition; user verification can be completed via device PIN, etc., and the RP typically does not receive raw biometrics. FIDO Passkeys
Concept
Function
Conclusion Not to Be Drawn
Discoverable credential
Authenticator can discover account credentials, supporting login without entering username first
All credentials must be cloud-synced
Attestation
Provides authenticator-related evidence according to format and trust chain
Automatically proves the real identity of a natural person
Backup eligibility / state
Indicates backup eligibility and current backup state
Equals a specific cloud service brand
signCount
Provides potential signals of cloning or out-of-order
0 is necessarily illegal, or non-incrementing is necessarily an attack
Authenticators that do not implement counters can always return 0; in other cases, non-incrementing may involve cloning, failure, or concurrent response out-of-order, and should be handled according to specifications and risk policies. One-time use of the challenge remains one of the cores of transaction freshness.
Domain scoping prevents spoofed sites from obtaining authentication results for the real RP like they would collecting passwords or OTPs. However, account recovery, already logged-in sessions, and terminal environments still have their own trust boundaries; removing passwords does not automatically strengthen weak recovery flows.
Why can a correct signature still fail login?
Initially RPexample.com differs from the login-subdomain origin, so direct equality is not the rule. Removing UV while retaining UP distinguishes presence from required verification; other correct fields cannot compensate.
Preparing the visual
Why can a correct signature still fail login?
A signature protects bytes but does not decide whether they fit the current transaction. RP ID need not equal the full origin host, yet both need checks.
{"id":"webauthn-scope-and-transaction","title":"Why can a correct signature still fail login?","summary":"A signature protects bytes but does not decide whether they fit the current transaction. RP ID need not equal the full origin host, yet both need checks.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"rp\"></span><input type=\"text\" id=\"rp\" value=\"example.com\" maxlength=\"40\" autocomplete=\"off\" spellcheck=\"false\"></label><label class=\"form-field\"><span data-i18n=\"origin\"></span><input type=\"text\" id=\"origin\" value=\"https://login.example.com\" maxlength=\"50\" autocomplete=\"off\" spellcheck=\"false\"></label><label class=\"form-field\"><span data-i18n=\"challenge\"></span><input type=\"text\" id=\"challenge\" value=\"C42\" maxlength=\"12\" autocomplete=\"off\" spellcheck=\"false\"></label><label class=\"check\"><input type=\"checkbox\" id=\"up\" checked><span data-i18n=\"up\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"uv\" checked><span data-i18n=\"uv\"></span></label><div class=\"track-row\"><span data-i18n=\"checks\"></span><div id=\"checks\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const checks={RP:$('#rp').value==='example.com',origin:$('#origin').value==='https://login.example.com',challenge:$('#challenge').value==='C42',UP:$('#up').checked,UV:$('#uv').checked};cells('checks',Object.entries(checks).map(([text,ok])=>({text,cls:ok?'on':'error'})));set('result',Object.values(checks).every(Boolean)?'ACCEPT':'REJECT');$('#result').dataset.checks=JSON.stringify(checks);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"challenge":"clientDataJSON.challenge","checks":"Independent validation","heading":"Why can a correct signature still fail login?","intro":"Vary RP ID, origin, challenge and UP/UV; inspect authenticator and client-data constraints.","origin":"clientDataJSON.origin","reset":"Reset","result":"Authentication result","rp":"RP ID represented by authenticatorData","scope":"Signature/account binding already valid. Expected RPexample.com, originhttps://login.example.com, challengeC42, require UP/UV. Server-condition model, no authenticator invocation.","scopeLabel":"Model scope and assumptions","up":"UP: user present","uv":"UV: user verified"}}
RADIUS and EAP: Authentication Server Decides, NAS Executes
In typical 802.1X, the terminal interacts with the AP/switch via EAPOL, and the NAS encapsulates EAP messages into RADIUS attributes to send to the authentication server. EAP is an extensible authentication framework; EAP-TLS is the method within it that uses TLS; do not call all EAP encrypted tunnels. RADIUS/EAP, EAP-TLS 1.3
RADIUS Access-Request can receive Access-Challenge, Access-Accept, or Access-Reject. Multi-round challenges use attributes like State to associate the authentication process; Accept may include authorization attributes such as VLAN, filtering, or session time limits. If authentication passes but VLAN is not correctly applied, check NAS execution and attribute support, rather than just re-entering the password.
Classic RADIUS/UDP commonly uses 1812 for authentication and 1813 for accounting, with a basic 20-byte header including Code, Identifier, Length, and 16-byte Authenticator; the specific construction of the Authenticator depends on the message type and cannot be uniformly called a "nonce" or "full packet encryption." Shared secrets and traditional attribute protection do not equal modern transport confidentiality for the entire packet. RFC 2865
RADIUS/TLS or DTLS protects transport between NAS/proxy and server, which is a different boundary from EAP-TLS from terminal to EAP server. RADIUS/1.1 defined in RFC 9765 is an experimental specification that removes shared secrets, MD5 packet authentication, and attribute obfuscation mechanisms from its mode via TLS/DTLS ALPN negotiation; do not claim that all existing UDP RADIUS has been upgraded because of this. RADIUS/1.1
RADIUS servers can use local configuration, certificates, directories, or other identity sources, and do not necessarily need to place account data in LDAP/AD. Accounting records describe session events and statistics and do not naturally constitute an exactly-once ledger; applications need to handle retransmissions, missing records, and session association.
TACACS+ is commonly used for authentication, per-command authorization, and accounting in device management. Its traditional protocol header is plaintext, and the body uses shared-secret-based obfuscation; it cannot be simplified as "end-to-end modern encryption"; whether it is suitable to replace a specific RADIUS deployment depends on device capabilities and management needs. RFC 8907
Combining Them into an Identity System
An enterprise can use LDAP to store directories, Kerberos to access domain services, have the IdP authenticate users via WebAuthn, and then issue SAML or OIDC results to web applications; office networks use RADIUS/EAP for access. These components share the personnel lifecycle but do not automatically share the same session state.
After an employee leaves, directory deactivation, remaining ticket validity, IdP sessions, application sessions, registered passkeys, and network access sessions change separately. Complete design should clarify the holder, validity period, and revocation propagation method for each type of state. After protocol selection, the identity lifecycle still needs to be strung together.