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 ) :
- The client's token
- 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 :
- A kerberos ticket
- A keytab file
- 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
run()
you are creating a new context. This is wrong. You must retain it. At best, use thePriviledgedAction
to obtain the server credential only. – Radiosurgery