/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.federation.kerberos.impl;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import org.jboss.logging.Logger;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.KerberosSerializationUtils;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosTicket;
import java.io.IOException;
import java.security.PrivilegedExceptionAction;
import java.util.Iterator;
import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SPNEGOAuthenticator {
private static final Logger log = Logger.getLogger(SPNEGOAuthenticator.class);
private final KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator;
private final String spnegoToken;
private final CommonKerberosConfig kerberosConfig;
private boolean authenticated = false;
private String authenticatedKerberosPrincipal = null;
private GSSCredential delegationCredential;
private KerberosTicket kerberosTicket;
private String responseToken = null;
public SPNEGOAuthenticator(CommonKerberosConfig kerberosConfig, KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator, String spnegoToken) {
this.kerberosConfig = kerberosConfig;
this.kerberosSubjectAuthenticator = kerberosSubjectAuthenticator;
this.spnegoToken = spnegoToken;
}
public void authenticate() {
if (log.isTraceEnabled()) {
log.trace("SPNEGO Login with token: " + spnegoToken);
}
try {
Subject serverSubject = kerberosSubjectAuthenticator.authenticateServerSubject();
authenticated = Subject.doAs(serverSubject, new AcceptSecContext());
// kerberosTicketis available in IBM JDK in case that GSSContext supports delegated credentials
Set<KerberosTicket> kerberosTickets = serverSubject.getPrivateCredentials(KerberosTicket.class);
Iterator<KerberosTicket> iterator = kerberosTickets.iterator();
if (iterator.hasNext()) {
kerberosTicket = iterator.next();
}
} catch (Exception e) {
log.warn("SPNEGO login failed", e);
} finally {
kerberosSubjectAuthenticator.logoutServerSubject();
}
}
public boolean isAuthenticated() {
return authenticated;
}
public String getResponseToken() {
return responseToken;
}
public String getSerializedDelegationCredential() {
if (delegationCredential == null) {
if (log.isTraceEnabled()) {
log.trace("No delegation credential available.");
}
return null;
}
try {
if (log.isTraceEnabled()) {
log.trace("Serializing credential " + delegationCredential);
}
return KerberosSerializationUtils.serializeCredential(kerberosTicket, delegationCredential);
} catch (KerberosSerializationUtils.KerberosSerializationException kse) {
log.warn("Couldn't serialize credential: " + delegationCredential, kse);
return null;
}
}
/**
* @return username to be used in Keycloak. Username is authenticated kerberos principal without realm name
*/
public String getAuthenticatedUsername() {
String[] tokens = authenticatedKerberosPrincipal.split("@");
String username = tokens[0];
if (!tokens[1].equalsIgnoreCase(kerberosConfig.getKerberosRealm())) {
throw new IllegalStateException("Invalid kerberos realm. Realm from the ticket: " + tokens[1] + ", configured realm: " + kerberosConfig.getKerberosRealm());
}
return username;
}
private class AcceptSecContext implements PrivilegedExceptionAction<Boolean> {
@Override
public Boolean run() throws Exception {
GSSContext gssContext = null;
try {
if (log.isTraceEnabled()) {
log.trace("Going to establish security context");
}
gssContext = establishContext();
logAuthDetails(gssContext);
if (gssContext.isEstablished()) {
if (gssContext.getSrcName() == null) {
log.warn("GSS Context accepted, but no context initiator recognized. Check your kerberos configuration and reverse DNS lookup configuration");
return false;
}
authenticatedKerberosPrincipal = gssContext.getSrcName().toString();
if (gssContext.getCredDelegState()) {
delegationCredential = gssContext.getDelegCred();
}
return true;
} else {
return false;
}
} finally {
if (gssContext != null) {
gssContext.dispose();
}
}
}
}
protected GSSContext establishContext() throws GSSException, IOException {
GSSManager manager = GSSManager.getInstance();
Oid[] supportedMechs = new Oid[] { KerberosConstants.KRB5_OID, KerberosConstants.SPNEGO_OID };
GSSCredential gssCredential = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, supportedMechs, GSSCredential.ACCEPT_ONLY);
GSSContext gssContext = manager.createContext(gssCredential);
byte[] inputToken = Base64.decode(spnegoToken);
byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length);
responseToken = Base64.encodeBytes(respToken);
return gssContext;
}
protected void logAuthDetails(GSSContext gssContext) throws GSSException {
if (log.isDebugEnabled()) {
String message = new StringBuilder("SPNEGO Security context accepted with token: " + responseToken)
.append(", established: ").append(gssContext.isEstablished())
.append(", credDelegState: ").append(gssContext.getCredDelegState())
.append(", mutualAuthState: ").append(gssContext.getMutualAuthState())
.append(", lifetime: ").append(gssContext.getLifetime())
.append(", confState: ").append(gssContext.getConfState())
.append(", integState: ").append(gssContext.getIntegState())
.append(", srcName: ").append(gssContext.getSrcName())
.append(", targName: ").append(gssContext.getTargName())
.toString();
log.debug(message);
}
}
}