skip to content

SPNEGO and GSSAPI

How a ticket reaches a web application: GSSAPI wraps the exchange, SPNEGO negotiates the mechanism, and HTTP carries it as Negotiate tokens, with NTLM as fallback. Asked when browser SSO breaks.

on this pageshow

explore

questions

5

In HTTP Negotiate authentication, what is inside the base64 value of the Authorization header a client sends?

level: juniorimportance: must knowfreq 42%

answer

  1. a handshake, not a stored credential
  2. the scheme name says negotiation
  3. base64 hides structure, not secrets
  4. SPNEGO wrapper, mechanism token inside
  5. AP-REQ sealed for one service principal

basics

~10 s

A GSS-API context-establishment token, normally a SPNEGO NegTokenInit wrapping a Kerberos AP-REQ built from a service ticket. It is one leg of a handshake for one service, not the user's password.

solid answer

~40 s

RFC 4559's `Negotiate` scheme carries a GSS-API context-establishment token, base64-encoded, in `Authorization: Negotiate <base64>`. Normally that token is a SPNEGO `NegTokenInit` under mechanism OID `1.3.6.1.5.5.2`, listing the mechanisms the client can use in `mechTypes` and often carrying an optimistic `mechToken` for the first of them; the scheme also permits a bare Kerberos V5 GSS-API token under OID `1.2.840.113554.1.2.2`. When the mechanism is Kerberos, the innermost object is an `AP-REQ`: a service ticket the client obtained earlier and cannot itself decrypt, plus a fresh `Authenticator` sealed with the session key that came with that ticket. The user's long-term key never travels. The blob is scoped to one service principal and one context, so a second service cannot use it.

code

http · 12 lines
http
GET /archive/rushes/2026-09-19 HTTP/1.1
Host: archive.internal.example

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Negotiate

GET /archive/rushes/2026-09-19 HTTP/1.1
Host: archive.internal.example
Authorization: Negotiate YIIFvAYGKwYBBQUCoIIFsDCCBayg...

HTTP/1.1 200 OK
WWW-Authenticate: Negotiate oYG3MIG0oAMKAQChCwYJKoZIhvcSAQICoo...

go deeper

for a junior

Recall the shape: the header is Authorization: Negotiate <base64>, and the base64 is a handshake token rather than a password or a session identifier.

for a middle

Explain that the payload is a GSS-API context token, normally a SPNEGO wrapper around a Kerberos AP-REQ, and say which part of it the client itself cannot read.

for a senior

Be able to tell from a captured exchange whether a Kerberos context was established or a weaker mechanism answered, and say what the final token on the 200 response is for.

for a principal

Weigh what this carriage commits an estate to: clients that already hold tickets, service names that resolve as registered, and a silent fallback path you must decide whether to permit at all.

## The scheme is a pipe, not a credential format The HTTP `Negotiate` authentication scheme, described in **RFC 4559**, defines no credential of its own. It defines carriage. The service offers the scheme with a bare `WWW-Authenticate: Negotiate` header — no parameters and nothing for the client to fill in — and the client answers with `Authorization: Negotiate <base64>`, where the base64 is a **GSS-API context-establishment token** exactly as the security mechanism produced it. Base64 is present because the token is binary DER-encoded ASN.1 and a header field value is text. It provides no confidentiality at all: anyone who can read the header can decode the bytes. ## What the bytes decode to Two shapes appear in practice: - a **SPNEGO negotiation token** under mechanism OID `1.3.6.1.5.5.2` (RFC 4178) — a `NegTokenInit` whose `mechTypes` field lists the mechanisms the initiator supports in preference order, and whose optional `mechToken` optimistically carries the first of those mechanisms' own token; - a **bare Kerberos V5 GSS-API token** under OID `1.2.840.113554.1.2.2`, with no negotiation wrapper around it. When the selected mechanism is Kerberos, the innermost object is an `AP-REQ` (`[APPLICATION 14]`). It has two parts that behave very differently: 1. the **service ticket**, issued earlier by the realm's KDC and encrypted under the *service principal's* long-term key — the client carries it but cannot read it; 2. the **`Authenticator`** (`[APPLICATION 2]`), built fresh by the client and encrypted under the **session key** that arrived with the ticket, carrying `cname`, `ctime`/`cusec` and a checksum. The client's own long-term key — the value derived from the user's password by `string-to-key` — is not in the token and is not derivable from it. ## What the value is, and is not | The value is | The value is not | |---|---| | a handshake message for one context | a stored, reusable credential | | scoped to one named service principal | presentable at a second service | | binary ASN.1, base64 only for transport | protected by that base64 | | possibly one of several legs | guaranteed to finish in one request | ## The exchange, leg by leg 1. The client requests the resource with no credential. 2. The service answers `401` with `WWW-Authenticate: Negotiate` carrying no data. 3. The client obtains a service ticket for that service, builds the context token, and repeats the request with `Authorization: Negotiate <base64>`. 4. If the mechanism needs a further leg, the service answers `401` again, this time `WWW-Authenticate: Negotiate <base64>`, and the client feeds that token back into its own side of the exchange. 5. On success the service returns the resource, and **may** include a final `WWW-Authenticate: Negotiate <base64>` on the `200` response. That last header is the part most readers skip past. When the initiator asked for mutual authentication — `GSS_C_MUTUAL_FLAG (2)` in the Kerberos mechanism's request — the acceptor's final token is what lets the client conclude that the service genuinely held the key the ticket was sealed under. A client that discards it has proved the user to the service and learned nothing about the service. ## Why the extra round trip exists The first request in the flow is deliberately credential-free, so every new context costs an extra request and response. Two things reduce that cost in practice: a client that remembers a host answered with `Negotiate` may send the token on the first request next time, and a context established on a persistent connection can serve later requests on it. Neither changes the shape of the exchange; they change how often you pay for it. ## Where this goes wrong - **Reading base64 as protection.** It is an encoding. The token's security comes from the sealed ticket and the keyed `Authenticator` inside it, not from the wrapper. - **Treating the blob as portable.** The ticket names one service principal and is encrypted under that principal's key; another service holds a different key and cannot open it. - **Expecting a readable document.** There is no JSON and no claim set here; the payload is ASN.1 that only the mechanism parses. - **Ignoring the final token.** Dropping the `200` response's header quietly turns a mutually authenticated exchange into a one-directional one. - **Assuming one leg.** Some mechanisms and some negotiations need two or three, and the intermediate legs look exactly like authentication failures to a reader who only counts `401`s.

  • Why does a successful 200 response still carry a `WWW-Authenticate: Negotiate` header?
    It carries the acceptor's final GSS-API token. When the initiator asked for mutual authentication, that token is what lets the client verify the service actually held the key the ticket was sealed under. RFC 4559 lets the service return it on the final response; a client that ignores it has authenticated the user to the service but not the service to the user.
  • May a client send `Authorization: Negotiate` before it has been challenged?
    The documented flow starts from a bare `WWW-Authenticate: Negotiate` challenge, so a first request to an unknown host normally goes out without it and pays an extra round trip. Clients commonly remember that a host offered `Negotiate` and send the token pre-emptively on later requests to the same host, which removes that round trip without changing the exchange.
  • Is the base64 token usable against a different service?
    No. The inner Kerberos `AP-REQ` names one service principal, and its ticket is encrypted under that principal's long-term key. A different service holds a different key and cannot open it, which is why this value is a proof for one service rather than a credential to carry around.

saying these in an interview costs you the question

  • Says the base64 is the user's password, encoded
  • Calls it a bearer credential any service can accept
  • Thinks base64 in the header provides confidentiality
  • Assumes the token is a JSON or claims document
  • Believes the client decrypts and reads the ticket inside
open as a page

What does GSS-API give a client application, and why is SPNEGO called a pseudo-mechanism inside it?

level: middleimportance: must knowfreq 46%

basics

~10 s

GSS-API gives one mechanism-independent loop: GSS_Init_sec_context and GSS_Accept_sec_context exchange opaque tokens until a context exists. SPNEGO is a mechanism whose only work is choosing which real mechanism, named by OID, both ends will run.

open as a page

In SPNEGO, what does a NegTokenInit offer an acceptor, and what does the reply's negState say?

level: middleimportance: should knowfreq 34%

basics

~10 s

NegTokenInit offers an ordered mechTypes list and often an optimistic mechToken for the first entry. The acceptor's NegTokenResp answers with negState — accept-completed(0), accept-incomplete(1), reject(2) or request-mic(3) — plus supportedMech and responseToken.

open as a page

Browser Negotiate sign-on works for an archive's registered host name but prompts on an alternative name — why?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The client builds the service principal name from the host in the URL, as HTTP/<host>. An alternative name is a different principal; with none registered the KDC answers KDC_ERR_S_PRINCIPAL_UNKNOWN (7) and the negotiation drops to a weaker mechanism or a prompt.

open as a page

Why does SPNEGO exchange a mechListMIC, and what is an acceptor requesting with negState request-mic?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Because the advertised mechTypes list travels before any key exists, an active attacker could delete the strong mechanism from it. The mechListMIC is a checksum over that list, computed once a context exists; request-mic(3) demands it.

open as a page