Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
e14c5adf55
6 changed files with 453 additions and 259 deletions
|
@ -96,7 +96,7 @@ public class AdapterDeploymentContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void resolveRealmKey(KeycloakDeployment deployment) {
|
public void resolveRealmKey(KeycloakDeployment deployment) {
|
||||||
if (deployment.getClient() == null) {
|
if (deployment.getClient() == null) {
|
||||||
throw new RuntimeException("KeycloakDeployment was never initialized through appropriate SPIs");
|
throw new RuntimeException("KeycloakDeployment was never initialized through appropriate SPIs");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,258 +0,0 @@
|
||||||
package org.keycloak.adapters;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
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 org.keycloak.KeycloakPrincipal;
|
|
||||||
import org.keycloak.RSATokenVerifier;
|
|
||||||
import org.keycloak.VerificationException;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 must be equal to "Bearer" and password is keycloak access token.
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class BearerTokenLoginModule implements LoginModule {
|
|
||||||
|
|
||||||
private final static Logger log = Logger.getLogger("" + BearerTokenLoginModule.class);
|
|
||||||
|
|
||||||
public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file";
|
|
||||||
public static final String REALM_OPTION = "realm";
|
|
||||||
public static final String RESOURCE_OPTION = "resource";
|
|
||||||
public static final String PUBLIC_KEY_OPTION = "realm-public-key";
|
|
||||||
public static final String AUTH_SERVER_URL_OPTION = "auth-server-url";
|
|
||||||
public static final String USE_RESOURCE_ROLE_MAPPINGS_OPTION = "use-resource-role-mappings";
|
|
||||||
public static final String PRINCIPAL_ATTRIBUTE_OPTION = "principal-attribute";
|
|
||||||
|
|
||||||
private Subject subject;
|
|
||||||
private CallbackHandler callbackHandler;
|
|
||||||
private Auth auth;
|
|
||||||
|
|
||||||
private static volatile KeycloakDeployment deployment;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
|
|
||||||
this.subject = subject;
|
|
||||||
this.callbackHandler = callbackHandler;
|
|
||||||
|
|
||||||
// Static as we don't want to parse config file in each authentication
|
|
||||||
if (deployment == null) {
|
|
||||||
KeycloakDeployment kd;
|
|
||||||
String configFile = (String)options.get(KEYCLOAK_CONFIG_FILE_OPTION);
|
|
||||||
if (configFile != null) {
|
|
||||||
InputStream is = loadKeycloakConfigFile(configFile);
|
|
||||||
kd = KeycloakDeploymentBuilder.build(is);
|
|
||||||
} else {
|
|
||||||
// init everything from provided options
|
|
||||||
String realm = (String) options.get(REALM_OPTION);
|
|
||||||
if (realm == null) {
|
|
||||||
throw new IllegalArgumentException("Realm is mandatory if you didn't provide keycloak-config-file");
|
|
||||||
}
|
|
||||||
String authServerUrl = (String) options.get(AUTH_SERVER_URL_OPTION);
|
|
||||||
String publicKey = (String) options.get(PUBLIC_KEY_OPTION);
|
|
||||||
if (publicKey == null && authServerUrl == null) {
|
|
||||||
throw new IllegalArgumentException("Option " + PUBLIC_KEY_OPTION + " is mandatory if you didn't provide keycloak-config-file or auth-server-url to resolver public key");
|
|
||||||
}
|
|
||||||
String resource = (String) options.get(RESOURCE_OPTION);
|
|
||||||
String resRoleMappings = (String) options.get(USE_RESOURCE_ROLE_MAPPINGS_OPTION);
|
|
||||||
boolean useResourceRoleMappings = resRoleMappings == null ? false : Boolean.parseBoolean(resRoleMappings);
|
|
||||||
if (useResourceRoleMappings && resource == null) {
|
|
||||||
throw new IllegalArgumentException("You want resource-role-mappings, but you didn't provide resource in configuration");
|
|
||||||
}
|
|
||||||
String principalAttribute = (String) options.get(PRINCIPAL_ATTRIBUTE_OPTION);
|
|
||||||
|
|
||||||
AdapterConfig cfg = new AdapterConfig();
|
|
||||||
cfg.setRealm(realm);
|
|
||||||
cfg.setResource(resource);
|
|
||||||
cfg.setUseResourceRoleMappings(useResourceRoleMappings);
|
|
||||||
cfg.setAuthServerUrl(authServerUrl);
|
|
||||||
cfg.setBearerOnly(true);
|
|
||||||
cfg.setPrincipalAttribute(principalAttribute);
|
|
||||||
cfg.setRealmKey(publicKey);
|
|
||||||
kd = KeycloakDeploymentBuilder.build(cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kd.getRealmKey() == null) {
|
|
||||||
new AdapterDeploymentContext().resolveRealmKey(kd);
|
|
||||||
}
|
|
||||||
deployment = kd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected InputStream loadKeycloakConfigFile(String keycloakConfigFile) {
|
|
||||||
return FindFile.findFile(keycloakConfigFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 = bearerAuth(username, password);
|
|
||||||
if (auth != null) {
|
|
||||||
this.auth = auth;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (UnsupportedCallbackException uce) {
|
|
||||||
LoginException le = new LoginException("Error: " + uce.getCallback().toString()
|
|
||||||
+ " not available to gather authentication information from the user");
|
|
||||||
le.initCause(uce);
|
|
||||||
throw le;
|
|
||||||
} catch (Exception ioe) {
|
|
||||||
LoginException le = new LoginException(ioe.toString());
|
|
||||||
le.initCause(ioe);
|
|
||||||
throw le;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Auth bearerAuth(String username, String tokenString) throws VerificationException {
|
|
||||||
if (!"Bearer".equalsIgnoreCase(username)) {
|
|
||||||
log.fine("Username is expected to be bearer but is " + username + ". Ignoring login module");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealm());
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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()) {
|
|
||||||
RolePrincipal rolePrinc = new RolePrincipal(roleName);
|
|
||||||
this.subject.getPrincipals().add(rolePrinc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Might be needed if subclass wants to setup security context in some env specific way
|
|
||||||
protected Auth getAuth() {
|
|
||||||
return auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class RolePrincipal implements Principal, Serializable {
|
|
||||||
private static final long serialVersionUID = -5538962177019315447L;
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
package org.keycloak.adapters.jaas;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.RSATokenVerifier;
|
||||||
|
import org.keycloak.VerificationException;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.FindFile;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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";
|
||||||
|
|
||||||
|
protected Subject subject;
|
||||||
|
protected CallbackHandler callbackHandler;
|
||||||
|
protected Auth auth;
|
||||||
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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) {
|
||||||
|
InputStream is = FindFile.findFile(keycloakConfigFile);
|
||||||
|
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is);
|
||||||
|
if (kd.getRealmKey() == null) {
|
||||||
|
new AdapterDeploymentContext().resolveRealmKey(kd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()) {
|
||||||
|
RolePrincipal rolePrinc = new RolePrincipal(roleName);
|
||||||
|
this.subject.getPrincipals().add(rolePrinc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealm());
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.keycloak.adapters.jaas;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.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 must be equal to "Bearer" 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 {
|
||||||
|
if (!"Bearer".equalsIgnoreCase(username)) {
|
||||||
|
log.debug("Username is expected to be bearer but is " + username + ". Ignoring login module");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bearerAuth(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Logger getLogger() {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package org.keycloak.adapters.jaas;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
import javax.security.auth.Subject;
|
||||||
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
|
import javax.security.auth.login.LoginException;
|
||||||
|
|
||||||
|
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.VerificationException;
|
||||||
|
import org.keycloak.constants.ServiceUrlConstants;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.keycloak.util.KeycloakUriBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
|
||||||
|
super.initialize(subject, callbackHandler, sharedState, options);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if ("Bearer".equalsIgnoreCase(username)) {
|
||||||
|
log.debug("Username is not expected to be bearer for this login module. Ignoring login module");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return directGrantAuth(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Logger getLogger() {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Auth directGrantAuth(String username, String password) throws IOException, VerificationException {
|
||||||
|
String authServerBaseUrl = deployment.getAuthServerBaseUrl();
|
||||||
|
URI directGrantUri = KeycloakUriBuilder.fromUri(authServerBaseUrl).path(ServiceUrlConstants.TOKEN_SERVICE_DIRECT_GRANT_PATH).build(deployment.getRealm());
|
||||||
|
HttpPost post = new HttpPost(directGrantUri);
|
||||||
|
|
||||||
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
|
formparams.add(new BasicNameValuePair("username", username));
|
||||||
|
formparams.add(new BasicNameValuePair("password", password));
|
||||||
|
|
||||||
|
if (deployment.isPublicClient()) { // if client is public access type
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
|
||||||
|
} else {
|
||||||
|
String clientId = deployment.getResourceName();
|
||||||
|
String clientSecret = deployment.getResourceCredentials().get("secret");
|
||||||
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
Map<String, String> errors = (Map<String, String>) JsonSerialization.readValue(is, Map.class);
|
||||||
|
errorBuilder.append(", OAuth2 error. Error: " + errors.get(OAuth2Constants.ERROR))
|
||||||
|
.append(", Error description: " + errors.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
|
||||||
|
return bearerAuth(tokenResponse.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<NameValuePair>();
|
||||||
|
if (deployment.isPublicClient()) { // if client is public access type
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName()));
|
||||||
|
} else {
|
||||||
|
String clientId = deployment.getResourceName();
|
||||||
|
String clientSecret = deployment.getResourceCredentials().get("secret");
|
||||||
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
Map<String, String> errors = (Map<String, String>) JsonSerialization.readValue(is, Map.class);
|
||||||
|
errorBuilder.append(", OAuth2 error. Error: " + errors.get(OAuth2Constants.ERROR))
|
||||||
|
.append(", Error description: " + errors.get(OAuth2Constants.ERROR_DESCRIPTION));
|
||||||
|
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
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 static final long serialVersionUID = -5538962177019315447L;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue