/* * 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; import org.jboss.logging.Logger; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.credential.CredentialAuthentication; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputUpdater; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserManager; import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserLookupProvider; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class KerberosFederationProvider implements UserStorageProvider, UserLookupProvider, CredentialInputValidator, CredentialInputUpdater, CredentialAuthentication, ImportedUserValidation { private static final Logger logger = Logger.getLogger(KerberosFederationProvider.class); public static final String KERBEROS_PRINCIPAL = "KERBEROS_PRINCIPAL"; protected KeycloakSession session; protected UserStorageProviderModel model; protected KerberosConfig kerberosConfig; protected KerberosFederationProviderFactory factory; public KerberosFederationProvider(KeycloakSession session, UserStorageProviderModel model, KerberosFederationProviderFactory factory) { this.session = session; this.model = model; this.kerberosConfig = new KerberosConfig(model); this.factory = factory; } @Override public UserModel validate(RealmModel realm, UserModel user) { if (!isValid(realm, user)) { return null; } if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { return new ReadOnlyKerberosUserModelDelegate(user, this); } else { return user; } } @Override public UserModel getUserByUsername(String username, RealmModel realm) { KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); if (authenticator.isUserAvailable(username)) { // Case when method was called with username including kerberos realm like john@REALM.ORG . Authenticator already checked that kerberos realm was correct if (username.contains("@")) { username = username.split("@")[0]; } return findOrCreateAuthenticatedUser(realm, username); } else { return null; } } @Override public UserModel getUserByEmail(String email, RealmModel realm) { return null; } @Override public UserModel getUserById(String id, RealmModel realm) { return null; } @Override public void preRemove(RealmModel realm) { } @Override public void preRemove(RealmModel realm, RoleModel role) { } @Override public void preRemove(RealmModel realm, GroupModel group) { } public boolean isValid(RealmModel realm, UserModel local) { // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm(); return kerberosPrincipal.equalsIgnoreCase(local.getFirstAttribute(KERBEROS_PRINCIPAL)); } @Override public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { if (!(input instanceof UserCredentialModel) || !CredentialModel.PASSWORD.equals(input.getType())) return false; if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { throw new ReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server"); } return false; } @Override public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { } @Override public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) { return Collections.EMPTY_SET; } @Override public boolean supportsCredentialType(String credentialType) { return credentialType.equals(CredentialModel.KERBEROS) || (kerberosConfig.isAllowPasswordAuthentication() && credentialType.equals(CredentialModel.PASSWORD)); } @Override public boolean supportsCredentialAuthenticationFor(String type) { return CredentialModel.KERBEROS.equals(type); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return supportsCredentialType(credentialType); } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { if (!(input instanceof UserCredentialModel)) return false; if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) { return validPassword(user.getUsername(), ((UserCredentialModel)input).getValue()); } else { return false; // invalid cred type } } protected boolean validPassword(String username, String password) { if (kerberosConfig.isAllowPasswordAuthentication()) { KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); return authenticator.validUser(username, password); } else { return false; } } @Override public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput input) { if (!(input instanceof UserCredentialModel)) return null; UserCredentialModel credential = (UserCredentialModel)input; if (credential.getType().equals(UserCredentialModel.KERBEROS)) { String spnegoToken = credential.getValue(); SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); spnegoAuthenticator.authenticate(); Map<String, String> state = new HashMap<String, String>(); if (spnegoAuthenticator.isAuthenticated()) { String username = spnegoAuthenticator.getAuthenticatedUsername(); UserModel user = findOrCreateAuthenticatedUser(realm, username); if (user == null) { return CredentialValidationOutput.failed(); } else { String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential(); if (delegationCredential != null) { state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential); } return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); } } else { state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken()); return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); } } else { return null; } } @Override public void close() { } /** * Called after successful authentication * * @param realm realm * @param username username without realm prefix * @return user if found or successfully created. Null if user with same username already exists, but is not linked to this provider */ protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) { UserModel user = session.userLocalStorage().getUserByUsername(username, realm); if (user != null) { user = session.users().getUserById(user.getId(), realm); // make sure we get a cached instance logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage"); if (!model.getId().equals(user.getFederationLink())) { logger.warn("User with username " + username + " already exists, but is not linked to provider [" + model.getName() + "]"); return null; } else { UserModel proxied = validate(realm, user); if (proxied != null) { return proxied; } else { logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getName() + "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getFirstAttribute(KERBEROS_PRINCIPAL)); logger.warn("Will re-create user"); new UserManager(session).removeUser(realm, user, session.userLocalStorage()); } } } logger.debug("Kerberos authenticated user " + username + " not in Keycloak storage. Creating him"); return importUserToKeycloak(realm, username); } protected UserModel importUserToKeycloak(RealmModel realm, String username) { // Just guessing email from kerberos realm String email = username + "@" + kerberosConfig.getKerberosRealm().toLowerCase(); logger.debugf("Creating kerberos user: %s, email: %s to local Keycloak storage", username, email); UserModel user = session.userLocalStorage().addUser(realm, username); user.setEnabled(true); user.setEmail(email); user.setFederationLink(model.getId()); user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm()); if (kerberosConfig.isUpdateProfileFirstLogin()) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); } return validate(realm, user); } }