Remove JAAS login modules

Closes #28789

Signed-off-by: Douglas Palmer <dpalmer@redhat.com>
This commit is contained in:
Douglas Palmer 2024-04-24 10:18:01 -07:00 committed by Marek Posolda
parent eae20c76bd
commit cca660067a
11 changed files with 3 additions and 908 deletions

View file

@ -46,8 +46,6 @@
org.apache.http.impl.cookie.*;version=${apache.httpcomponents.fuse.version}, org.apache.http.impl.cookie.*;version=${apache.httpcomponents.fuse.version},
org.apache.http.impl.execchain.*;version=${apache.httpcomponents.fuse.version}, org.apache.http.impl.execchain.*;version=${apache.httpcomponents.fuse.version},
org.apache.http.*;version=${apache.httpcomponents.httpcore.fuse.version}, org.apache.http.*;version=${apache.httpcomponents.httpcore.fuse.version},
org.apache.karaf.jaas.boot.principal;resolution:=optional,
org.apache.karaf.jaas.modules;resolution:=optional,
*;resolution:=optional *;resolution:=optional
</keycloak.osgi.import> </keycloak.osgi.import>
</properties> </properties>

View file

@ -1,261 +0,0 @@
/*
* 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.AdapterTokenVerifier;
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.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
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";
public static final String PROFILE_RESOURCE = "profile:";
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<>();
@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 = null;
if (keycloakConfigFile.startsWith(PROFILE_RESOURCE)) {
try {
is = new URL(keycloakConfigFile).openStream();
} catch (MalformedURLException mfue) {
throw new RuntimeException(mfue);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
} else {
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 = AdapterTokenVerifier.verifyToken(tokenString, deployment);
return postTokenVerification(tokenString, token);
}
/**
* Called after accessToken was verified (including signature, expiration etc)
*
*/
protected Auth postTokenVerification(String tokenString, AccessToken token) {
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;
}
}
}

View file

@ -1,45 +0,0 @@
/*
* 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.common.VerificationException;
/**
* Login module, which allows to authenticate Keycloak access token in environments, which rely on JAAS
* <p/>
* It expects login based on username and password where username doesn't matter and password is keycloak access token.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class BearerTokenLoginModule extends AbstractKeycloakLoginModule {
private static final Logger log = Logger.getLogger(BearerTokenLoginModule.class);
@Override
protected Auth doAuth(String username, String password) throws VerificationException {
// Should do some checking of authenticated username if it's equivalent to passed value?
return bearerAuth(password);
}
@Override
protected Logger getLogger() {
return log;
}
}

View file

@ -1,193 +0,0 @@
/*
* 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.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.util.JsonSerialization;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Login module based on Resource Owner password credentials grant from OAuth2 specs. It's supposed to be used in environments. which
* can't rely on HTTP (like SSH authentication for instance). It needs that Direct Grant is enabled on particular realm in Keycloak.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DirectAccessGrantsLoginModule extends AbstractKeycloakLoginModule {
private static final Logger log = Logger.getLogger(DirectAccessGrantsLoginModule.class);
public static final String SCOPE_OPTION = "scope";
private String refreshToken;
private String scope;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
super.initialize(subject, callbackHandler, sharedState, options);
this.scope = (String)options.get(SCOPE_OPTION);
// This is used just for logout
Iterator<RefreshTokenHolder> iterator = subject.getPrivateCredentials(RefreshTokenHolder.class).iterator();
if (iterator.hasNext()) {
refreshToken = iterator.next().refreshToken;
}
}
@Override
protected Auth doAuth(String username, String password) throws IOException, VerificationException {
return directGrantAuth(username, password);
}
@Override
protected Logger getLogger() {
return log;
}
protected Auth directGrantAuth(String username, String password) throws IOException, VerificationException {
String authServerBaseUrl = deployment.getAuthServerBaseUrl();
HttpPost post = new HttpPost(deployment.getTokenUrl());
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password));
if (scope != null) {
formparams.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
AdapterUtils.setClientCredentials(deployment, post, formparams);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpClient client = deployment.getClient();
HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
StringBuilder errorBuilder = new StringBuilder("Login failed. Invalid status: " + status);
if (entity != null) {
InputStream is = entity.getContent();
OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class);
errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError())
.append(", Error description: " + errorRep.getErrorDescription());
}
String error = errorBuilder.toString();
log.warn(error);
throw new IOException(error);
}
if (entity == null) {
throw new IOException("No Entity");
}
InputStream is = entity.getContent();
AccessTokenResponse tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
// refreshToken will be saved to privateCreds of Subject for now
refreshToken = tokenResponse.getRefreshToken();
AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenResponse.getToken(), tokenResponse.getIdToken(), deployment);
return postTokenVerification(tokenResponse.getToken(), tokens.getAccessToken());
}
@Override
public boolean commit() throws LoginException {
boolean superCommit = super.commit();
// refreshToken will be saved to privateCreds of Subject for now
if (refreshToken != null) {
RefreshTokenHolder refreshTokenHolder = new RefreshTokenHolder();
refreshTokenHolder.refreshToken = refreshToken;
subject.getPrivateCredentials().add(refreshTokenHolder);
}
return superCommit;
}
@Override
public boolean logout() throws LoginException {
if (refreshToken != null) {
try {
URI logoutUri = deployment.getLogoutUrl().clone().build();
HttpPost post = new HttpPost(logoutUri);
List<NameValuePair> formparams = new ArrayList<>();
AdapterUtils.setClientCredentials(deployment, post, formparams);
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpClient client = deployment.getClient();
HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 204) {
StringBuilder errorBuilder = new StringBuilder("Logout of refreshToken failed. Invalid status: " + status);
if (entity != null) {
InputStream is = entity.getContent();
if (status == 400) {
OAuth2ErrorRepresentation errorRep = JsonSerialization.readValue(is, OAuth2ErrorRepresentation.class);
errorBuilder.append(", OAuth2 error. Error: " + errorRep.getError())
.append(", Error description: " + errorRep.getErrorDescription());
} else {
if (is != null) is.close();
}
}
// Should do something better than warn if logout failed? Perhaps update of refresh tokens on existing subject might be supported too...
log.warn(errorBuilder.toString());
}
} catch (IOException ioe) {
log.warn(ioe);
}
}
return super.logout();
}
private static class RefreshTokenHolder implements Serializable {
private String refreshToken;
}
}

View file

@ -1,53 +0,0 @@
/*
* 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 java.io.Serializable;
import java.security.Principal;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RolePrincipal implements Principal, Serializable {
private String roleName = null;
public RolePrincipal(String roleName) {
this.roleName = roleName;
}
public boolean equals (Object p) {
if (! (p instanceof RolePrincipal))
return false;
return getName().equals(((RolePrincipal)p).getName());
}
public int hashCode () {
return getName().hashCode();
}
public String getName () {
return this.roleName;
}
public String toString ()
{
return getName();
}
}

View file

@ -1,37 +0,0 @@
[[_jaas_adapter]]
==== JAAS plugin
include::adapter-deprecation-notice.adoc[]
It's generally not needed to use JAAS for most of the applications, especially if they are HTTP based, and you should most likely choose one of our other adapters.
However, some applications and systems may still rely on pure legacy JAAS solution.
{project_name} provides two login modules to help in these situations.
The provided login modules are:
org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule::
This login module allows to authenticate with username/password from {project_name}.
It's using <<_resource_owner_password_credentials_flow,Resource Owner Password Credentials>> flow to validate if the provided
username/password is valid. It's useful for non-web based systems, which need to rely on JAAS and want to use {project_name}, but can't use the standard browser
based flows due to their non-web nature. Example of such application could be messaging or SSH.
org.keycloak.adapters.jaas.BearerTokenLoginModule::
This login module allows to authenticate with {project_name} access token passed to it through CallbackHandler as password.
It may be useful for example in case, when you have {project_name} access token from standard based authentication flow and your web application then
needs to talk to external non-web based system, which rely on JAAS. For example a messaging system.
Both modules use the following configuration properties:
keycloak-config-file::
The location of the `keycloak.json` configuration file. The configuration file can either be located on the filesystem or on the classpath. If it's located
on the classpath you need to prefix the location with `classpath:` (for example `classpath:/path/keycloak.json`).
This is _REQUIRED._
`role-principal-class`::
Configure alternative class for Role principals attached to JAAS Subject.
Default value is `org.keycloak.adapters.jaas.RolePrincipal`. Note: The class is required to have a constructor with a single `String` argument.
`scope`::
This option is only applicable to the `DirectAccessGrantsLoginModule`. The specified value will be used as the OAuth2 `scope`
parameter in the Resource Owner Password Credentials Grant request.

View file

@ -21,10 +21,6 @@ ifeval::[{project_community}==true]
include::servlet-filter-adapter.adoc[] include::servlet-filter-adapter.adoc[]
endif::[] endif::[]
ifeval::[{project_community}==true]
include::jaas.adoc[]
endif::[]
ifeval::[{project_community}==true] ifeval::[{project_community}==true]
include::adapter-context.adoc[] include::adapter-context.adoc[]
include::adapter_error_handling.adoc[] include::adapter_error_handling.adoc[]

View file

@ -14,23 +14,12 @@
<property name="hawtio.authenticationEnabled" value="true" /> <property name="hawtio.authenticationEnabled" value="true" />
<property name="hawtio.realm" value="hawtio" /> <property name="hawtio.realm" value="hawtio" />
<property name="hawtio.roles" value="admin,viewer" /> <property name="hawtio.roles" value="admin,viewer" />
<property name="hawtio.rolePrincipalClasses" value="org.keycloak.adapters.jaas.RolePrincipal" />
<property name="hawtio.keycloakEnabled" value="true" /> <property name="hawtio.keycloakEnabled" value="true" />
<property name="hawtio.keycloakClientConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio-client.json" /> <property name="hawtio.keycloakClientConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio-client.json" />
<property name="hawtio.keycloakServerConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio.json" /> <property name="hawtio.keycloakServerConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio.json" />
</system-properties> </system-properties>
</xsl:template> </xsl:template>
<xsl:template match="//*[local-name()='security-domain' and @name = 'hawtio-domain']">
<security-domain name="hawtio" cache-type="default" xmlns="urn:jboss:domain:security:1.2">
<authentication>
<login-module code="org.keycloak.adapters.jaas.BearerTokenLoginModule" flag="required">
<module-option name="keycloak-config-file" value="${{hawtio.keycloakServerConfig}}"/>
</login-module>
</authentication>
</security-domain>
</xsl:template>
<xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $keycloakNamespace)]"> <xsl:template match="//*[local-name()='subsystem' and starts-with(namespace-uri(), $keycloakNamespace)]">
<xsl:copy> <xsl:copy>
<secure-deployment name="hawtio.war" xmlns="urn:jboss:domain:keycloak:1.2"/> <secure-deployment name="hawtio.war" xmlns="urn:jboss:domain:keycloak:1.2"/>

View file

@ -1,298 +0,0 @@
/*
* Copyright 2017 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.testsuite.jaas;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URI;
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.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.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule;
import org.keycloak.adapters.jaas.BearerTokenLoginModule;
import org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule;
import org.keycloak.adapters.jaas.RolePrincipal;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
import org.keycloak.testsuite.utils.io.IOUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LoginModulesTest extends AbstractKeycloakTest {
public static final URI DIRECT_GRANT_CONFIG;
public static final URI BEARER_CONFIG;
private static final File DIRECT_GRANT_CONFIG_FILE;
private static final File BEARER_CONFIG_FILE;
static {
try {
DIRECT_GRANT_CONFIG = MethodHandles.lookup().lookupClass().getResource("/adapter-test/customer-portal/WEB-INF/keycloak.json").toURI();
BEARER_CONFIG = MethodHandles.lookup().lookupClass().getResource("/adapter-test/customer-db-audience-required/WEB-INF/keycloak.json").toURI();
DIRECT_GRANT_CONFIG_FILE = File.createTempFile("LoginModulesTest", "testDirectAccessGrantLoginModuleLoginFailed");
BEARER_CONFIG_FILE = File.createTempFile("LoginModulesTest", "testBearerLoginFailedLogin");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/adapter-test/demorealm.json"));
}
private static void enabled() {
Assume.assumeTrue(AUTH_SERVER_SSL_REQUIRED);
}
@BeforeClass
public static void createTemporaryFiles() throws Exception {
enabled();
copyContentAndReplaceAuthServerAddress(new File(DIRECT_GRANT_CONFIG), DIRECT_GRANT_CONFIG_FILE);
copyContentAndReplaceAuthServerAddress(new File(BEARER_CONFIG), BEARER_CONFIG_FILE);
}
@AfterClass
public static void removeTemporaryFiles() {
DIRECT_GRANT_CONFIG_FILE.deleteOnExit();
BEARER_CONFIG_FILE.deleteOnExit();
}
private static void copyContentAndReplaceAuthServerAddress(File input, File output) throws IOException {
try (InputStream inputStream = httpsAwareConfigurationStream(new FileInputStream(input))) {
try (FileOutputStream outputStream = new FileOutputStream(output)) {
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
outputStream.write(buffer);
}
}
}
@Before
public void generateAudienceClientScope() {
if (ApiUtil.findClientScopeByName(adminClient.realm("demo"), "customer-db-audience-required") != null) {
return;
}
// Generate audience client scope
String clientScopeId = testingClient.testing().generateAudienceClientScope("demo", "customer-db-audience-required");
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("demo"), "customer-portal");
client.addOptionalClientScope(clientScopeId);
}
@Test
public void testDirectAccessGrantLoginModuleLoginFailed() throws Exception {
LoginContext loginContext = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("bburke@redhat.com", "bad-password"),
createJaasConfigurationForDirectGrant(null));
try {
loginContext.login();
Assert.fail("Not expected to successfully login");
} catch (LoginException le) {
// Ignore
}
}
@Test
public void testDirectAccessGrantLoginModuleLoginSuccess() throws Exception {
oauth.realm("demo");
LoginContext loginContext = directGrantLogin(null);
Subject subject = loginContext.getSubject();
// Assert principals in subject
KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next();
Assert.assertEquals("bburke@redhat.com", principal.getKeycloakSecurityContext().getToken().getPreferredUsername());
assertToken(principal.getKeycloakSecurityContext().getTokenString(), true);
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
Assert.assertEquals(1, roles.size());
Assert.assertEquals("user", roles.iterator().next().getName());
// Logout and assert token not valid anymore
loginContext.logout();
assertToken(principal.getKeycloakSecurityContext().getTokenString(), false);
}
@Test
public void testBearerLoginFailedLogin() throws Exception {
oauth.realm("demo");
LoginContext directGrantCtx = directGrantLogin(null);
String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next()
.getKeycloakSecurityContext().getTokenString();
LoginContext bearerCtx = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("doesn-not-matter", accessToken),
createJaasConfigurationForBearer());
// Login should fail due insufficient audience in the token
try {
bearerCtx.login();
Assert.fail("Not expected to successfully login");
} catch (LoginException le) {
// Ignore
}
directGrantCtx.logout();
}
@Test
public void testBearerLoginSuccess() throws Exception {
oauth.realm("demo");
LoginContext directGrantCtx = directGrantLogin("customer-db-audience-required");
String accessToken = directGrantCtx.getSubject().getPrincipals(KeycloakPrincipal.class).iterator().next()
.getKeycloakSecurityContext().getTokenString();
LoginContext bearerCtx = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("doesn-not-matter", accessToken),
createJaasConfigurationForBearer());
// Login should be successful
bearerCtx.login();
// Assert subject
Subject subject = bearerCtx.getSubject();
KeycloakPrincipal principal = subject.getPrincipals(KeycloakPrincipal.class).iterator().next();
Assert.assertEquals("bburke@redhat.com", principal.getKeycloakSecurityContext().getToken().getPreferredUsername());
assertToken(principal.getKeycloakSecurityContext().getTokenString(), true);
Set<RolePrincipal> roles = subject.getPrincipals(RolePrincipal.class);
Assert.assertEquals(1, roles.size());
Assert.assertEquals("user", roles.iterator().next().getName());
// Logout
bearerCtx.logout();
directGrantCtx.logout();
}
private LoginContext directGrantLogin(String scope) throws LoginException {
LoginContext loginContext = new LoginContext("does-not-matter", null,
createJaasCallbackHandler("bburke@redhat.com", "password"),
createJaasConfigurationForDirectGrant(scope));
loginContext.login();
return loginContext;
}
private void assertToken(String accessToken, boolean expectActive) throws IOException {
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential("customer-portal", "password", accessToken);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(introspectionResponse);
Assert.assertEquals(expectActive, jsonNode.get("active").asBoolean());
}
private CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
return new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
NameCallback nameCallback = (NameCallback) callback;
nameCallback.setName(principal);
} else if (callback instanceof PasswordCallback) {
PasswordCallback passwordCallback = (PasswordCallback) callback;
passwordCallback.setPassword(password.toCharArray());
} else {
throw new UnsupportedCallbackException(callback, "Unsupported callback: " + callback.getClass().getCanonicalName());
}
}
}
};
}
private Configuration createJaasConfigurationForDirectGrant(String scope) {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, DIRECT_GRANT_CONFIG_FILE.getAbsolutePath());
if (scope != null) {
options.put(DirectAccessGrantsLoginModule.SCOPE_OPTION, scope);
}
AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(DirectAccessGrantsLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { LMConfiguration };
}
};
}
private Configuration createJaasConfigurationForBearer() {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, Object> options = new HashMap<>();
options.put(AbstractKeycloakLoginModule.KEYCLOAK_CONFIG_FILE_OPTION, BEARER_CONFIG_FILE.getAbsolutePath());
AppConfigurationEntry LMConfiguration = new AppConfigurationEntry(BearerTokenLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
return new AppConfigurationEntry[] { LMConfiguration };
}
};
}
}

View file

@ -19,7 +19,6 @@ feature,4
federation,5 federation,5
forms,5 forms,5
i18n,5 i18n,5
jaas,5
javascript,5 javascript,5
keys,4 keys,4
login,4 login,4

View file

@ -114,7 +114,7 @@ public class FuseUtils {
"feature:repo-add mvn:org.keycloak/keycloak-osgi-features/" + fuseAdapterVersion + "/xml/features; " + "feature:repo-add mvn:org.keycloak/keycloak-osgi-features/" + fuseAdapterVersion + "/xml/features; " +
"feature:repo-add mvn:org.keycloak.testsuite/fuse-example-keycloak-features/" + projectVersion + "/xml/features; " + "feature:repo-add mvn:org.keycloak.testsuite/fuse-example-keycloak-features/" + projectVersion + "/xml/features; " +
"feature:install pax-web-http-undertow; " + "feature:install pax-web-http-undertow; " +
"feature:install keycloak-jaas keycloak-pax-http-undertow; " + "feature:install keycloak-pax-http-undertow; " +
"feature:install keycloak-fuse-7.0-example", "feature:install keycloak-fuse-7.0-example",
Result.OK); Result.OK);
@ -135,7 +135,7 @@ public class FuseUtils {
"system:property -p hawtio.keycloakClientConfig ${karaf.etc}/keycloak-hawtio-client.json; " + "system:property -p hawtio.keycloakClientConfig ${karaf.etc}/keycloak-hawtio-client.json; " +
"system:property -p hawtio.keycloakServerConfig ${karaf.etc}/keycloak-bearer.json; " + "system:property -p hawtio.keycloakServerConfig ${karaf.etc}/keycloak-bearer.json; " +
"system:property -p hawtio.roles admin,manager,viewer,ssh; " + "system:property -p hawtio.roles admin,manager,viewer,ssh; " +
"system:property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal;", "system:property -p hawtio.rolePrincipalClasses org.apache.karaf.jaas.boot.principal.RolePrincipal;",
Result.EMPTY); Result.EMPTY);
// KEYCLOAK-17873 For older version of Fuse // KEYCLOAK-17873 For older version of Fuse
@ -189,7 +189,7 @@ public class FuseUtils {
"system-property -p hawtio.keycloakEnabled true; " + "system-property -p hawtio.keycloakEnabled true; " +
"system-property -p hawtio.realm keycloak; " + "system-property -p hawtio.realm keycloak; " +
"system-property -p hawtio.keycloakClientConfig file://" + appServerHome + "/etc/keycloak-hawtio-client.json; " + "system-property -p hawtio.keycloakClientConfig file://" + appServerHome + "/etc/keycloak-hawtio-client.json; " +
"system-property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal; ", "system-property -p hawtio.rolePrincipalClasses org.apache.karaf.jaas.boot.principal.RolePrincipal; ",
Result.EMPTY); Result.EMPTY);
String output = getCommandOutput(managementUser, managementPassword, "osgi:list | grep hawtio | grep web;"); String output = getCommandOutput(managementUser, managementPassword, "osgi:list | grep hawtio | grep web;");