Service to obtain tokens for integrations

This commit is contained in:
AFaust
2021-01-09 16:29:28 +01:00
parent 89d8ecc5dc
commit f9e16e0ef4
15 changed files with 1190 additions and 350 deletions

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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;
}
} }

View File

@@ -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;
} }
} }

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}