/* * JBoss, Home of Professional Open Source. * Copyright 2016 Red Hat, Inc., and individual 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.wildfly.security.auth.util; import static java.security.AccessController.doPrivileged; import static javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; import static org.wildfly.common.Assert.checkNotNullParam; import static org.wildfly.security._private.ElytronMessages.log; import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.kerberos.KerberosTicket; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.wildfly.common.function.ExceptionSupplier; import org.wildfly.security.SecurityFactory; import org.wildfly.security.auth.callback.FastUnsupportedCallbackException; import org.wildfly.security.credential.GSSKerberosCredential; /** * A {@link SecurityFactory} implementation for obtaining a {@link GSSCredential}. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public final class GSSCredentialSecurityFactory implements SecurityFactory<GSSKerberosCredential> { private static final boolean IS_IBM = System.getProperty("java.vendor").contains("IBM"); private static final String KRB5LoginModule = "com.sun.security.auth.module.Krb5LoginModule"; private static final String IBMKRB5LoginModule = "com.ibm.security.auth.module.Krb5LoginModule"; public static final Oid KERBEROS_V5; public static final Oid SPNEGO; static { try { KERBEROS_V5 = new Oid("1.2.840.113554.1.2.2"); SPNEGO = new Oid("1.3.6.1.5.5.2"); } catch (GSSException e) { throw new RuntimeException("Unable to initialise Oid", e); } } private final int minimumRemainingLifetime; private final ExceptionSupplier<GSSKerberosCredential, GeneralSecurityException> rawSupplier; private volatile GSSKerberosCredential cachedCredential; GSSCredentialSecurityFactory(final int minimumRemainingLifetime, final ExceptionSupplier<GSSKerberosCredential, GeneralSecurityException> rawSupplier) { this.minimumRemainingLifetime = minimumRemainingLifetime; this.rawSupplier = rawSupplier; } @Override public GSSKerberosCredential create() throws GeneralSecurityException { GSSKerberosCredential currentCredentialCredential = cachedCredential; GSSCredential currentCredential = currentCredentialCredential != null ? currentCredentialCredential.getGssCredential() : null; try { if (currentCredential != null && currentCredential.getRemainingLifetime() >= minimumRemainingLifetime) { log.tracef("Used cached GSSCredential [%s]", currentCredential); return currentCredentialCredential; } log.tracef("No valid cached credential, obtaining new one..."); currentCredentialCredential = rawSupplier.get(); log.tracef("Obtained GSSCredentialCredential [%s]", currentCredentialCredential); this.cachedCredential = currentCredentialCredential; return currentCredentialCredential; } catch (GSSException e) { throw new GeneralSecurityException(e); } } public static Builder builder() { return new Builder(); } public static class Builder { private boolean built = false; private List<Oid> mechanismOids = new ArrayList<>(); private String principal; private File keyTab; private boolean isServer; private boolean obtainKerberosTicket; private int minimumRemainingLifetime; private int requestLifetime; private boolean debug; private boolean wrapGssCredential; private Map<String, Object> options; Builder() { } /** * Set the keytab file to obtain the identity. * * @param keyTab the keytab file to obtain the identity. * @return {@code this} to allow chaining. */ public Builder setKeyTab(final File keyTab) { assertNotBuilt(); this.keyTab = keyTab; return this; } /** * Set if the credential returned from the factory is representing the server side of the connection. * * @param isServer is the credential returned from the factory is representing the server side of the connection. * @return {@code this} to allow chaining. */ public Builder setIsServer(final boolean isServer) { assertNotBuilt(); this.isServer = isServer; return this; } /** * Set if the KerberosTicket should also be obtained and associated with the Credential/ * * @param obtainKerberosTicket if the KerberosTicket should also be obtained and associated with the Credential/ * @return {@code this} to allow chaining. */ public Builder setObtainKerberosTicket(final boolean obtainKerberosTicket) { assertNotBuilt(); this.obtainKerberosTicket = obtainKerberosTicket; return this; } /** * Once the factory has been called once it will cache the resulting {@link GSSCredential}, this setting * defines how much life it must have left in seconds for it to be re-used. * * @param minimumRemainingLifetime the time in seconds of life a {@link GSSCredential} must have to be re-used. * @return {@code this} to allow chaining. */ public Builder setMinimumRemainingLifetime(final int minimumRemainingLifetime) { assertNotBuilt(); this.minimumRemainingLifetime = minimumRemainingLifetime; return this; } /** * Set the lifetime to request newly created credentials are valid for. * * @param requestLifetime the lifetime to request newly created credentials are valid for. * @return {@code this} to allow chaining. */ public Builder setRequestLifetime(final int requestLifetime) { assertNotBuilt(); this.requestLifetime = requestLifetime < 0 ? GSSCredential.INDEFINITE_LIFETIME : requestLifetime; return this; } /** * Add an {@link Oid} for a mechanism the {@link GSSCredential} should be usable with. * * @param oid the {@link Oid} for the mechanism the {@link GSSCredential} should be usable with. * @return {@code this} to allow chaining. */ public Builder addMechanismOid(final Oid oid) { assertNotBuilt(); mechanismOids.add(checkNotNullParam("oid", oid)); return this; } /** * Set the principal name for the initial authentication from the KeyTab. * * @param principal the principal name for the initial authentication from the KeyTab. * @return {@code this} to allow chaining. */ public Builder setPrincipal(final String principal) { assertNotBuilt(); this.principal = principal; return this; } /** * Set if debug logging should be enabled for the JAAS authentication portion of obtaining the {@link GSSCredential} * * @param debug if debug logging should be enabled for the JAAS authentication portion of obtaining the {@link GSSCredential} * @return {@code this} to allow chaining. */ public Builder setDebug(final boolean debug) { assertNotBuilt(); this.debug = debug; return this; } /** * Set if the constructed {@link GSSCredential} should be wrapped to prevent improper credential disposal or not. * * @param value {@code true} if the constructed {@link GSSCredential} should be wrapped; {@code false} otherwise. * @return {@code this} to allow chaining. */ public Builder setWrapGssCredential(final boolean value) { assertNotBuilt(); this.wrapGssCredential = value; return this; } /** * Set other configuration options for {@code Krb5LoginModule} * * @param options the configuration options which will be appended to options passed into {@code Krb5LoginModule} * @return {@code this} to allow chaining. */ public Builder setOptions(final Map<String, Object> options) { assertNotBuilt(); this.options = options; return this; } public SecurityFactory<GSSKerberosCredential> build() throws IOException { assertNotBuilt(); final Configuration configuration = createConfiguration(); built = true; return new GSSCredentialSecurityFactory(minimumRemainingLifetime > 0 ? minimumRemainingLifetime : 0, () -> createGSSCredential(configuration)); } private GSSKerberosCredential createGSSCredential(Configuration configuration) throws GeneralSecurityException { final Subject subject = new Subject(); try { final LoginContext lc = new LoginContext("KDC", subject, (c) -> { throw new FastUnsupportedCallbackException(c[0]); }, configuration); log.tracef("Logging in using LoginContext and subject [%s]", subject); lc.login(); log.tracef("Logging in using LoginContext and subject [%s] succeed", subject); final KerberosTicket kerberosTicket; if (obtainKerberosTicket) { Set<KerberosTicket> kerberosTickets = doPrivileged((PrivilegedAction<Set<KerberosTicket>>) () -> subject.getPrivateCredentials(KerberosTicket.class)); if (kerberosTickets.size() > 1) { throw log.tooManyKerberosTicketsFound(); } kerberosTicket = kerberosTickets.size() == 1 ? kerberosTickets.iterator().next() : null; } else { kerberosTicket = null; } final GSSManager manager = GSSManager.getInstance(); return Subject.doAs(subject, (PrivilegedExceptionAction<GSSKerberosCredential>) () -> { Set<KerberosPrincipal> principals = subject.getPrincipals(KerberosPrincipal.class); if (principals.size() < 1) { throw log.noKerberosPrincipalsFound(); } else if (principals.size() > 1) { throw log.tooManyKerberosPrincipalsFound(); } KerberosPrincipal principal = principals.iterator().next(); log.tracef("Creating GSSName for Principal '%s'", principal); GSSName name = manager.createName(principal.getName(), GSSName.NT_USER_NAME, KERBEROS_V5); if (wrapGssCredential) { return new GSSKerberosCredential(wrapCredential(manager.createCredential(name, requestLifetime, mechanismOids.toArray(new Oid[mechanismOids.size()]), isServer ? GSSCredential.ACCEPT_ONLY : GSSCredential.INITIATE_ONLY)), kerberosTicket); } return new GSSKerberosCredential(manager.createCredential(name, requestLifetime, mechanismOids.toArray(new Oid[mechanismOids.size()]), isServer ? GSSCredential.ACCEPT_ONLY : GSSCredential.INITIATE_ONLY), kerberosTicket); }); } catch (LoginException e) { throw log.unableToPerformInitialLogin(e); } catch (PrivilegedActionException e) { if (e.getCause() instanceof GeneralSecurityException) { throw (GeneralSecurityException) e.getCause(); } throw new GeneralSecurityException(e.getCause()); } } private Configuration createConfiguration() throws IOException { Map<String, Object> options = new HashMap<>(); if (debug) { options.put("debug", "true"); } options.put("principal", principal); if (IS_IBM) { options.put("noAddress", "true"); options.put("credsType", (isServer && !obtainKerberosTicket) ? "acceptor" : "initiator"); options.put("useKeytab", keyTab.toURI().toURL().toString()); } else { options.put("storeKey", "true"); options.put("useKeyTab", "true"); options.put("keyTab", keyTab.getAbsolutePath()); options.put("isInitiator", (isServer && !obtainKerberosTicket) ? "false" : "true"); } if (this.options != null) { options.putAll(this.options); } log.tracef("Created LoginContext configuration: %s", options.toString()); final AppConfigurationEntry[] aceArray = new AppConfigurationEntry[] { new AppConfigurationEntry(IS_IBM ? IBMKRB5LoginModule : KRB5LoginModule, REQUIRED, options) }; return new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { assert "KDC".equals(name); return aceArray; } }; } private void assertNotBuilt() { if (built) { throw log.builderAlreadyBuilt(); } } } private static GSSCredential wrapCredential(final GSSCredential credential) { return new GSSCredential() { @Override public int getUsage(Oid mech) throws GSSException { return credential.getUsage(mech); } @Override public int getUsage() throws GSSException { return credential.getUsage(); } @Override public int getRemainingLifetime() throws GSSException { return credential.getRemainingLifetime(); } @Override public int getRemainingInitLifetime(Oid mech) throws GSSException { return credential.getRemainingInitLifetime(mech); } @Override public int getRemainingAcceptLifetime(Oid mech) throws GSSException { return credential.getRemainingAcceptLifetime(mech); } @Override public GSSName getName(Oid mech) throws GSSException { return credential.getName(mech); } @Override public GSSName getName() throws GSSException { return credential.getName(); } @Override public Oid[] getMechs() throws GSSException { return credential.getMechs(); } @Override public void dispose() throws GSSException { // Prevent disposal of our credential. } @Override public void add(GSSName name, int initLifetime, int acceptLifetime, Oid mech, int usage) throws GSSException { credential.add(name, initLifetime, acceptLifetime, mech, usage); } }; } }