Using GSSManager to validate a Kerberos ticket

Josh C. picture Josh C. · Aug 13, 2014 · Viewed 12.8k times · Source

I have the following code:

public static void main(String args[]){
    try {
        //String ticket = "Negotiate YIGCBg...==";
        //byte[] kerberosTicket = ticket.getBytes();
        byte[] kerberosTicket = Base64.decode("YIGCBg...==");
        GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
        context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

Of course it fails. Here's the exception:

GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)

I don't know what I'm supposed to do to solve this. Honestly, I don't really understand Kerberos.

I got this ticket by sending a 401 with the appropriate header WWW-Authenticate with 'Negotiate' as the value. The browser immediately issued the same request again with an authorization header containing this ticket.

I was hoping I could validate the ticket and determine who the user is.

Do I need a keytab file? If so, what credentials would I run this under? I'm trying to use the Kerberos ticket for auth for a web-site. Would the credentials be the credentials from IIS?

What am I missing?


Update 1 From Michael-O's reply, I did a bit more googling and found this article, which led me to this article.

On table 3, I found 1.3.6.1.5.5.2 SPNEGO.

I have now added that to my credentials following the example from the first article. Here's my code:

public static void main(String args[]){
    try {            
        Oid mechOid = new Oid("1.3.6.1.5.5.2");

        GSSManager manager = GSSManager.getInstance();

        GSSCredential myCred = manager.createCredential(null,
                GSSCredential.DEFAULT_LIFETIME,
                mechOid,
                GSSCredential.ACCEPT_ONLY);

        GSSContext context = manager.createContext(myCred);

        byte[] ticket = Base64.decode("YIGCBg...==");
        context.acceptSecContext(ticket, 0, ticket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

But now the code is failing on createCredential with this error:

GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos credentails)

Here's the entire ticket: YIGCBgYrBgEFBQKgeDB2oDAwLgYKKwYBBAGCNwICCgYJKoZIgvcSAQICBgkqhkiG9xIBAgIGCisGAQQBgjcCAh6iQgRATlRMTVNTUAABAAAAl7II4g4ADgAyAAAACgAKACgAAAAGAbEdAAAAD0xBUFRPUC0yNDVMSUZFQUNDT1VOVExMQw==

Answer

Dominic A. picture Dominic A. · Aug 22, 2014

Validating an SPNEGO ticket from Java is a somewhat convoluted process. Here's a brief overview but bear in mind that the process can have tons of pitfalls. You really need to understand how Active Directory, Kerberos, SPNEGO, and JAAS all operate to successfully diagnose problems.

Before you start, make sure you know your kerberos realm name for your windows domain. For the purposes of this answer I'll assume it's MYDOMAIN. You can obtain the realm name by running echo %userdnsdomain% from a cmd window. Note that kerberos is case sensitive and the realm is almost always ALL CAPS.

Step 1 - Obtain a Kerberos Keytab

In order for a kerberos client to access a service, it requests a ticket for the Service Principal Name [SPN] that represents that service. SPNs are generally derived from the machine name and the type of service being accessed (e.g. HTTP/www.my-domain.com). In order to validate a kerberos ticket for a particular SPN, you must have a keytab file that contains a shared secret known to both the Kerberos Domain Controller [KDC] Ticket Granting Ticket [TGT] service and the service provider (you).

In terms of Active Directory, the KDC is the Domain Controller, and the shared secret is just the plain text password of the account that owns the SPN. A SPN may be owned by either a Computer or a User object within the AD.

The easiest way to setup a SPN in AD if you are defining a service is to setup a user-based SPN like so:

  1. Create an unpriviledged service account in AD whose password doesn't expire e.g. SVC_HTTP_MYSERVER with password ReallyLongRandomPass
  2. Bind the service SPN to the account using the windows setspn utility. Best practice is to define multiple SPNs for both the short name and the FQDN of the host:

    setspn -U -S HTTP/myserver@MYDOMAIN SVC_HTTP_MYSERVER
    setspn -U -S HTTP/myserver.my-domain.com@MYDOMAIN SVC_HTTP_MYSERVER
    
  3. Generate a keytab for the account using Java's ktab utility.

    ktab -k FILE:http_myserver.ktab -a HTTP/myserver@MYDOMAIN ReallyLongRandomPass
    ktab -k FILE:http_myserver.ktab -a HTTP/myserver.my-domain.com@MYDOMAIN ReallyLongRandomPass
    

If you are trying to authenticate a pre-existing SPN that is bound to a Computer account or to a User account you do not control, the above will not work. You will need to extract the keytab from ActiveDirectory itself. The Wireshark Kerberos Page has some good pointers for this.

Step 2 - Setup your krb5.conf

In %JAVA_HOME%/jre/lib/security create a krb5.conf that describes your domain. Make sure the realm you define here matches what you setup for your SPN. If you don't put the file in the JVM directory, you can point to it by setting -Djava.security.krb5.conf=C:\path\to\krb5.conf on the command line.

Example:

[libdefaults]
  default_realm = MYDOMAIN

[realms]
  MYDOMAIN = {
    kdc = dc1.my-domain.com
    default_domain = my-domain.com
  }

[domain_realm]
  .my-domain.com = MYDOMAIN
  my-domain.com = MYDOMAIN

Step 3 - Setup JAAS login.conf

Your JAAS login.conf should define a login configuration that sets up the Krb5LoginModule as a acceptor. Here's an example that assumes that the keytab we created above is in C:\http_myserver.ktab. Point to the JASS config file by setting -Djava.security.auth.login.config=C:\path\to\login.conf on the command line.

http_myserver_mydomain {
  com.sun.security.auth.module.Krb5LoginModule required
  principal="HTTP/myserver.my-domain.com@MYDOMAIN"
  doNotPrompt="true" 
  useKeyTab="true" 
  keyTab="C:/http_myserver.ktab"
  storeKey="true"
  isInitiator="false";
};

Alternatively, you can generate a JAAS config at runtime like so:

public static Configuration getJaasKrb5TicketCfg(
    final String principal, final String realm, final File keytab) {
  return new Configuration() {
    @Override
    public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
      Map<String, String> options = new HashMap<String, String>();
      options.put("principal",    principal);
      options.put("keyTab",       keytab.getAbsolutePath());
      options.put("doNotPrompt", "true");
      options.put("useKeyTab",   "true");
      options.put("storeKey",    "true");
      options.put("isInitiator", "false");

      return new AppConfigurationEntry[] {
        new AppConfigurationEntry(
          "com.sun.security.auth.module.Krb5LoginModule",
          LoginModuleControlFlag.REQUIRED, options)
      };
    }
  };
}

You would create a LoginContext for this configuration like so:

LoginContext ctx = new LoginContext("doesn't matter", subject, null, 
  getJaasKrbValidationCfg("HTTP/myserver.my-domain.com@MYDOMAIN", "MYDOMAIN", 
    new File("C:/path/to/my.ktab")));

Step 4 - Accepting the ticket

This is a little off-the-cuff, but the general idea is to define a PriviledgedAction that performs the SPNEGO protocol using the ticket. Note that this example does not check that SPNEGO protocol is complete. For example if the client requested server authentication, you would need to return the token generated by acceptSecContext() in the authentication header in the HTTP response.

public class Krb5TicketValidateAction implements PrivilegedExceptionAction<String> {
  public Krb5TicketValidateAction(byte[] ticket, String spn) {
    this.ticket = ticket;
    this.spn = spn;
  }

  @Override
  public String run() throws Exception {
    final Oid spnegoOid = new Oid("1.3.6.1.5.5.2");

    GSSManager gssmgr = GSSManager.getInstance();

    // tell the GSSManager the Kerberos name of the service
    GSSName serviceName = gssmgr.createName(this.spn, GSSName.NT_USER_NAME);

    // get the service's credentials. note that this run() method was called by Subject.doAs(),
    // so the service's credentials (Service Principal Name and password) are already 
    // available in the Subject
    GSSCredential serviceCredentials = gssmgr.createCredential(serviceName,
      GSSCredential.INDEFINITE_LIFETIME, spnegoOid, GSSCredential.ACCEPT_ONLY);

    // create a security context for decrypting the service ticket
    GSSContext gssContext = gssmgr.createContext(serviceCredentials);

    // decrypt the service ticket
    System.out.println("Entering accpetSecContext...");
    gssContext.acceptSecContext(this.ticket, 0, this.ticket.length);

    // get the client name from the decrypted service ticket
    // note that Active Directory created the service ticket, so we can trust it
    String clientName = gssContext.getSrcName().toString();

    // clean up the context
    gssContext.dispose();

    // return the authenticated client name
    return clientName;
  }

  private final byte[] ticket;
  private final String spn;
}

Then to authenticate the ticket, you would do something like the following. Assume that ticket contains the already-base-64-decoded ticket from the authentication header. The spn should be derived from the Host header in the HTTP request if the format of HTTP/<HOST>@<REALM>. E.g. if the Host header was myserver.my-domain.com then spn should be HTTP/myserver.my-domain.com@MYDOMAIN.

public boolean isTicketValid(String spn, byte[] ticket) {
  LoginContext ctx = null;
  try {
    // this is the name from login.conf.  This could also be a parameter
    String ctxName = "http_myserver_mydomain";

    // define the principal who will validate the ticket
    Principal principal = new KerberosPrincipal(spn, KerberosPrincipal.KRB_NT_SRV_INST);
    Set<Principal> principals = new HashSet<Principal>();
    principals.add(principal);

    // define the subject to execute our secure action as
    Subject subject = new Subject(false, principals, new HashSet<Object>(), 
      new HashSet<Object>());

    // login the subject
    ctx = new LoginContext("http_myserver_mydomain", subject);
    ctx.login();

    // create a validator for the ticket and execute it
    Krb5TicketValidateAction validateAction = new Krb5TicketValidateAction(ticket, spn);
    String username = Subject.doAs(subject, validateAction);
    System.out.println("Validated service ticket for user " + username 
      + " to access service " + spn );
    return true;
  } catch(PriviledgedActionException e ) {
     System.out.println("Invalid ticket for " + spn + ": " + e);
  } catch(LoginException e) {
    System.out.println("Error creating validation LoginContext for " 
      + spn + ": " + e);
  } finally {
    try {
      if(ctx!=null) { ctx.logout(); }
    } catch(LoginException e) { /* noop */ }
  }

  return false;
}