Functional test for Kerberos Ticket Validation
Asked Answered
C

1

6

I have written some code to validate a client's kerberos ticket on my server. I have also written unit tests for my classes. The unit tests are written by mocking the calls to the GSS library classes. This does not give me enough confidence though since the actual GSS calls are mocked.

From my research so far, I have gathered that in order for me to validate the client's token I'll need to decrypt it with the shared key I have with KDC, which I can get from the keytab file. So in order to perform the validation, I need two things ( Stand to be corrected ) :

  1. The client's token
  2. Keytab file on the server

Now if I have these files in my classpath, can I perform an actual validation of the token, without any mock calls ? Are there any technical challenges in doing so? If yes then what are they ?

Update 1 :

It seems I needed to set some system properties as well so that the GSS libraries pick up the correct realm, kdc etc. So in essence we need 3 things :

  1. A kerberos ticket
  2. A keytab file
  3. The system properties that correspond to the keytab file and the ticket.

With this, I seem be able to get a test working end to end, with validation, but only for 5 minutes. :)

The situation is, If I pick up a kerberos token freshly generated by KDC and put it in my test, the test runs successfully but starts failing after 5 minutes with the exception "Clock skew too great". I changed the kerberos policy on the KDC to generate a never expiring ticket, but the error persists. The silver lining here is that now I have a proof of concept that the approach works.

The problem boils down to getting past the "Clock skew too great" error.

Update 2 :

The clock skew value could be modified by specifying it in the krb.conf file. That's another system property I needed to set. With this the test now works end to end. Writing an answer now.


Stack trace for clock skew error:

Caused by: java.security.PrivilegedActionException: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:422)
    at com.example.vidm.eks.request.KerberosTokenValidator.getPrincipalUserName(KerberosTokenValidator.java:91)
    at com.example.vidm.eks.request.KerberosTokenValidator.lambda$validateToken$0(KerberosTokenValidator.java:80)
    ... 7 more
Caused by: GSSException: Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))
    at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:856)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
    at sun.security.jgss.spnego.SpNegoContext.GSS_acceptSecContext(SpNegoContext.java:906)
    at sun.security.jgss.spnego.SpNegoContext.acceptSecContext(SpNegoContext.java:556)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:342)
    at sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:285)
    at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:47)
    at com.example.vidm.eks.krb.KerberosValidateAction.run(KerberosValidateAction.java:22)
    ... 11 more
Caused by: KrbException: Clock skew too great (37)
    at sun.security.krb5.KrbApReq.authenticate(KrbApReq.java:302)
    at sun.security.krb5.KrbApReq.<init>(KrbApReq.java:149)
    at sun.security.jgss.krb5.InitSecContextToken.<init>(InitSecContextToken.java:108)
    at sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:829)
    ... 19 more

My functional test code :

public class KerberosTokenValidatorTest extends AbstractUnitTestBase {

  public static final String NO_PRINCIPAL = null;
  private String kerberosTicket;
  public static final String USERNAME = "username";
  private static final String REALM = "EXAMPLE.COM";
  private static final String PRINCIPAL = USERNAME + "@" + REALM;

  @BeforeClass
  public void beforeClass(){
    System.setProperty("java.security.krb5.kdc", "host/hw-99402.example.com");
    System.setProperty("java.security.krb5.realm", "EXAMPLE.COM");
    System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
    String confFile = String.format("/tmp/%s", RandomStringUtils.random(10));
    try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("testkrb.conf")) {
      Files.copy(is, Paths.get(confFile));
    } catch (IOException e) {
      // An error occurred copying the resource
    }
    System.setProperty("java.security.krb5.conf", confFile);
  }

  @Test
  public void myTest() throws IOException, GSSException, ExecutionException, InterruptedException {
    KerberosTokenValidator kerberosTokenValidator = new KerberosTokenValidator();
    String kticket = FileSystemUtils.loadClasspathResourceAsString("kerberosticket");
    kerberosTokenValidator.validateToken(kticket, "hw-99402.example.com", "userPrincipalName").get();
  }

}

My validation code :

private String getPrincipalUserName(String token1, String serverName) throws LoginException, PrivilegedActionException {
  javax.security.auth.Subject serviceSubject = getServiceSubject(serverName);
  byte[] token = base64Decoder.decode(token1);
  KerberosTicketValidation ticketValidation = javax.security.auth.Subject.doAs(serviceSubject, new KerberosValidateAction(token));
  String kdcPrincipal = ticketValidation.getUsername();
  if (StringUtils.isBlank(kdcPrincipal)) {
    throw new LoginException("KDC principal is blank after ticket validation");
  }
  return kdcPrincipal;
}

private javax.security.auth.Subject getServiceSubject(String serverName) throws LoginException {
  String servicePrincipal = SERVICE_PRINCIPAL_SERVICE + "/" + serverName;
  final Set<Principal> princ = new HashSet<>(1);
  princ.add(new KerberosPrincipal(servicePrincipal));
  javax.security.auth.Subject sub = new javax.security.auth.Subject(false, princ, Collections.emptySet(), Collections.emptySet());
  KerberosConfig kerberosConfig = new KerberosConfig(KEYTAB_PATH, servicePrincipal);
  LoginContext lc = new LoginContext("", sub, null, kerberosConfig);
  lc.login();
  return lc.getSubject();
}

My Unit test :

@BeforeMethod
public void setup() throws Exception {
  reset(mockGSSContext, mockGSSManager, mockGSSName);
  mockGSSManager();
}

@InjectMocks
private KerberosTokenValidator kerberosTokenValidator;

@Mock protected GSSManager mockGSSManager;
@Mock protected GSSContext mockGSSContext;
@Mock protected GSSName mockGSSName;

@Test
public void canValidateKerberosToken() throws Throwable {
  when(mockGSSName.toString()).thenReturn(PRINCIPAL);
  Subject subject = blockAndThrow(kerberosTokenValidator.validateToken(kerberosTicket, "hw-99402.vidmlabs.com", "sAMAccountName"));
  Assert.assertEquals(subject.getNameId(), USERNAME);
}

private void mockGSSManager() throws Exception {
    when(mockGSSManager.createContext((GSSCredential) null)).thenReturn(mockGSSContext);
    when(mockGSSContext.isEstablished()).thenReturn(true);
    when(mockGSSContext.acceptSecContext(any(byte[].class), anyInt(), anyInt())).thenReturn(null);
    when(mockGSSContext.getSrcName()).thenReturn(mockGSSName);
    KerberosValidateAction.setGssManager(mockGSSManager);

}

KerberosValidateAction :

public class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
  private static GSSManager gssManager = GSSManager.getInstance();

  private byte[] kerberosTicket;
  private GSSCredential serviceCredentials;

  public KerberosValidateAction(byte[] kerberosTicket) {
    this(kerberosTicket, null);
  }

  public KerberosValidateAction(byte[] kerberosTicket, GSSCredential serviceCredentials) {
    this.kerberosTicket = kerberosTicket;
    this.serviceCredentials = serviceCredentials;
  }

  @VisibleForTesting
  public static void setGssManager(GSSManager manager) {
    gssManager = manager;
  }

  @Override
  public KerberosTicketValidation run() throws Exception {
    GSSName gssName = null;
    GSSContext context = gssManager.createContext(serviceCredentials);
    byte[] token = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
    if (!context.isEstablished()) {
      throw new ContinueNeededException(token);
    }
    gssName = context.getSrcName();
    if (gssName == null) {
      throw new AuthenticationException("GSSContext name of the context initiator is null");
    }
    context.dispose();
    return new KerberosTicketValidation(gssName.toString());
  }
}

krb5.conf file :

[libdefaults]
    clockskew  = 999999999
Concinnity answered 26/5, 2019 at 5:59 Comment(7)
You can use apache directory server to do your test. You can find a typical example at github.com/apache/karaf/blob/master/jaas/modules/src/test/java/…Isatin
Hi @AlexandreCartapanis, thanks for pointing out. This is something that can definitely work in practice. But the whole idea of the question was to avoid having to setup a directory altogether. Again, since I already have the two entities I need for validation ( per my understanding ), using the Apache directory server will be an overkill compared to the approach I'm after.Concinnity
You have a logical error in your code. The security context is stateful, you have to maintain it until it is established. You do not!Radiosurgery
@Michael-O, could you please elaborate a little bit? I'd really want to get rid of any bugs that might be there. Is there any read up you can point me to so that I can get more context to help me understand what you are saying ? Thanks in advance.Concinnity
Start with RFC 7546.Radiosurgery
@Michael-O, I have done some reading and even discussed with my colleagues. It isn't very clear. Would you be able to pin point which line you are referring to ?Concinnity
context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length); As soon as reenter run() you are creating a new context. This is wrong. You must retain it. At best, use the PriviledgedAction to obtain the server credential only.Radiosurgery
C
0

The critical components needed to perform an end to end test are

  1. A keytab file located on the authenticating server
  2. A kerberos ticket that the client has obtained from KDC.

Apart from these the server needs to be told what the default KDC and REALM values are that correspond to these keytab and token files. These can be specified using the system properties

  • java.security.krb5.kdc
  • java.security.krb5.realm

These in place, GSS api used to authenticate the ticket will still give a clock skew error as kerberos wants to make sure that the ticket was obtained within a 5 minutes interval. There is no direct system property that can be set to modify this value. But you can have a custom krb5.conf file, specify the system property to use this file and put the value there.

  • java.security.krb5.conf

krb5.conf file :

[libdefaults]
    clockskew  = 999999999

In fact the other values ( kdc and realm ) can also be specified in this file. Using this file will also mean that it will have to be written on disk for the library to pick it up. ( Haven't found a better way for it yet ).

A more straightforward way could have been to make use of the ticket lifetime value rather than modifying the clock skew, but all my attempts failed to use the ticket lifetime to override the clockskew. The clockskew somehow seem to ignore the ticket lifetime completely. Created another question for this topic. Neverthless, for testing the clockskew scenario, one can omit the clockskew override, for other scenarios, the override can be made the default test config using a large value.

All properties to set have been updated in the question.

Concinnity answered 4/6, 2019 at 6:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.