1

I'm building a single-sign-on solution where:

  • A user boots up theirActive Directory–joined Windows machine.
  • My app auto-starts after the Winlogon event.
  • Since the user already has a TGT from logon, my app requests aTGS for my custom SPN and sends this service ticket to another machine running a custom NodeJS service.
  • That NodeJS service has akeytab with full AD read access.

Goal: Decrypt the service ticket on the server and extract the AD username.


Steps I've taken

  1. Created a minimal-privilege service account:

    New-ADUser -Name "schoolieService" -SamAccountName "schoolieService" -AccountPassword (ConvertTo-SecureString 'SH8DXIrR2iWY' -AsPlainText -Force) -Enabled $true
  2. Registered an SPN for the service account:

    setspn -S HTTP/schoolie-server.schooliead.local schoolieService
  3. Generated a keytab for the service principal:

    ktpass /princ HTTP/[email protected] /mapuser[email protected] /pass SH8DXIrR2iWY /out ./http.keytab /ptype KRB5_NT_PRINCIPAL /crypto AES256-SHA1
  4. Copied the keytab to the server machine.


Client Code (NodeJS)

import Kerberos from "kerberos";const service = "HTTP/[email protected]";Kerberos.initializeClient(service, {}, (err, client) => {  if (err) throw err;  client.step('', (err, token) => {    if (err) throw err;    console.log(btoa(token)); // base64 service ticket  });});

This works - I get a long base64 service ticket.


Server Code (NodeJS)

import Kerberos from "kerberos";// Point kerberos at the keytabprocess.env.KRB5_KTNAME = "/path/to/http.keytab";const serviceTokenFromClient = "longBase64StringHere";// Fails hereconst kerberosServer = await Kerberos.initializeServer("HTTP/[email protected]");const responseToken = await kerberosServer.step(serviceTokenFromClient);console.log(responseToken);if (kerberosServer.username) {  console.log(kerberosServer.username);}

The Problem

When initializing the server with the keytab, I get this error:

[Error: No credentials were supplied, or the credentials were unavailable or inaccessible: No key table entry found matching HTTP\/schoolie-server.schooliead.local/schooliead.local@]

I've verified:

  • The SPN is correctly registered.
  • The keytab was generated without errors.
  • File permissions are correct.

I also ran:

klist -k -t -K http.keytab

Which shows the principal is in the keytab:

Keytab name: FILE:http.keytabKVNO Timestamp           Principal---- ------------------- ------------------------------------------------------  10 01/01/1970 01:00:00 HTTP/[email protected] (0x36ef8e2e07a150712f8ab3911d3c1d5a)

TL;DR

  • Client can generate a Kerberos service ticket using thekerberos npm package.
  • Server should decrypt the ticket with the keytab and extract the username.
  • Instead, I getNo key table entry found when callinginitializeServer.

Question: How can I correctly configure the keytab/SPN so the NodeJS server can read the username from the Kerberos service ticket?

Update

Thanks tograwity, I have successfully managed to initialize the server. The fix was changing the server-side service principal format from:

HTTP/[email protected]

to:

[email protected]

After this change, theinitializeServer call no longer throws the "No key table entry found" error.


However, I am still struggling with verifying the base64 service ticket. A few observations:

  • On the client side, the service parameter must remain in the original format (although the documentation says the same format is needed for both the client and the server as confirmed inthis test in the github repo line 43):

HTTP/[email protected]

Withany other format or string I get:

Error: InitializeSecurityContext: The specified target is unknown or unreachable

  • Using the old format allows the client to generate a valid service ticket.
  • But when I send that ticket to the server (which now uses the updated format) and callstep(), I get:
node:internal/process/promises:288        triggerUncaughtException(err, true /* fromPromise */);        ^[Error: Invalid token was supplied: Unknown error]

So currently:

  • Server initialization works with[email protected].
  • Client ticket generation works withHTTP/[email protected].
  • Token verification fails probably due to a format mismatch between client ticket and server principal.

This makes it clear that the remaining challenge is aligning the client service principal format with what the server expects for decryption, so the service ticket can be successfully verified and the username extracted.

askedSep 6 at 19:19
TechTomic's user avatar
0

2 Answers2

2

According todocumentation, initializeClient() and initializeServer() do not actually accept a Kerberos principal name. Instead they both expect a GSSAPI "host-based service" name, in the format ofservice@host, and will internally transform that to a Kerberos principal name. Providing a Kerberos principal directly will result in double transformation, similar tothis earlier thread.

So you should instead specify[email protected] as the service name.

(Though this is somewhat at odds with Windows SSPI only supporting the "Kerberos principal" syntax and not the "generic host-based service" syntax. So if your current format works on the client side, you can keep that and only change it on the server side.

Internally the API has a "name type" parameter to explicitly tell Kerberos whether you're passing a GSS name or a Kerberos name; it's unfortunate that the NodeJS module doesn't expose it.)


Some pedantry:

  1. "TGS" is not a service ticket; it stands for "ticket-granting service" and it's the service thatissues service tickets (and the protocol for requesting them). In other words, the TGS-REQ message doesn't request a TGS but rather talksto a TGS.

    (A service ticketmight be called a "TGS ticket" but only in the sense that it is a ticket that was issued by the TGS (not a ticket of 'TGS' type), but this is ambiguous because by the same logic a ticket-granting ticket (TGT) mightalso be called a "TGS ticket" in the sense that it is a ticket meant to be presented to the TGS.)

  2. Don't confuse Kerberos tickets and tokens. The service ticket is 'public' and is never sent alone – what you get from step() is an AP-REQ token that contains both the service ticketand an authenticator (signature), very similar to how a TLS handshake always sends the certificate with an accompanying private-key signature.

    And related to this point: HardcodingserviceTokenFromClient in your prototype might not work reliably, because Kerberos servers have an anti-replay cache which will prevent the same token from being given to step() twice

    (So the client will re-use the same cached ticket for successive initializeClient() calls, but will generate new authenticators and therefore return unique AP-REQ tokens every time.)

  3. The service account does not need AD read access for this operation. The username (i.e. client Kerberos principal) is already included within the ticket – the Kerberos server does not contact the KDC to retrieve that information.

    Read access might only be needed later, if your server app decides to get additional user information by querying LDAP as a separate step. (If you plan on doing that, you might want to set env.KRB5_CLIENT_KTNAME as well.)

    When searching LDAP, keep in mind that Active Directory builds user Kerberos principals out of the sAMAccountName (+ "@" + realm),not from userPrincipalName.

On the client side, the service parameter must remain in the original format (although the documentation says the same format is needed for both the client and the server as confirmed in this test in the github repo line 43):

That's an unfortunate mismatch between GSS-API and Windows SSPI... with an unfortunate porting decision by the Nodejs module developer, on top of that.

The module was likely written with GSS-API in mind (for Linux/Unix/macOS platforms) and later ported to Windows SSPI, and one of the differences is that Unix GSS-API prefers the generic name format whereas Windows SSPI requires the Kerberos name format (and doesn't support the generic "host-based service" format at all).

Ideally the Nodejs 'kerberos' module ought to accept an additional "name type" parameter (like the underlying C API does), then it would become possible to directly pass a Kerberos principal on all platforms.

Token verification fails probably due to a format mismatch between client ticket and server principal.

That is unlikely. The generic name is always transformed to a Kerberos principal name when GSS invokes Kerberos, so it doesn't matter whether the API is provided a Kerberos principal or a GSS "host-based service" name – the result is the same either way.

(GSS service names exist because both GSS-API and Windows SSPI are really frontends to multiple mechanisms – e.g. they also implement legacy NTLM through exactly the same C API.)

I suspect it's more likely that a) you forgot to Base64-decode the token on the server side, or b) you are reusing the token and it is being rejected due to anti-replay checks.

A successful verification "consumes" the token (by caching it as already used), therefore every test attempt needs a new token to be made (although it'll be from the same ticket) – i.e. you need to call initializeClient() and get a token from .step() for every new test.

On that note:

If you're planning to use this with HTTP Negotiate – yes, every HTTP request needs a new token. But most other protocols are connection-oriented, with one token per connection.

Note however that in connection-oriented protocols you're really supposed tocontinue the step() exchange until the mechanism indicates it's complete – the server's step() actually returns another token that needs to be sent to the client and fed back into 2nd step() for mutual authentication, and so forth. (Naturally this means the context created by initializeClient has to be retained as it is stateful.)

Only HTTP by its stateless nature has to be exempted from this process (relying solely on TLS to provide server authentication), so each HTTP request has to initialize a new client context, get a token from the 1st step(), then abandon the context without completing it.

answeredSep 7 at 12:16
grawity's user avatar
Sign up to request clarification or add additional context in comments.

Comments

0

Thanks tograwity, I was able to solve this.


The Issue

The problem was caused byincorrect principal formatting and thewrong encryption type when generating thehttp.keytab.


Fix

1. Create AD user and set SPN

New-ADUser -Name "schoolieService" -SamAccountName "schoolieService" `  -AccountPassword (ConvertTo-SecureString 'SH8DXIrR2iWY' -AsPlainText -Force) `  -Enabled $truesetspn -S HTTP/schoolie-server.schooliead.local schoolieService

2. Generate keytab

ktpass /princ HTTP/[email protected] `  /mapuser[email protected] `  /pass SH8DXIrR2iWY `  /out ./http.keytab `  /ptype KRB5_NT_PRINCIPAL `  /crypto RC4-HMAC-NT

Working Client (Node.js on Windows)

Note: theservice string uses a slightly different encoding than the docs suggest, but this works.

import Kerberos from "kerberos";const service = "HTTP/[email protected]";Kerberos.initializeClient(service, {}, (err, client) => {  if (err) throw err;  client.step('', (err, token) => {    if (err) throw err;    console.log(btoa(token)); // Base64-encoded service ticket    // Send this ticket to the server  });});

Working Server (Node.js on Linux)

If you see areplay error, it means the ticket was already cached. Just use a new one.

import Kerberos from "kerberos";// Point Kerberos to the keytab fileprocess.env.KRB5_KTNAME = "/path/to/http.keytab";const serviceTokenFromClient = "base64TokenFromClient";const actualToken = btoa(serviceTokenFromClient);const kerberosServer = await Kerberos.initializeServer("[email protected]");const responseToken = await kerberosServer.step(actualToken);console.log(responseToken);if (kerberosServer.username) {  console.log(kerberosServer.username);}
answeredSep 13 at 8:34
TechTomic's user avatar

Comments

Your Answer

Sign up orlog in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

By clicking “Post Your Answer”, you agree to ourterms of service and acknowledge you have read ourprivacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.