mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-09-10 14:11:09 +00:00
Service to obtain tokens for integrations
This commit is contained in:
@@ -55,6 +55,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="${moduleId}.AccessTokenService" class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
|
||||||
|
<property name="applicationContextManager">
|
||||||
|
<ref bean="Authentication" />
|
||||||
|
</property>
|
||||||
|
<property name="interfaces">
|
||||||
|
<list>
|
||||||
|
<value>${project.artifactId}.token.AccessTokenService</value>
|
||||||
|
</list>
|
||||||
|
</property>
|
||||||
|
<property name="defaultTarget">
|
||||||
|
<bean class="${project.artifactId}.token.NoOpAccessTokenServiceImpl" />
|
||||||
|
</property>
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="${moduleId}.authenticationListener" class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
|
<bean id="${moduleId}.authenticationListener" class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
|
||||||
<property name="applicationContextManager">
|
<property name="applicationContextManager">
|
||||||
<ref bean="Authentication" />
|
<ref bean="Authentication" />
|
||||||
|
@@ -159,6 +159,7 @@
|
|||||||
|
|
||||||
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
|
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
|
||||||
<property name="deployment" ref="keycloakDeployment" />
|
<property name="deployment" ref="keycloakDeployment" />
|
||||||
|
<property name="accessTokenService" ref="accessTokenService.impl" />
|
||||||
<property name="userName" value="${keycloak.synchronization.user}" />
|
<property name="userName" value="${keycloak.synchronization.user}" />
|
||||||
<property name="password" value="${keycloak.synchronization.password}" />
|
<property name="password" value="${keycloak.synchronization.password}" />
|
||||||
</bean>
|
</bean>
|
||||||
@@ -170,6 +171,10 @@
|
|||||||
<property name="groupLoadBatchSize" value="${keycloak.synchronization.groupLoadBatchSize}" />
|
<property name="groupLoadBatchSize" value="${keycloak.synchronization.groupLoadBatchSize}" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="accessTokenService.impl" class="${project.artifactId}.token.AccessTokenServiceImpl">
|
||||||
|
<property name="deployment" ref="keycloakDeployment" />
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="roleService.impl" class="${project.artifactId}.roles.RoleServiceImpl">
|
<bean id="roleService.impl" class="${project.artifactId}.roles.RoleServiceImpl">
|
||||||
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
||||||
<property name="idmClient" ref="idmClient" />
|
<property name="idmClient" ref="idmClient" />
|
||||||
|
@@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -38,33 +36,18 @@ import org.alfresco.service.cmr.repository.NodeRef;
|
|||||||
import org.alfresco.service.cmr.repository.NodeService;
|
import org.alfresco.service.cmr.repository.NodeService;
|
||||||
import org.alfresco.service.namespace.QName;
|
import org.alfresco.service.namespace.QName;
|
||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
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.apache.http.util.EntityUtils;
|
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.ServerRequest;
|
|
||||||
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
|
||||||
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
|
||||||
import org.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
|
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
|
||||||
import org.keycloak.constants.ServiceUrlConstants;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ApplicationContextAware;
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.token.AccessTokenClient;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.token.AccessTokenException;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.token.AccessTokenRefreshException;
|
||||||
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
|
||||||
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
import net.sf.acegisecurity.Authentication;
|
import net.sf.acegisecurity.Authentication;
|
||||||
@@ -73,6 +56,8 @@ import net.sf.acegisecurity.GrantedAuthorityImpl;
|
|||||||
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This component provides Keycloak-integrated user/password authentication support to an Alfresco instance.
|
||||||
|
*
|
||||||
* @author Axel Faust
|
* @author Axel Faust
|
||||||
*/
|
*/
|
||||||
public class KeycloakAuthenticationComponent extends AbstractAuthenticationComponent
|
public class KeycloakAuthenticationComponent extends AbstractAuthenticationComponent
|
||||||
@@ -101,6 +86,8 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
|
|
||||||
protected KeycloakDeployment deployment;
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
protected AccessTokenClient accessTokenClient;
|
||||||
|
|
||||||
protected Collection<AuthorityExtractor> authorityExtractors;
|
protected Collection<AuthorityExtractor> authorityExtractors;
|
||||||
|
|
||||||
protected Collection<UserProcessor> userProcessors;
|
protected Collection<UserProcessor> userProcessors;
|
||||||
@@ -115,6 +102,7 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
|
PropertyCheck.mandatory(this, "applicationContext", this.applicationContext);
|
||||||
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
||||||
|
|
||||||
|
this.accessTokenClient = new AccessTokenClient(this.deployment);
|
||||||
this.authorityExtractors = Collections
|
this.authorityExtractors = Collections
|
||||||
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
.unmodifiableList(new ArrayList<>(this.applicationContext.getBeansOfType(AuthorityExtractor.class, false, true).values()));
|
||||||
this.userProcessors = Collections
|
this.userProcessors = Collections
|
||||||
@@ -261,27 +249,12 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
final AccessTokenResponse response = ServerRequest.invokeRefresh(this.deployment, ticketToken.getRefreshToken());
|
result = this.accessTokenClient.refreshAccessToken(ticketToken.getRefreshToken());
|
||||||
final VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(response.getToken(), response.getIdToken(),
|
|
||||||
this.deployment);
|
|
||||||
|
|
||||||
result = new RefreshableAccessTokenHolder(response, tokens);
|
|
||||||
}
|
}
|
||||||
catch (final ServerRequest.HttpFailure httpFailure)
|
catch (final AccessTokenRefreshException atrex)
|
||||||
{
|
{
|
||||||
LOGGER.error("Error refreshing Keycloak authentication - {} {}", httpFailure.getStatus(), httpFailure.getError());
|
LOGGER.error("Error refreshing Keycloak authentication", atrex);
|
||||||
throw new AuthenticationException(
|
throw new AuthenticationException("Failed to refresh Keycloak authentication", atrex);
|
||||||
"Failed to refresh Keycloak authentication: " + httpFailure.getStatus() + " " + httpFailure.getError());
|
|
||||||
}
|
|
||||||
catch (final VerificationException vex)
|
|
||||||
{
|
|
||||||
LOGGER.error("Error refreshing Keycloak authentication - access token verification failed", vex);
|
|
||||||
throw new AuthenticationException("Failed to refresh Keycloak authentication", vex);
|
|
||||||
}
|
|
||||||
catch (final IOException ioex)
|
|
||||||
{
|
|
||||||
LOGGER.error("Error refreshing Keycloak authentication - unexpected IO exception", ioex);
|
|
||||||
throw new AuthenticationException("Failed to refresh Keycloak authentication", ioex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (this.failExpiredTicketTokens && !ticketToken.isActive())
|
else if (this.failExpiredTicketTokens && !ticketToken.isActive())
|
||||||
@@ -311,38 +284,30 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
throw new AuthenticationException("Simple login via user name + password is not allowed");
|
throw new AuthenticationException("Simple login via user name + password is not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
final AccessTokenResponse response;
|
final RefreshableAccessTokenHolder accessTokenHolder;
|
||||||
final VerifiedTokens tokens;
|
|
||||||
String realUserName = userName;
|
String realUserName = userName;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = this.getAccessTokenImpl(userName, new String(password));
|
accessTokenHolder = this.accessTokenClient.obtainAccessToken(userName, new String(password));
|
||||||
tokens = AdapterTokenVerifier.verifyTokens(response.getToken(), response.getIdToken(), this.deployment);
|
realUserName = accessTokenHolder.getAccessToken().getPreferredUsername();
|
||||||
|
|
||||||
realUserName = tokens.getAccessToken().getPreferredUsername();
|
|
||||||
|
|
||||||
// for potential one-off authentication, we do not care particularly about the token TTL - so no validation here
|
// for potential one-off authentication, we do not care particularly about the token TTL - so no validation here
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(this.lastTokenResponseStoreEnabled.get()))
|
if (Boolean.TRUE.equals(this.lastTokenResponseStoreEnabled.get()))
|
||||||
{
|
{
|
||||||
this.lastTokenResponse.set(new RefreshableAccessTokenHolder(response, tokens));
|
this.lastTokenResponse.set(accessTokenHolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (final VerificationException vex)
|
catch (final AccessTokenException atex)
|
||||||
{
|
{
|
||||||
LOGGER.error("Error authenticating against Keycloak - access token verification failed", vex);
|
LOGGER.error("Error authenticating against Keycloak", atex);
|
||||||
throw new AuthenticationException("Failed to authenticate against Keycloak", vex);
|
throw new AuthenticationException("Failed to authenticate against Keycloak", atex);
|
||||||
}
|
|
||||||
catch (final IOException ioex)
|
|
||||||
{
|
|
||||||
LOGGER.error("Error authenticating against Keycloak - unexpected IO exception", ioex);
|
|
||||||
throw new AuthenticationException("Failed to authenticate against Keycloak", ioex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Override setCurrentUser to perform user existence validation and role retrieval for non-Keycloak logins (e.g. via public API
|
// TODO Override setCurrentUser to perform user existence validation and role retrieval for non-Keycloak logins
|
||||||
// setCurrentUser)
|
// (e.g. via public API setCurrentUser)
|
||||||
this.setCurrentUser(realUserName);
|
this.setCurrentUser(realUserName);
|
||||||
this.handleUserTokens(tokens.getAccessToken(), tokens.getIdToken(), true);
|
this.handleUserTokens(accessTokenHolder.getAccessToken(), accessTokenHolder.getIdToken(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,75 +418,4 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
{
|
{
|
||||||
return this.allowGuestLogin;
|
return this.allowGuestLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
|
|
||||||
*
|
|
||||||
* @param userName
|
|
||||||
* the user to use for synchronisation access
|
|
||||||
* @param password
|
|
||||||
* the password of the user
|
|
||||||
* @return the access token
|
|
||||||
* @throws IOException
|
|
||||||
* when errors occur in the HTTP interaction
|
|
||||||
*/
|
|
||||||
// implementing this method locally avoids having the dependency on Keycloak authz-client
|
|
||||||
// authz-client does not support refresh, so would be of limited value anyway
|
|
||||||
protected AccessTokenResponse getAccessTokenImpl(final String userName, final String password) throws IOException
|
|
||||||
{
|
|
||||||
AccessTokenResponse tokenResponse = null;
|
|
||||||
final HttpClient client = this.deployment.getClient();
|
|
||||||
|
|
||||||
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
|
||||||
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
|
|
||||||
final List<NameValuePair> formParams = new ArrayList<>();
|
|
||||||
|
|
||||||
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
|
||||||
formParams.add(new BasicNameValuePair("username", userName));
|
|
||||||
formParams.add(new BasicNameValuePair("password", password));
|
|
||||||
|
|
||||||
ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams);
|
|
||||||
|
|
||||||
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
|
||||||
post.setEntity(form);
|
|
||||||
|
|
||||||
final HttpResponse response = client.execute(post);
|
|
||||||
final int status = response.getStatusLine().getStatusCode();
|
|
||||||
final HttpEntity entity = response.getEntity();
|
|
||||||
|
|
||||||
if (status != 200)
|
|
||||||
{
|
|
||||||
final String statusReason = response.getStatusLine().getReasonPhrase();
|
|
||||||
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
|
||||||
EntityUtils.consumeQuietly(entity);
|
|
||||||
|
|
||||||
LOGGER.debug("Failed to authenticate user against Keycloak. Status: {} Reason: {}", status, statusReason);
|
|
||||||
throw new AuthenticationException("Failed to authenticate against Keycloak - Status: " + status + ", Reason: " + statusReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entity == null)
|
|
||||||
{
|
|
||||||
LOGGER.debug("Failed to authenticate against Keycloak - Response did not contain a message body");
|
|
||||||
throw new AuthenticationException("Failed to authenticate against Keycloak - Response did not contain a message body");
|
|
||||||
}
|
|
||||||
|
|
||||||
final InputStream is = entity.getContent();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
is.close();
|
|
||||||
}
|
|
||||||
catch (final IOException e)
|
|
||||||
{
|
|
||||||
LOGGER.trace("Error closing entity stream", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenResponse;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -21,10 +21,7 @@ import com.fasterxml.jackson.databind.MappingIterator;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.alfresco.error.AlfrescoRuntimeException;
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
@@ -33,24 +30,11 @@ import org.alfresco.util.ParameterCheck;
|
|||||||
import org.alfresco.util.PropertyCheck;
|
import org.alfresco.util.PropertyCheck;
|
||||||
import org.apache.http.HttpEntity;
|
import org.apache.http.HttpEntity;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.NameValuePair;
|
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.ServerRequest;
|
|
||||||
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
|
||||||
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.constants.ServiceUrlConstants;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
@@ -60,7 +44,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
import de.acosix.alfresco.keycloak.repo.token.AccessTokenHolder;
|
||||||
|
import de.acosix.alfresco.keycloak.repo.token.AccessTokenService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the API for a client to the Keycloak admin ReST API specific to IDM structures.
|
* Implements the API for a client to the Keycloak admin ReST API specific to IDM structures.
|
||||||
@@ -72,15 +57,15 @@ public class IDMClientImpl implements InitializingBean, IDMClient
|
|||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(IDMClientImpl.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(IDMClientImpl.class);
|
||||||
|
|
||||||
protected final ReentrantReadWriteLock tokenLock = new ReentrantReadWriteLock(true);
|
|
||||||
|
|
||||||
protected KeycloakDeployment deployment;
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
protected AccessTokenService accessTokenService;
|
||||||
|
|
||||||
protected String userName;
|
protected String userName;
|
||||||
|
|
||||||
protected String password;
|
protected String password;
|
||||||
|
|
||||||
protected RefreshableAccessTokenHolder token;
|
protected AccessTokenHolder accessToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -89,6 +74,7 @@ public class IDMClientImpl implements InitializingBean, IDMClient
|
|||||||
public void afterPropertiesSet()
|
public void afterPropertiesSet()
|
||||||
{
|
{
|
||||||
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
PropertyCheck.mandatory(this, "keycloakDeployment", this.deployment);
|
||||||
|
PropertyCheck.mandatory(this, "accessTokenService", this.accessTokenService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,6 +86,15 @@ public class IDMClientImpl implements InitializingBean, IDMClient
|
|||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param accessTokenService
|
||||||
|
* the accessTokenService to set
|
||||||
|
*/
|
||||||
|
public void setAccessTokenService(final AccessTokenService accessTokenService)
|
||||||
|
{
|
||||||
|
this.accessTokenService = accessTokenService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param userName
|
* @param userName
|
||||||
* the userName to set
|
* the userName to set
|
||||||
@@ -607,214 +602,24 @@ public class IDMClientImpl implements InitializingBean, IDMClient
|
|||||||
*/
|
*/
|
||||||
protected String getValidAccessTokenForRequest()
|
protected String getValidAccessTokenForRequest()
|
||||||
{
|
{
|
||||||
String validToken = null;
|
if (this.accessToken == null)
|
||||||
|
|
||||||
this.tokenLock.readLock().lock();
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (this.token != null && this.token.isActive()
|
synchronized (this)
|
||||||
&& (!this.token.canRefresh() || !this.token.shouldRefresh(this.deployment.getTokenMinimumTimeToLive())))
|
|
||||||
{
|
{
|
||||||
validToken = this.token.getToken();
|
if (this.accessToken == null)
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.tokenLock.readLock().unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validToken == null)
|
|
||||||
{
|
|
||||||
this.tokenLock.writeLock().lock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (this.token != null && this.token.isActive()
|
|
||||||
&& (!this.token.canRefresh() || !this.token.shouldRefresh(this.deployment.getTokenMinimumTimeToLive())))
|
|
||||||
{
|
{
|
||||||
validToken = this.token.getToken();
|
if (this.userName != null && !this.userName.isEmpty())
|
||||||
}
|
{
|
||||||
|
this.accessToken = this.accessTokenService.obtainAccessToken(this.userName, this.password);
|
||||||
if (validToken == null)
|
}
|
||||||
{
|
else
|
||||||
this.obtainOrRefreshAccessToken();
|
{
|
||||||
|
this.accessToken = this.accessTokenService.obtainAccessToken();
|
||||||
validToken = this.token.getToken();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.tokenLock.writeLock().unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return validToken;
|
return this.accessToken.getAccessToken();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves or refreshes an access token, depending on the presence of valid refresh token for a previous access token.
|
|
||||||
*/
|
|
||||||
protected void obtainOrRefreshAccessToken()
|
|
||||||
{
|
|
||||||
AccessTokenResponse response;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (this.token != null && this.token.canRefresh())
|
|
||||||
{
|
|
||||||
response = ServerRequest.invokeRefresh(this.deployment, this.token.getRefreshToken());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response = this.userName != null && !this.userName.isEmpty() ? this.getAccessToken(this.userName, this.password)
|
|
||||||
: this.getAccessToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (final IOException ioex)
|
|
||||||
{
|
|
||||||
LOGGER.error("Error retrieving / refreshing access token", ioex);
|
|
||||||
throw new AlfrescoRuntimeException("Error retrieving / refreshing access token", ioex);
|
|
||||||
}
|
|
||||||
catch (final ServerRequest.HttpFailure httpFailure)
|
|
||||||
{
|
|
||||||
LOGGER.error("Refreshing access token failed: {} {}", httpFailure.getStatus(), httpFailure.getError());
|
|
||||||
throw new AlfrescoRuntimeException("Failed to refresh access token: " + httpFailure.getStatus() + " " + httpFailure.getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String tokenString = response.getToken();
|
|
||||||
|
|
||||||
final AdapterTokenVerifier.VerifiedTokens tokens;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tokens = AdapterTokenVerifier.verifyTokens(tokenString, response.getIdToken(), this.deployment);
|
|
||||||
}
|
|
||||||
catch (final VerificationException vex)
|
|
||||||
{
|
|
||||||
LOGGER.error("Token verification failed", vex);
|
|
||||||
throw new AlfrescoRuntimeException("Failed to verify token", vex);
|
|
||||||
}
|
|
||||||
|
|
||||||
final AccessToken accessToken = tokens.getAccessToken();
|
|
||||||
|
|
||||||
if ((accessToken.getExp() - this.deployment.getTokenMinimumTimeToLive()) <= Time.currentTime())
|
|
||||||
{
|
|
||||||
throw new AlfrescoRuntimeException("Failed to retrieve / refresh the access token with a longer time-to-live than the minimum");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tokenLock.writeLock().lock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.token = new RefreshableAccessTokenHolder(response, tokens);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
this.tokenLock.writeLock().unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.getNotBeforePolicy() > this.deployment.getNotBefore())
|
|
||||||
{
|
|
||||||
this.deployment.updateNotBefore(response.getNotBeforePolicy());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an access token using client credentials.
|
|
||||||
*
|
|
||||||
* @return the access token
|
|
||||||
* @throws IOException
|
|
||||||
* when errors occur in the HTTP interaction
|
|
||||||
*/
|
|
||||||
protected AccessTokenResponse getAccessToken() throws IOException
|
|
||||||
{
|
|
||||||
LOGGER.debug("Retrieving access token with client credentrials");
|
|
||||||
final AccessTokenResponse tokenResponse = this.getAccessTokenImpl(formParams -> {
|
|
||||||
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
|
||||||
});
|
|
||||||
|
|
||||||
return tokenResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an access token for a specified synchronisation user.
|
|
||||||
*
|
|
||||||
* @param userName
|
|
||||||
* the user to use for synchronisation access
|
|
||||||
* @param password
|
|
||||||
* the password of the user
|
|
||||||
* @return the access token
|
|
||||||
* @throws IOException
|
|
||||||
* when errors occur in the HTTP interaction
|
|
||||||
*/
|
|
||||||
protected AccessTokenResponse getAccessToken(final String userName, final String password) throws IOException
|
|
||||||
{
|
|
||||||
LOGGER.debug("Retrieving access token for user {}", userName);
|
|
||||||
final AccessTokenResponse tokenResponse = this.getAccessTokenImpl(formParams -> {
|
|
||||||
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
|
||||||
formParams.add(new BasicNameValuePair("username", userName));
|
|
||||||
formParams.add(new BasicNameValuePair("password", password));
|
|
||||||
});
|
|
||||||
|
|
||||||
return tokenResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
|
|
||||||
*
|
|
||||||
* @param postParamProvider
|
|
||||||
* a provider of HTTP POST parameters for the access token request
|
|
||||||
* @return the access token
|
|
||||||
* @throws IOException
|
|
||||||
* when errors occur in the HTTP interaction
|
|
||||||
*/
|
|
||||||
// implementing this method locally avoids having the dependency on Keycloak authz-client
|
|
||||||
// authz-client does not support refresh, so would be of limited value anyway
|
|
||||||
protected AccessTokenResponse getAccessTokenImpl(final Consumer<List<NameValuePair>> postParamProvider) throws IOException
|
|
||||||
{
|
|
||||||
AccessTokenResponse tokenResponse = null;
|
|
||||||
final HttpClient client = this.deployment.getClient();
|
|
||||||
|
|
||||||
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
|
||||||
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
|
|
||||||
final List<NameValuePair> formParams = new ArrayList<>();
|
|
||||||
|
|
||||||
postParamProvider.accept(formParams);
|
|
||||||
|
|
||||||
ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams);
|
|
||||||
|
|
||||||
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
|
||||||
post.setEntity(form);
|
|
||||||
|
|
||||||
final HttpResponse response = client.execute(post);
|
|
||||||
final int status = response.getStatusLine().getStatusCode();
|
|
||||||
final HttpEntity entity = response.getEntity();
|
|
||||||
if (status != 200)
|
|
||||||
{
|
|
||||||
final String statusReason = response.getStatusLine().getReasonPhrase();
|
|
||||||
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
|
||||||
EntityUtils.consumeQuietly(entity);
|
|
||||||
throw new AlfrescoRuntimeException("Failed to retrieve access token due to HTTP error " + status + ": " + statusReason);
|
|
||||||
}
|
|
||||||
if (entity == null)
|
|
||||||
{
|
|
||||||
throw new AlfrescoRuntimeException("Response to access token request did not contain a response body");
|
|
||||||
}
|
|
||||||
|
|
||||||
final InputStream is = entity.getContent();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
is.close();
|
|
||||||
}
|
|
||||||
catch (final IOException e)
|
|
||||||
{
|
|
||||||
LOGGER.trace("Error closing entity stream", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenResponse;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,306 @@
|
|||||||
|
package de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
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.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.TokenVerifier;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.ServerRequest;
|
||||||
|
import org.keycloak.adapters.ServerRequest.HttpFailure;
|
||||||
|
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||||
|
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
||||||
|
import org.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.constants.ServiceUrlConstants;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class provide the most common, low-level access token client logic that may be used across multiple higher-level
|
||||||
|
* components in this module.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenClient
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenClient.class);
|
||||||
|
|
||||||
|
protected final KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
public AccessTokenClient(final KeycloakDeployment deployment)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatory("deployment", deployment);
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains an access token for the service account of the client used to integrate this Alfresco isntance with Keycloak. This requires
|
||||||
|
* that a service account has been enabled / configured in Keycloak.
|
||||||
|
*
|
||||||
|
* @return the access token
|
||||||
|
* @throws AccessTokenException
|
||||||
|
* if the access token cannot be obtained
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder obtainAccessToken()
|
||||||
|
{
|
||||||
|
LOGGER.debug("Obtaining client access token");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
|
||||||
|
});
|
||||||
|
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
|
||||||
|
LOGGER.debug("Obtained client access token {}", response.getToken());
|
||||||
|
return refreshableToken;
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenException("Failed to obtain accses token", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains an access token for a specific user using a direct access grant. This requires that the client used to integrate this
|
||||||
|
* Alfresco instance with Keycloak is configured to allow direct access grants.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* the name of the user
|
||||||
|
* @param password
|
||||||
|
* the password provided by / for the user
|
||||||
|
* @return the access token
|
||||||
|
* @throws AccessTokenException
|
||||||
|
* if the access token cannot be obtained
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder obtainAccessToken(final String user, final String password)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Obtaining access token for user {}", user);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD));
|
||||||
|
formParams.add(new BasicNameValuePair("username", user));
|
||||||
|
formParams.add(new BasicNameValuePair("password", password));
|
||||||
|
});
|
||||||
|
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
|
||||||
|
LOGGER.debug("Obtained user access token {}", response.getToken());
|
||||||
|
return refreshableToken;
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenException("Failed to obtain accses token", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchanges an access token provided by a client / end user to this service for an access token to another client / service, retaining
|
||||||
|
* the original identity but enabling that client / service to know the access is being delegated through this Alfresco instance.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token to exchange
|
||||||
|
* @param client
|
||||||
|
* the client / service for which to obtain an access token
|
||||||
|
* @return the access token to the requested client / service
|
||||||
|
* @throws AccessTokenException
|
||||||
|
* if the token cannot be exchanged
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder exchangeToken(final String accessToken, final String client)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Exchanging {} for access token to client {}", accessToken, client);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AccessTokenResponse response = this.getAccessTokenImpl(formParams -> {
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, client));
|
||||||
|
formParams.add(new BasicNameValuePair(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE));
|
||||||
|
});
|
||||||
|
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response, client);
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
|
||||||
|
LOGGER.debug("Obtained exchanged token {}", response.getToken());
|
||||||
|
return refreshableToken;
|
||||||
|
}
|
||||||
|
catch (final IOException ioex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenException("Failed to exchange accses token", ioex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes an access token via a previously obtained refresh token.
|
||||||
|
*
|
||||||
|
* @param refreshToken
|
||||||
|
* the refresh token with which to retrieve a fresh access token
|
||||||
|
* @return the fresh access token
|
||||||
|
* @throws AccessTokenRefreshException
|
||||||
|
* if the refresh failed
|
||||||
|
*/
|
||||||
|
public RefreshableAccessTokenHolder refreshAccessToken(final String refreshToken)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Performing direct refresh via refresh token {}", refreshToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final AccessTokenResponse response = ServerRequest.invokeRefresh(this.deployment, refreshToken);
|
||||||
|
final VerifiedTokens verifiedTokens = this.verifyAccessTokenResponse(response);
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = new RefreshableAccessTokenHolder(response, verifiedTokens);
|
||||||
|
LOGGER.debug("Refreshed access token {}", response.getToken());
|
||||||
|
return refreshableToken;
|
||||||
|
}
|
||||||
|
catch (final IOException | HttpFailure ioex)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed direct refresh due to {}", ioex.getMessage());
|
||||||
|
throw new AccessTokenRefreshException("Failed to refresh access token due HTTP / IO error", ioex);
|
||||||
|
}
|
||||||
|
catch (final AccessTokenVerificationException verex)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed direct refresh due to {}", verex.getMessage());
|
||||||
|
throw new AccessTokenRefreshException("Failed to refresh access token due to verification error", verex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response)
|
||||||
|
{
|
||||||
|
final VerifiedTokens tokens;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tokens = AdapterTokenVerifier.verifyTokens(response.getToken(), response.getIdToken(), this.deployment);
|
||||||
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenVerificationException("Failed to verify token", vex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((tokens.getAccessToken().getExp() - this.deployment.getTokenMinimumTimeToLive()) <= Time.currentTime())
|
||||||
|
{
|
||||||
|
throw new AccessTokenVerificationException(
|
||||||
|
"Failed to retrieve / refresh the access token with a longer time-to-live than the minimum");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected VerifiedTokens verifyAccessTokenResponse(final AccessTokenResponse response, final String client)
|
||||||
|
{
|
||||||
|
final VerifiedTokens tokens;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final TokenVerifier<AccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(response.getToken(), this.deployment, true,
|
||||||
|
AccessToken.class);
|
||||||
|
tokenVerifier.audience(client);
|
||||||
|
tokenVerifier.issuedFor(this.deployment.getResourceName());
|
||||||
|
|
||||||
|
tokens = new VerifiedTokens(tokenVerifier.verify().getToken(), null);
|
||||||
|
}
|
||||||
|
catch (final VerificationException vex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenVerificationException("Failed to verify token", vex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((tokens.getAccessToken().getExp() - this.deployment.getTokenMinimumTimeToLive()) <= Time.currentTime())
|
||||||
|
{
|
||||||
|
throw new AccessTokenVerificationException(
|
||||||
|
"Failed to retrieve / refresh the access token with a longer time-to-live than the minimum");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an OIDC access token with the specific token request parameter up to the caller to define via the provided consumer.
|
||||||
|
*
|
||||||
|
* @param postParamProvider
|
||||||
|
* a provider of HTTP POST parameters for the access token request
|
||||||
|
* @return the access token
|
||||||
|
* @throws IOException
|
||||||
|
* when errors occur in the HTTP interaction
|
||||||
|
*/
|
||||||
|
// implementing this method locally avoids having the dependency on Keycloak authz-client
|
||||||
|
// authz-client does not support refresh, so would be of limited value anyway
|
||||||
|
protected AccessTokenResponse getAccessTokenImpl(final Consumer<List<NameValuePair>> postParamProvider) throws IOException
|
||||||
|
{
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
final HttpClient client = this.deployment.getClient();
|
||||||
|
|
||||||
|
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
|
||||||
|
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
|
||||||
|
final List<NameValuePair> formParams = new ArrayList<>();
|
||||||
|
|
||||||
|
postParamProvider.accept(formParams);
|
||||||
|
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams);
|
||||||
|
|
||||||
|
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
|
||||||
|
final HttpResponse response = client.execute(post);
|
||||||
|
final int status = response.getStatusLine().getStatusCode();
|
||||||
|
final HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200)
|
||||||
|
{
|
||||||
|
final String statusReason = response.getStatusLine().getReasonPhrase();
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
final ErrorResponse error = this.readResponseEntity(entity, ErrorResponse.class);
|
||||||
|
if ("unauthorized_client".equals(error.getError()))
|
||||||
|
{
|
||||||
|
// configuration error
|
||||||
|
LOGGER.error("Unable to retrieve access token due to invalid client configuration: {}", error.getErrorDescription());
|
||||||
|
throw new AccessTokenUnsupportedException(error.getErrorDescription());
|
||||||
|
}
|
||||||
|
// TODO Other types of more specific exceptions
|
||||||
|
LOGGER.debug("Failed to retrieve access token due to {}: {}", error.getError(), error.getErrorDescription());
|
||||||
|
throw new AccessTokenException("{0}: {1}", new Object[] { error.getError(), error.getErrorDescription() });
|
||||||
|
}
|
||||||
|
LOGGER.debug("Failed to retrieve access token due to HTTP {}: {}", status, statusReason);
|
||||||
|
throw new AccessTokenException("Failed to retrieve access token due to HTTP " + status + ": " + statusReason);
|
||||||
|
}
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new AccessTokenException("Response to access token request did not contain a response body");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse = this.readResponseEntity(entity, AccessTokenResponse.class);
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> T readResponseEntity(final HttpEntity entity, final Class<T> responseCls) throws IOException
|
||||||
|
{
|
||||||
|
final InputStream is = entity.getContent();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerialization.readValue(is, responseCls);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.trace("Error closing entity stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class signal errors / problems of any kind within the access token service.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenException extends AlfrescoRuntimeException
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 5504292228618468389L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
*/
|
||||||
|
public AccessTokenException(final String msgId)
|
||||||
|
{
|
||||||
|
super(msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenException(final String msgId, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
*/
|
||||||
|
public AccessTokenException(final String msgId, final Object[] msgParams)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenException(final String msgId, final Object[] msgParams, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface act as holders of a specific access authentication, encapsulating functionality which may dynamically change
|
||||||
|
* the value of the effective access token, such as automatic refresh and re-obtaining of the access token when necessary.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface AccessTokenHolder
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the access token from this instance. The result of this operation must never be externally cached as this operation
|
||||||
|
* transparently handles validation, potential refresh and re-obtaining of the underlying access token when necessary.
|
||||||
|
*
|
||||||
|
* @return the valid access token
|
||||||
|
* @throws AccessTokenRefreshException
|
||||||
|
* if a necessary refresh of the access token fails or cannot not be performed due to the way the access token was
|
||||||
|
* originally obtained
|
||||||
|
*/
|
||||||
|
String getAccessToken();
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class provide the technical implementation of the holder interface.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenHolderImpl implements AccessTokenHolder
|
||||||
|
{
|
||||||
|
|
||||||
|
private final ReentrantReadWriteLock tokenLock = new ReentrantReadWriteLock(true);
|
||||||
|
|
||||||
|
private RefreshableAccessTokenHolder token;
|
||||||
|
|
||||||
|
private final int minimumTimeToLive;
|
||||||
|
|
||||||
|
private final Function<String, RefreshableAccessTokenHolder> refresher;
|
||||||
|
|
||||||
|
private Supplier<RefreshableAccessTokenHolder> obtainer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class to wrap the provided initial access token.
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* the initial access token
|
||||||
|
* @param minimumTimeToLive
|
||||||
|
* the minimum time to live for access tokens expected by the Keycloak deployment
|
||||||
|
* @param refresher
|
||||||
|
* the callback function to refresh an expired access token
|
||||||
|
*/
|
||||||
|
public AccessTokenHolderImpl(final RefreshableAccessTokenHolder token, final int minimumTimeToLive,
|
||||||
|
final Function<String, RefreshableAccessTokenHolder> refresher)
|
||||||
|
{
|
||||||
|
this.token = token;
|
||||||
|
this.minimumTimeToLive = minimumTimeToLive;
|
||||||
|
this.refresher = refresher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class to wrap the provided initial access token.
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
* the initial access token
|
||||||
|
* @param minimumTimeToLive
|
||||||
|
* the minimum time to live for access tokens expected by the Keycloak deployment
|
||||||
|
* @param refresher
|
||||||
|
* the callback function to refresh an expired access token
|
||||||
|
* @param obtainer
|
||||||
|
* the supplier to re-obtain the access token after both access token and its refresh token have expired
|
||||||
|
*/
|
||||||
|
public AccessTokenHolderImpl(final RefreshableAccessTokenHolder token, final int minimumTimeToLive,
|
||||||
|
final Function<String, RefreshableAccessTokenHolder> refresher, final Supplier<RefreshableAccessTokenHolder> obtainer)
|
||||||
|
{
|
||||||
|
this.token = token;
|
||||||
|
this.minimumTimeToLive = minimumTimeToLive;
|
||||||
|
this.refresher = refresher;
|
||||||
|
this.obtainer = obtainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccessToken()
|
||||||
|
{
|
||||||
|
String validToken = null;
|
||||||
|
|
||||||
|
this.tokenLock.readLock().lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.token != null && this.token.isActive()
|
||||||
|
&& (!this.token.canRefresh() || !this.token.shouldRefresh(this.minimumTimeToLive)))
|
||||||
|
{
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.tokenLock.readLock().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validToken == null)
|
||||||
|
{
|
||||||
|
this.tokenLock.writeLock().lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.token != null && this.token.isActive()
|
||||||
|
&& (!this.token.canRefresh() || !this.token.shouldRefresh(this.minimumTimeToLive)))
|
||||||
|
{
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validToken == null)
|
||||||
|
{
|
||||||
|
if (this.token != null && this.token.canRefresh())
|
||||||
|
{
|
||||||
|
this.token = this.refresher.apply(this.token.getRefreshToken());
|
||||||
|
}
|
||||||
|
else if (this.obtainer != null)
|
||||||
|
{
|
||||||
|
this.token = this.obtainer.get();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AccessTokenRefreshException(
|
||||||
|
"The way this access token was originally obtained does not allow to re-obtain it after expiration of the token and its associated refresh token");
|
||||||
|
}
|
||||||
|
|
||||||
|
validToken = this.token.getToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.tokenLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validToken;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class signal errors / problems refreshing an access token.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenRefreshException extends AccessTokenException
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -8643258624236748350L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
*/
|
||||||
|
public AccessTokenRefreshException(final String msgId)
|
||||||
|
{
|
||||||
|
super(msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenRefreshException(final String msgId, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
*/
|
||||||
|
public AccessTokenRefreshException(final String msgId, final Object[] msgParams)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenRefreshException(final String msgId, final Object[] msgParams, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this interface allow for the retrieval of access tokens in the Keycloak realm to which this Alfresco instance is connected.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public interface AccessTokenService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains a generic realm access token for the specific client of this Alfresco instance.
|
||||||
|
*
|
||||||
|
* @return the holder for the access token
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak
|
||||||
|
*/
|
||||||
|
AccessTokenHolder obtainAccessToken();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains a generic realm access token for a specific user of the realm.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* the name of the user
|
||||||
|
* @param password
|
||||||
|
* the password of the user
|
||||||
|
* @return the holder for the access token
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* if no access token for the client can be obtained, e.g. if direct access grants have not been enabled for the specific
|
||||||
|
* client of this Alfresco instance
|
||||||
|
*/
|
||||||
|
AccessTokenHolder obtainAccessToken(String user, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a token exchange operation to obtain an access token for a specific client, acting in the name / context of the user for
|
||||||
|
* which the original access token was issued.
|
||||||
|
*
|
||||||
|
* @param accessToken
|
||||||
|
* the access token to exchange for token to another client
|
||||||
|
* @param client
|
||||||
|
* the client for which to obtain an access token
|
||||||
|
* @return the holder for the access token
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* if no access token for the client can be obtained, e.g. if no service account for it has been configured in Keycloak
|
||||||
|
*/
|
||||||
|
AccessTokenHolder exchangeToken(String accessToken, String client);
|
||||||
|
}
|
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
import org.alfresco.util.ParameterCheck;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
|
||||||
|
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class provide the technical implementation of the service interface.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenServiceImpl implements AccessTokenService, InitializingBean
|
||||||
|
{
|
||||||
|
|
||||||
|
protected KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
protected AccessTokenClient accessTokenClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterPropertiesSet()
|
||||||
|
{
|
||||||
|
PropertyCheck.mandatory(this, "deployment", this.deployment);
|
||||||
|
this.accessTokenClient = new AccessTokenClient(this.deployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param deployment
|
||||||
|
* the deployment to set
|
||||||
|
*/
|
||||||
|
public void setDeployment(final KeycloakDeployment deployment)
|
||||||
|
{
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder obtainAccessToken()
|
||||||
|
{
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken();
|
||||||
|
|
||||||
|
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(),
|
||||||
|
this.accessTokenClient::refreshAccessToken, () -> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return this.accessTokenClient.obtainAccessToken();
|
||||||
|
}
|
||||||
|
catch (final AccessTokenException atex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenRefreshException("Error re-obtaining access token as part of refresh", atex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder obtainAccessToken(final String user, final String password)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("user", user);
|
||||||
|
ParameterCheck.mandatoryString("password", password);
|
||||||
|
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.obtainAccessToken(user, password);
|
||||||
|
|
||||||
|
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(),
|
||||||
|
this.accessTokenClient::refreshAccessToken, () -> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return this.accessTokenClient.obtainAccessToken(user, password);
|
||||||
|
}
|
||||||
|
catch (final AccessTokenException atex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenRefreshException("Error re-obtaining access token as part of refresh", atex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder exchangeToken(final String accessToken, final String client)
|
||||||
|
{
|
||||||
|
ParameterCheck.mandatoryString("accessToken", accessToken);
|
||||||
|
ParameterCheck.mandatoryString("client", client);
|
||||||
|
|
||||||
|
final RefreshableAccessTokenHolder refreshableToken = this.accessTokenClient.exchangeToken(accessToken, client);
|
||||||
|
|
||||||
|
return new AccessTokenHolderImpl(refreshableToken, this.deployment.getTokenMinimumTimeToLive(), refreshToken -> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final String newAccessToken = this.accessTokenClient.refreshAccessToken(refreshToken).getToken();
|
||||||
|
return this.accessTokenClient.exchangeToken(newAccessToken, client);
|
||||||
|
}
|
||||||
|
catch (final AccessTokenException atex)
|
||||||
|
{
|
||||||
|
throw new AccessTokenRefreshException("Error re-obtaining access token as part of refresh", atex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class signal errors / problems obtaining an access token because of a configuration error or other constellation making
|
||||||
|
* the attempt fundamentally impossible.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenUnsupportedException extends AccessTokenException
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -7719788367606723647L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
*/
|
||||||
|
public AccessTokenUnsupportedException(final String msgId)
|
||||||
|
{
|
||||||
|
super(msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenUnsupportedException(final String msgId, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
*/
|
||||||
|
public AccessTokenUnsupportedException(final String msgId, final Object[] msgParams)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenUnsupportedException(final String msgId, final Object[] msgParams, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class signal errors / problems verifying an access token retrieved from Keycloak.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class AccessTokenVerificationException extends AccessTokenException
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 6057892110861316386L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
*/
|
||||||
|
public AccessTokenVerificationException(final String msgId)
|
||||||
|
{
|
||||||
|
super(msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenVerificationException(final String msgId, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
*/
|
||||||
|
public AccessTokenVerificationException(final String msgId, final Object[] msgParams)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of this class
|
||||||
|
*
|
||||||
|
* @param msgId
|
||||||
|
* the message i18n key or actual message for the exception
|
||||||
|
* @param msgParams
|
||||||
|
* the parameters for constructing a human readable message based on pattern-based message
|
||||||
|
* @param cause
|
||||||
|
* the underlying cause of this exception
|
||||||
|
*/
|
||||||
|
public AccessTokenVerificationException(final String msgId, final Object[] msgParams, final Throwable cause)
|
||||||
|
{
|
||||||
|
super(msgId, msgParams, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances of this class represent error details from Keycloak responses.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class ErrorResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
@JsonProperty("error")
|
||||||
|
protected String error;
|
||||||
|
|
||||||
|
@JsonProperty("error_description")
|
||||||
|
protected String errorDescription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the error
|
||||||
|
*/
|
||||||
|
public String getError()
|
||||||
|
{
|
||||||
|
return this.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param error
|
||||||
|
* the error to set
|
||||||
|
*/
|
||||||
|
public void setError(final String error)
|
||||||
|
{
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the errorDescription
|
||||||
|
*/
|
||||||
|
public String getErrorDescription()
|
||||||
|
{
|
||||||
|
return this.errorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param errorDescription
|
||||||
|
* the errorDescription to set
|
||||||
|
*/
|
||||||
|
public void setErrorDescription(final String errorDescription)
|
||||||
|
{
|
||||||
|
this.errorDescription = errorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 - 2020 Acosix GmbH
|
||||||
|
*
|
||||||
|
* 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 de.acosix.alfresco.keycloak.repo.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This no-op implementation class of an access token service may be used as a default implemenation in a subsystem proxy to avoid failing
|
||||||
|
* if no Keycloak subsystem instance is active.
|
||||||
|
*
|
||||||
|
* @author Axel Faust
|
||||||
|
*/
|
||||||
|
public class NoOpAccessTokenServiceImpl implements AccessTokenService
|
||||||
|
{
|
||||||
|
|
||||||
|
private static final String UNSUPPORTED_MESSAGE = "A Keycloak subsystem is not configured / enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder obtainAccessToken()
|
||||||
|
{
|
||||||
|
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder obtainAccessToken(final String user, final String password)
|
||||||
|
{
|
||||||
|
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessTokenHolder exchangeToken(final String accessToken, final String client)
|
||||||
|
{
|
||||||
|
throw new AccessTokenUnsupportedException(UNSUPPORTED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user