Using GSSManager to validate a Kerberos ticket
Asked Answered
R

3

12

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==

Rupture answered 13/8, 2014 at 14:44 Comment(0)
U
30

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;
}
Unprepared answered 22/8, 2014 at 15:46 Comment(16)
Gahlee this is an awesome response. I have some questions. Do I absolutely need to setup a krb5.conf? Is there a way to do this all programmatically? I don't understand authenticate a pre-existing SPN that is bound to a Computer account or to a User account. How do I know if that's what I'm trying to do? Does IIS depend upon a keytab to perform Windows Auth? Can I just use that somehow?Rupture
Also, why the kerberos mech id "1.2.840.113554.1.2.2"? I thought this was spnego.Rupture
Sorry, yeah, that should probably be 1.2.840.113554.1.2.2Unprepared
I edited the answer to describe how to dynamically generate a JAAS config instead of using login.conf. Unfortunately there is no way to do away with krb5.conf -- the underlying C library that Java is piggybacking on top of is hard-coded to read its configuration from file, AFAIK.Unprepared
In terms of IIS and the SPN to use, I'm not sure. I haven't really used IIS over 10 years. I believe the SPN will be tied to the service account that IIS is running on. If IIS is running as LOCAL SYSTEM then I believe the SPN will be attached to the computer's account in the domain. This article seems to have some good information, but again, I know very little about IIS.Unprepared
I forgot to add, yes you absolutely must have a keytab. IIS probably has something like the equivalent somewhere internally, but I would hazard that it is probably also leveraging native windows security APIs. Java, being multiplatform can't make use of any of that. You have to perform the authentication using the standard security protocols directly.Unprepared
So I'm facing a similar problem. You say: "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." It appears that my process does not expect a response (it doesn't return it or store it), it is only concerned with the getSrcName() value. I've discovered that a token IS being generated by acceptSecContext, but I'm not exactly sure how to decode it to figure out what it is expecting to happen next. The returned token is (base64 encoded) oQcwBaADCgECDecencies
I know this is an old thread but I really want to thank you. This is the best answer I actually found in the whole internet. So I got everything working to the point I get a Checksum exception for my token (YIIGC2....) I configured everything as shown in your post and added the SPN to the USER the Tomcat is running under. I talked with our sys admin and the kerberos and ad are displaying no errors at all. The communication works. So i wonder if I have to create a service account or if the SPN need to be registered to the LDAP Object of the Server. (ex. Server1 has the entity Server1 in AD)Outbid
@DominicA. thanks for the useful informations. I have a doubt about your code snippet that generates the Krb configuration, you set the 'realm' option, but I cannot see this option in Oracle Krb5LoginModule documentation. Is this an error or 'undocumented internals'?Junket
I also checked the Krb5LoginManager code but I cannot see any realm property or any use of it. Perhaps it is well hidden.Junket
@FabianoTarlao, you are right. The realm property is not having any effect. I believe you need to specify realm via the java.security.krb5.realm system property, or, as in this case, the realm can be derived from the @ syntax in the principal name.Unprepared
I have wrote code inspired from your snippet and 'simply works' without the realm option. Currently I provide a principal with a full name (user@DOMAIN). I suppose that you are right about the domain being extracted by system properties or principal full name. RegardsJunket
/MTDOMAIN/MYDOMAIN/Manuel
Edited to fix MTDOMAIN typo and to remove useless realm property from Krb5LoginModule config.Unprepared
Thanks @DominicA. This was really useful. :-)Melanoma
Note for anyone else: You can avoid the krb5.conf if you set both realm and kdc. See: hereGabar
T
6

This is not a Kerberos ticket but a SPNEGO ticket. Your context has the wrong mechanism.

Edit: Though, you now have the correct mech, you client is sending you a NTLM token which the GSS-API is not able to process. Take the Base 64 token, decode to raw bytes and display ASCII chars. If it starts with NTLMSSP, it won't work for sure and you have broken Kerberos setup.

Edit 2: This is your ticket:

60 81 82 06 06 2B 06 01 05 05 02 A0 78 30 76 A0 30 30 2E 06  `..+..... x0v 00..
0A 2B 06 01 04 01 82 37 02 02 0A 06 09 2A 86 48 82 F7 12 01  .+....7.....*H÷..
02 02 06 09 2A 86 48 86 F7 12 01 02 02 06 0A 2B 06 01 04 01  ....*H÷......+....
82 37 02 02 1E A2 42 04 40 4E 54 4C 4D 53 53 50 00 01 00 00  7...¢B.@NTLMSSP....
00 97 B2 08 E2 0E 00 0E 00 32 00 00 00 0A 00 0A 00 28 00 00  .².â....2.......(..
00 06 01 B1 1D 00 00 00 0F 4C 41 50 54 4F 50 2D 32 34 35 4C  ...±.....LAPTOP-245L
49 46 45 41 43 43 4F 55 4E 54 4C 4C 43                       IFEACCOUNTLLC       

This is a wrapped NTLM token inside a SPNEGO token. which simply means that Kerberos has failed for some reasons, e.g.,

  • SPN not registered
  • Clockskew
  • Not allowed for Kerberos
  • Incorrect DNS records

Best option is to use Wireshark on the client to find the root cause.

Please note that Java does not support NTLM as a SPNEGO submechanism. NTLM is only supported by SSPI and Heimdal.

Tervalent answered 18/8, 2014 at 19:38 Comment(4)
@JoshC, Can you post the entire ticket?Tervalent
So, I got this ticket by watching the http traffic for a working site that is using Windows Authentication in IIS. I didn't specify my WWW-Authenticate header value. It's Negotiate. Would that make a difference? Again, I'm trying to use a working site as an example and reverse-engineer this to work with open-source tech.Rupture
@JoshC., it is not possible what you are trying. Regard of what the JGSS is able, you cannot decrypt the ticket without the credentials of the acceptor, which is the machine and not you.Tervalent
Let us continue this discussion in chat.Rupture
P
1

If the server does not have a keytab and associated key registered the KDC, you will never be able use kerberos to validate a ticket.

Getting SPNEGO to work is tricky at best and will be next to impossible without at least a cursory understanding of how kerberos works. Try reading this dialog and see if you can get a better understanding.

http://web.mit.edu/kerberos/dialogue.html

SPNEGO requires an SPN of the form HTTP/server.example.com and you'll need to tell the GSS libraries where that keytab is when you start the server.

Phenosafranine answered 19/8, 2014 at 16:18 Comment(4)
I'm running this on localhost. How would I know if it has a keytab file or where it is?Rupture
If you don't know where it is you likely don't have it. Keytabs for SPNEGO are not installed by default. You can use ktutil to look in /etc/krb5.keytab, but that's just a guess.Phenosafranine
I have a .Net site that supports Windows Authentication. When I pull the source and debug it locally, it works correctly. Now, I'm trying to do the same thing outside of .Net. Is there a "default" keytab file that the apps can share?Rupture
Sorry, not a clue. I only know about this stuff from the Unix/Apache side of things.Phenosafranine

© 2022 - 2024 — McMap. All rights reserved.