Remove JAAS login modules
Closes #28789 Signed-off-by: Douglas Palmer <dpalmer@redhat.com>
This commit is contained in:
parent
eae20c76bd
commit
cca660067a
11 changed files with 3 additions and 908 deletions
|
@ -46,8 +46,6 @@
|
|||
org.apache.http.impl.cookie.*;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.karaf.jaas.boot.principal;resolution:=optional,
|
||||
org.apache.karaf.jaas.modules;resolution:=optional,
|
||||
*;resolution:=optional
|
||||
</keycloak.osgi.import>
|
||||
</properties>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
@ -21,10 +21,6 @@ ifeval::[{project_community}==true]
|
|||
include::servlet-filter-adapter.adoc[]
|
||||
endif::[]
|
||||
|
||||
ifeval::[{project_community}==true]
|
||||
include::jaas.adoc[]
|
||||
endif::[]
|
||||
|
||||
ifeval::[{project_community}==true]
|
||||
include::adapter-context.adoc[]
|
||||
include::adapter_error_handling.adoc[]
|
||||
|
|
|
@ -14,23 +14,12 @@
|
|||
<property name="hawtio.authenticationEnabled" value="true" />
|
||||
<property name="hawtio.realm" value="hawtio" />
|
||||
<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.keycloakClientConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio-client.json" />
|
||||
<property name="hawtio.keycloakServerConfig" value="${{jboss.server.config.dir}}/keycloak-hawtio.json" />
|
||||
</system-properties>
|
||||
</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:copy>
|
||||
<secure-deployment name="hawtio.war" xmlns="urn:jboss:domain:keycloak:1.2"/>
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ feature,4
|
|||
federation,5
|
||||
forms,5
|
||||
i18n,5
|
||||
jaas,5
|
||||
javascript,5
|
||||
keys,4
|
||||
login,4
|
||||
|
|
|
@ -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.testsuite/fuse-example-keycloak-features/" + projectVersion + "/xml/features; " +
|
||||
"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",
|
||||
Result.OK);
|
||||
|
||||
|
@ -135,7 +135,7 @@ public class FuseUtils {
|
|||
"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.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);
|
||||
|
||||
// KEYCLOAK-17873 For older version of Fuse
|
||||
|
@ -189,7 +189,7 @@ public class FuseUtils {
|
|||
"system-property -p hawtio.keycloakEnabled true; " +
|
||||
"system-property -p hawtio.realm keycloak; " +
|
||||
"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);
|
||||
|
||||
String output = getCommandOutput(managementUser, managementPassword, "osgi:list | grep hawtio | grep web;");
|
||||
|
|
Loading…
Reference in a new issue