/* * 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.adapters.jaas; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.FindFile; import org.keycloak.common.util.reflections.Reflections; import org.keycloak.representations.AccessToken; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import java.io.InputStream; import java.lang.reflect.Constructor; import java.security.Principal; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public abstract class AbstractKeycloakLoginModule implements LoginModule { public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file"; public static final String ROLE_PRINCIPAL_CLASS_OPTION = "role-principal-class"; protected Subject subject; protected CallbackHandler callbackHandler; protected Auth auth; protected KeycloakDeployment deployment; protected String rolePrincipalClass; // This is to avoid parsing keycloak.json file in each request. Key is file location, Value is parsed keycloak deployment private static ConcurrentMap<String, KeycloakDeployment> deployments = new ConcurrentHashMap<String, KeycloakDeployment>(); @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { this.subject = subject; this.callbackHandler = callbackHandler; String configFile = (String)options.get(KEYCLOAK_CONFIG_FILE_OPTION); rolePrincipalClass = (String)options.get(ROLE_PRINCIPAL_CLASS_OPTION); getLogger().debug("Declared options: " + KEYCLOAK_CONFIG_FILE_OPTION + "=" + configFile + ", " + ROLE_PRINCIPAL_CLASS_OPTION + "=" + rolePrincipalClass); if (configFile != null) { deployment = deployments.get(configFile); if (deployment == null) { // lazy init of our deployment deployment = resolveDeployment(configFile); deployments.putIfAbsent(configFile, deployment); } } } protected KeycloakDeployment resolveDeployment(String keycloakConfigFile) { try { InputStream is = FindFile.findFile(keycloakConfigFile); KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is); return kd; } catch (RuntimeException e) { getLogger().debug("Unable to find or parse file " + keycloakConfigFile + " due to " + e.getMessage(), e); throw e; } } @Override public boolean login() throws LoginException { // get username and password Callback[] callbacks = new Callback[2]; callbacks[0] = new NameCallback("username"); callbacks[1] = new PasswordCallback("password", false); try { callbackHandler.handle(callbacks); String username = ((NameCallback) callbacks[0]).getName(); char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword(); String password = new String(tmpPassword); ((PasswordCallback) callbacks[1]).clearPassword(); Auth auth = doAuth(username, password); if (auth != null) { this.auth = auth; return true; } else { return false; } } catch (UnsupportedCallbackException uce) { getLogger().warn("Error: " + uce.getCallback().toString() + " not available to gather authentication information from the user"); return false; } catch (Exception e) { LoginException le = new LoginException(e.toString()); le.initCause(e); throw le; } } @Override public boolean commit() throws LoginException { if (auth == null) { return false; } this.subject.getPrincipals().add(auth.getPrincipal()); this.subject.getPrivateCredentials().add(auth.getTokenString()); if (auth.getRoles() != null) { for (String roleName : auth.getRoles()) { Principal rolePrinc = createRolePrincipal(roleName); this.subject.getPrincipals().add(rolePrinc); } } return true; } protected Principal createRolePrincipal(String roleName) { if (rolePrincipalClass != null && rolePrincipalClass.length() > 0) { try { Class<Principal> clazz = Reflections.classForName(rolePrincipalClass, getClass().getClassLoader()); Constructor<Principal> constructor = clazz.getDeclaredConstructor(String.class); return constructor.newInstance(roleName); } catch (Exception e) { getLogger().warn("Unable to create declared roleClass " + rolePrincipalClass + " due to " + e.getMessage()); } } // Fallback to default rolePrincipal class return new RolePrincipal(roleName); } @Override public boolean abort() throws LoginException { return true; } @Override public boolean logout() throws LoginException { Set<Principal> principals = new HashSet<Principal>(subject.getPrincipals()); for (Principal principal : principals) { if (principal.getClass().equals(KeycloakPrincipal.class) || principal.getClass().equals(RolePrincipal.class)) { subject.getPrincipals().remove(principal); } } Set<Object> creds = subject.getPrivateCredentials(); for (Object cred : creds) { subject.getPrivateCredentials().remove(cred); } subject = null; callbackHandler = null; return true; } protected Auth bearerAuth(String tokenString) throws VerificationException { AccessToken token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); boolean verifyCaller; if (deployment.isUseResourceRoleMappings()) { verifyCaller = token.isVerifyCaller(deployment.getResourceName()); } else { verifyCaller = token.isVerifyCaller(); } if (verifyCaller) { throw new IllegalStateException("VerifyCaller not supported yet in login module"); } RefreshableKeycloakSecurityContext skSession = new RefreshableKeycloakSecurityContext(deployment, null, tokenString, token, null, null, null); String principalName = AdapterUtils.getPrincipalName(deployment, token); final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(principalName, skSession); final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(skSession); return new Auth(principal, roles, tokenString); } protected abstract Auth doAuth(String username, String password) throws Exception; protected abstract Logger getLogger(); public static class Auth { private final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal; private final Set<String> roles; private final String tokenString; public Auth(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal, Set<String> roles, String accessToken) { this.principal = principal; this.roles = roles; this.tokenString = accessToken; } public KeycloakPrincipal<RefreshableKeycloakSecurityContext> getPrincipal() { return principal; } public Set<String> getRoles() { return roles; } public String getTokenString() { return tokenString; } } }