diff --git a/pom.xml b/pom.xml
index e151e6c72b..fea021393c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
alfresco-repository
Alfresco Repository
- 6.32-SNAPSHOT
+ 6.32-TOKEN-AUTH-SNAPSHOT
jar
@@ -56,7 +56,8 @@
5.1.40
2.0.1
3.5.2
-
+ 3.4.3.Final
+ 3.3.1.Final
@@ -693,6 +694,68 @@
+
+
+ org.keycloak
+ keycloak-core
+ ${dependency.keycloak.version}
+
+
+ *
+ *
+
+
+
+
+ org.keycloak
+ keycloak-common
+ ${dependency.keycloak.version}
+
+
+ *
+ *
+
+
+
+
+ org.keycloak
+ keycloak-adapter-core
+ ${dependency.keycloak.version}
+
+
+ *
+ *
+
+
+
+
+ org.keycloak
+ keycloak-adapter-spi
+ ${dependency.keycloak.version}
+
+
+ *
+ *
+
+
+
+
+ org.keycloak
+ keycloak-servlet-adapter-spi
+ ${dependency.keycloak.version}
+
+
+ *
+ *
+
+
+
+
+ org.jboss.logging
+ jboss-logging
+ ${dependency.jboss.logging.version}
+
+
org.mockito
diff --git a/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoBearerTokenRequestAuthenticator.java b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoBearerTokenRequestAuthenticator.java
new file mode 100644
index 0000000000..63f9749cf8
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoBearerTokenRequestAuthenticator.java
@@ -0,0 +1,61 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import org.keycloak.adapters.BearerTokenRequestAuthenticator;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OIDCAuthenticationError.Reason;
+import org.keycloak.adapters.spi.AuthChallenge;
+import org.keycloak.adapters.spi.HttpFacade;
+
+/**
+ * Extends the Keycloak BearerTokenRequestAuthenticator class to capture the error description
+ * when token valiation fails.
+ *
+ * @author Gavin Cornwell
+ */
+public class AlfrescoBearerTokenRequestAuthenticator extends BearerTokenRequestAuthenticator
+{
+ private String validationFailureDescription;
+
+ public AlfrescoBearerTokenRequestAuthenticator(KeycloakDeployment deployment)
+ {
+ super(deployment);
+ }
+
+ public String getValidationFailureDescription()
+ {
+ return this.validationFailureDescription;
+ }
+
+ @Override
+ protected AuthChallenge challengeResponse(HttpFacade facade, Reason reason, String error, String description)
+ {
+ this.validationFailureDescription = description;
+
+ return super.challengeResponse(facade, reason, error, description);
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakAdapterConfig.java b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakAdapterConfig.java
new file mode 100644
index 0000000000..f365910a6f
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakAdapterConfig.java
@@ -0,0 +1,81 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.springframework.beans.factory.InitializingBean;
+
+public class AlfrescoKeycloakAdapterConfig extends AdapterConfig implements InitializingBean
+{
+ private static Log logger = LogFactory.getLog(AlfrescoKeycloakAdapterConfig.class);
+
+ private static final String CREDENTIALS_SECRET = "keycloak.credentials.secret";
+ private static final String CREDENTIALS_PROVIDER = "keycloak.credentials.provider";
+
+ private Properties globalProperties;
+
+ public void setGlobalProperties(Properties globalProperties)
+ {
+ this.globalProperties = globalProperties;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ // programatically build the more complex objects i.e. credentials
+ Map credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ String secret = this.globalProperties.getProperty(CREDENTIALS_SECRET);
+ if (secret != null && !secret.isEmpty())
+ {
+ credentials.put("secret", secret);
+ }
+
+ String provider = this.globalProperties.getProperty(CREDENTIALS_PROVIDER);
+ if (provider != null && !provider.isEmpty())
+ {
+ credentials.put("provider", provider);
+ }
+
+ // TODO: add support for redirect-rewrite-rules and policy-enforcer if and when we need to support it
+
+ if (!credentials.isEmpty())
+ {
+ this.setCredentials(credentials);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Created credentials map from config: " + credentials);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakDeploymentFactoryBean.java b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakDeploymentFactoryBean.java
new file mode 100644
index 0000000000..4414b0aa0e
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakDeploymentFactoryBean.java
@@ -0,0 +1,77 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.springframework.beans.factory.FactoryBean;
+
+/**
+ * Creates an instance of a KeycloakDeployment object.
+ *
+ * @author Gavin Cornwell
+ */
+public class AlfrescoKeycloakDeploymentFactoryBean implements FactoryBean
+{
+ private static Log logger = LogFactory.getLog(AlfrescoKeycloakDeploymentFactoryBean.class);
+
+ private AdapterConfig keycloakAdapterConfig;
+
+ public void setAdapterConfig(AdapterConfig adapterConfig)
+ {
+ this.keycloakAdapterConfig = adapterConfig;
+ }
+
+ @Override
+ public KeycloakDeployment getObject() throws Exception
+ {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(this.keycloakAdapterConfig);
+
+ if (logger.isInfoEnabled())
+ {
+ logger.info("Keycloak JWKS URL: " + deployment.getJwksUrl());
+ logger.info("Keycloak Realm: " + deployment.getRealm());
+ logger.info("Keycloak Client ID: " + deployment.getResourceName());
+ }
+
+ return deployment;
+ }
+
+ @Override
+ public Class getObjectType()
+ {
+ return KeycloakDeployment.class;
+ }
+
+ @Override
+ public boolean isSingleton()
+ {
+ return true;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakHttpFacade.java b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakHttpFacade.java
new file mode 100644
index 0000000000..ebdb5050ec
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/security/authentication/token/AlfrescoKeycloakHttpFacade.java
@@ -0,0 +1,107 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.keycloak.adapters.servlet.ServletHttpFacade;
+
+/**
+ * Keycloak HttpFacade wrapper so we can re-use Keycloak authenticator classes.
+ *
+ * @author Gavin Cornwell
+ */
+public class AlfrescoKeycloakHttpFacade extends ServletHttpFacade
+{
+ public AlfrescoKeycloakHttpFacade(HttpServletRequest request)
+ {
+ super(request, null);
+ }
+
+ @Override
+ public Response getResponse()
+ {
+ // return our dummy NoOp implementation so we don't effect the ACS response
+ return new NoOpResponseFacade();
+ }
+
+ /**
+ * NoOp implementation of Keycloak Response interface.
+ */
+ private class NoOpResponseFacade implements Response
+ {
+
+ @Override
+ public void setStatus(int status)
+ {
+ }
+
+ @Override
+ public void addHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void setHeader(String name, String value)
+ {
+ }
+
+ @Override
+ public void resetCookie(String name, String path)
+ {
+ }
+
+ @Override
+ public void setCookie(String name, String value, String path, String domain, int maxAge,
+ boolean secure, boolean httpOnly)
+ {
+ }
+
+ @Override
+ public OutputStream getOutputStream()
+ {
+ return new ByteArrayOutputStream();
+ }
+
+ @Override
+ public void sendError(int code)
+ {
+ }
+
+ @Override
+ public void sendError(int code, String message)
+ {
+ }
+
+ @Override
+ public void end()
+ {
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapper.java b/src/main/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapper.java
new file mode 100644
index 0000000000..a636b1247d
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapper.java
@@ -0,0 +1,247 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+import org.alfresco.repo.security.authentication.AuthenticationException;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
+import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
+import org.alfresco.service.cmr.security.PersonService;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.keycloak.adapters.BasicAuthRequestAuthenticator;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.spi.AuthOutcome;
+import org.keycloak.representations.AccessToken;
+
+/**
+ * A {@link RemoteUserMapper} implementation that detects and validates JWTs.
+ *
+ * @author Gavin Cornwell
+ */
+public class TokenRemoteUserMapper implements RemoteUserMapper, ActivateableBean
+{
+ private static Log logger = LogFactory.getLog(TokenRemoteUserMapper.class);
+
+ /** Is the mapper enabled */
+ private boolean isEnabled;
+
+ /** Are token validation failures handled silently? */
+ private boolean isValidationFailureSilent;
+
+ /** The person service. */
+ private PersonService personService;
+
+ /** The Keycloak deployment object */
+ private KeycloakDeployment keycloakDeployment;
+
+ /**
+ * Sets the active flag
+ *
+ * @param isEnabled true to enable the subsystem
+ */
+ public void setActive(boolean isEnabled)
+ {
+ this.isEnabled = isEnabled;
+ }
+
+ /**
+ * Determines whether token validation failures are silent
+ *
+ * @param silent true to silently fail, false to throw an exception
+ */
+ public void setValidationFailureSilent(boolean silent)
+ {
+ this.isValidationFailureSilent = silent;
+ }
+
+ /**
+ * Sets the person service.
+ *
+ * @param personService
+ * the person service
+ */
+ public void setPersonService(PersonService personService)
+ {
+ this.personService = personService;
+ }
+
+ public void setKeycloakDeployment(KeycloakDeployment deployment)
+ {
+ this.keycloakDeployment = deployment;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(javax.servlet.http.HttpServletRequest)
+ */
+ public String getRemoteUser(HttpServletRequest request)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Retrieving username from http request...");
+
+ if (!this.isEnabled)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("TokenRemoteUserMapper is disabled, returning null.");
+
+ return null;
+ }
+
+ String headerUserId = extractUserFromHeader(request);
+
+ if (headerUserId != null)
+ {
+ // Normalize the user ID taking into account case sensitivity settings
+ String normalizedUserId = normalizeUserId(headerUserId);
+
+ if (logger.isDebugEnabled())
+ logger.debug("Returning username: " + normalizedUserId);
+
+ return normalizedUserId;
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive()
+ */
+ public boolean isActive()
+ {
+ return this.isEnabled;
+ }
+
+ /**
+ * Extracts the user name from the JWT in the given request.
+ *
+ * @param request The request containing the JWT
+ * @return The user name or null if it can not be determined
+ */
+ private String extractUserFromHeader(HttpServletRequest request)
+ {
+ String userName = null;
+
+ AlfrescoKeycloakHttpFacade facade = new AlfrescoKeycloakHttpFacade(request);
+
+ // try authenticating with bearer token first
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Trying bearer token...");
+ }
+
+ AlfrescoBearerTokenRequestAuthenticator tokenAuthenticator =
+ new AlfrescoBearerTokenRequestAuthenticator(this.keycloakDeployment);
+ AuthOutcome tokenOutcome = tokenAuthenticator.authenticate(facade);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Bearer token outcome: " + tokenOutcome);
+ }
+
+ if (tokenOutcome == AuthOutcome.FAILED && !isValidationFailureSilent)
+ {
+ throw new AuthenticationException("Token validation failed: " +
+ tokenAuthenticator.getValidationFailureDescription());
+ }
+
+ if (tokenOutcome == AuthOutcome.AUTHENTICATED)
+ {
+ userName = extractUserFromToken(tokenAuthenticator.getToken());
+ }
+ else
+ {
+ // if bearer token failed, try basic auth, if enabled
+ if (this.keycloakDeployment.isEnableBasicAuth())
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Trying basic auth...");
+ }
+
+ BasicAuthRequestAuthenticator basicAuthenticator =
+ new BasicAuthRequestAuthenticator(this.keycloakDeployment);
+ AuthOutcome basicOutcome = basicAuthenticator.authenticate(facade);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Basic auth outcome: " + basicOutcome);
+ }
+
+ // if auth was successful, extract username and return
+ if (basicOutcome == AuthOutcome.AUTHENTICATED)
+ {
+ userName = extractUserFromToken(basicAuthenticator.getToken());
+ }
+ }
+ }
+
+ return userName;
+ }
+
+ private String extractUserFromToken(AccessToken jwt)
+ {
+ // retrieve the preferred_username claim
+ String userName = jwt.getPreferredUsername();
+
+ if (logger.isDebugEnabled())
+ logger.debug("Extracted username: " + userName);
+
+ return userName;
+ }
+
+ /**
+ * Normalizes a user id, taking into account existing user accounts and case sensitivity settings.
+ *
+ * @param userId
+ * the user id
+ * @return the string
+ */
+ private String normalizeUserId(final String userId)
+ {
+ if (userId == null)
+ {
+ return null;
+ }
+
+ String normalized = AuthenticationUtil.runAs(new RunAsWork()
+ {
+ public String doWork() throws Exception
+ {
+ return personService.getUserIdentifier(userId);
+ }
+ }, AuthenticationUtil.getSystemUserName());
+
+ if (logger.isDebugEnabled())
+ logger.debug("Normalized user name for '" + userId + "': " + normalized);
+
+ return normalized == null ? userId : normalized;
+ }
+}
diff --git a/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication-context.xml b/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication-context.xml
new file mode 100644
index 0000000000..afba45e137
--- /dev/null
+++ b/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication-context.xml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${token.authentication.defaultAdministratorUserNames}
+
+
+
+
+
+
+
+ org.alfresco.repo.security.authentication.AuthenticationComponent
+
+
+
+
+
+
+
+
+
+
+ ${server.transaction.mode.default}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+ ${authentication.protection.limit}
+
+
+ ${authentication.protection.periodSeconds}
+
+
+
+
+
+
+
+
+
+
+
+ ${keycloak.realm}
+
+
+ ${keycloak.realm-public-key:#{null}}
+
+
+ ${keycloak.auth-server-url}
+
+
+ ${keycloak.ssl-required:external}
+
+
+ ${keycloak.confidential-port:0}
+
+
+ ${keycloak.resource}
+
+
+ ${keycloak.use-resource-role-mappings:false}
+
+
+ ${keycloak.enable-cors:false}
+
+
+ ${keycloak.cors-max-age:-1}
+
+
+ ${keycloak.cors-allowed-headers:#{null}}
+
+
+ ${keycloak.cors-allowed-methods:#{null}}
+
+
+ ${keycloak.cors-exposed-headers:#{null}}
+
+
+ ${keycloak.expose-token:false}
+
+
+ ${keycloak.bearer-only:false}
+
+
+ ${keycloak.autodetect-bearer-only:false}
+
+
+ ${keycloak.enable-basic-auth:false}
+
+
+ ${keycloak.public-client:false}
+
+
+ ${keycloak.allow-any-hostname:false}
+
+
+ ${keycloak.disable-trust-manager:false}
+
+
+ ${keycloak.truststore:#{null}}
+
+
+ ${keycloak.truststore-password:#{null}}
+
+
+ ${keycloak.client-keystore:#{null}}
+
+
+ ${keycloak.client-keystore-password:#{null}}
+
+
+ ${keycloak.client-key-password:#{null}}
+
+
+ ${keycloak.connection-pool-size:20}
+
+
+ ${keycloak.always-refresh-token:false}
+
+
+ ${keycloak.register-node-at-startup:false}
+
+
+ ${keycloak.register-node-period:-1}
+
+
+ ${keycloak.token-store:#{null}}
+
+
+ ${keycloak.principal-attribute:#{null}}
+
+
+ ${keycloak.turn-off-change-session-id-on-login:false}
+
+
+ ${keycloak.token-minimum-time-to-live:0}
+
+
+ ${keycloak.min-time-between-jwks-requests:10}
+
+
+ ${keycloak.public-key-cache-ttl:86400}
+
+
+ ${keycloak.enable-pkce:false}
+
+
+ ${keycloak.ignore-oauth-query-parameter:false}
+
+
+
+
+
+
+
+
+
+
+
+
+ ${token.authentication.enabled}
+
+
+ ${token.authentication.validation.failure.silent}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication.properties b/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication.properties
new file mode 100644
index 0000000000..22bd745473
--- /dev/null
+++ b/src/main/resources/alfresco/subsystems/Authentication/token/token-authentication.properties
@@ -0,0 +1,10 @@
+token.authentication.enabled=true
+token.authentication.validation.failure.silent=true
+token.authentication.defaultAdministratorUserNames=admin
+
+# Keycloak configuration
+keycloak.auth-server-url=http://localhost:8180/auth
+keycloak.realm=springboot
+keycloak.ssl-required=none
+keycloak.resource=activiti
+keycloak.public-client=true
\ No newline at end of file
diff --git a/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java b/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java
index f8d66cc544..de311be4da 100644
--- a/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java
+++ b/src/test/java/org/alfresco/repo/security/SecurityTestSuite.java
@@ -25,10 +25,6 @@
*/
package org.alfresco.repo.security;
-import junit.framework.JUnit4TestAdapter;
-import junit.framework.Test;
-import junit.framework.TestSuite;
-
import org.alfresco.repo.domain.permissions.FixedAclUpdaterTest;
import org.alfresco.repo.ownable.impl.OwnableServiceTest;
import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest;
@@ -42,6 +38,7 @@ import org.alfresco.repo.security.authentication.UpgradePasswordHashTest;
import org.alfresco.repo.security.authentication.external.DefaultRemoteUserMapperTest;
import org.alfresco.repo.security.authentication.external.LocalAuthenticationServiceTest;
import org.alfresco.repo.security.authentication.subsystems.SubsystemChainingFtpAuthenticatorTest;
+import org.alfresco.repo.security.authentication.token.TokenRemoteUserMapperTest;
import org.alfresco.repo.security.authority.AuthorityBridgeTableAsynchronouslyRefreshedCacheTest;
import org.alfresco.repo.security.authority.AuthorityServiceTest;
import org.alfresco.repo.security.authority.DuplicateAuthorityTest;
@@ -57,6 +54,10 @@ import org.alfresco.repo.security.permissions.impl.model.PermissionModelTest;
import org.alfresco.repo.security.person.HomeFolderProviderSynchronizerTest;
import org.alfresco.repo.security.person.PersonTest;
+import junit.framework.JUnit4TestAdapter;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
/**
* @author Andy Hind
*
@@ -100,6 +101,7 @@ public class SecurityTestSuite extends TestSuite
suite.addTestSuite(FixedAclUpdaterTest.class);
suite.addTestSuite(DefaultRemoteUserMapperTest.class);
+ suite.addTestSuite(TokenRemoteUserMapperTest.class);
suite.addTestSuite(SubsystemChainingFtpAuthenticatorTest.class);
suite.addTest(new JUnit4TestAdapter(LocalAuthenticationServiceTest.class));
diff --git a/src/test/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapperTest.java b/src/test/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapperTest.java
new file mode 100644
index 0000000000..6d6bac6062
--- /dev/null
+++ b/src/test/java/org/alfresco/repo/security/authentication/token/TokenRemoteUserMapperTest.java
@@ -0,0 +1,512 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.token;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.Map;
+import java.util.Vector;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.alfresco.repo.management.subsystems.AbstractChainedSubsystemTest;
+import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
+import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
+import org.alfresco.repo.security.authentication.AuthenticationException;
+import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
+import org.alfresco.util.ApplicationContextHelper;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpClient;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.AccessToken;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Tests the token based authentication subsystem.
+ *
+ * @author Gavin Cornwell
+ */
+public class TokenRemoteUserMapperTest extends AbstractChainedSubsystemTest
+{
+ private static final String REMOTE_USER_MAPPER_BEAN_NAME = "remoteUserMapper";
+ private static final String KEYCLOAK_DEPLOYMENT_BEAN_NAME = "keycloakDeployment";
+
+ private static final String TEST_USER_USERNAME = "testuser";
+ private static final String TEST_USER_EMAIL = "testuser@mail.com";
+
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String BEARER_PREFIX = "Bearer ";
+ private static final String BASIC_PREFIX = "Basic ";
+
+ private static final String CONFIG_SILENT_ERRORS = "token.authentication.validation.failure.silent";
+
+ private static final String PASSWORD_GRANT_RESPONSE = "{" +
+ "\"access_token\": \"%s\"," +
+ "\"expires_in\": 300," +
+ "\"refresh_expires_in\": 1800," +
+ "\"refresh_token\": \"%s\"," +
+ "\"token_type\": \"bearer\"," +
+ "\"not-before-policy\": 0," +
+ "\"session_state\": \"71c2c5ba-9c98-49fc-882f-dedcf80ee1b5\"}";
+
+ ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+ DefaultChildApplicationContextManager childApplicationContextManager;
+ ChildApplicationContextFactory childApplicationContextFactory;
+
+ private KeyPair keyPair;
+ private AlfrescoKeycloakAdapterConfig keycloakAdapterConfig;
+
+ /* (non-Javadoc)
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception
+ {
+ // switch authentication to use token auth
+ childApplicationContextManager = (DefaultChildApplicationContextManager) ctx.getBean("Authentication");
+ childApplicationContextManager.stop();
+ childApplicationContextManager.setProperty("chain", "token1:token");
+ childApplicationContextFactory = getChildApplicationContextFactory(childApplicationContextManager, "token1");
+
+ // generate keys for test
+ this.keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
+
+ // hardcode the realm public key in the Keycloak deployment bean to stop it fetching keys
+ applyHardcodedPublicKey(this.keyPair.getPublic());
+
+ // extract config
+ this.keycloakAdapterConfig = (AlfrescoKeycloakAdapterConfig)childApplicationContextFactory.
+ getApplicationContext().getBean("keycloakAdpapterConfig");
+ }
+
+ /* (non-Javadoc)
+ * @see junit.framework.TestCase#tearDown()
+ */
+ @Override
+ protected void tearDown() throws Exception
+ {
+ childApplicationContextManager.destroy();
+ childApplicationContextManager = null;
+ childApplicationContextFactory = null;
+ }
+
+ public void testKeycloakConfig() throws Exception
+ {
+ // check string overrides
+ assertEquals("keycloak.auth-server-url", "http://192.168.0.1:8180/auth",
+ this.keycloakAdapterConfig.getAuthServerUrl());
+
+ assertEquals("keycloak.realm", "test",
+ this.keycloakAdapterConfig.getRealm());
+
+ assertEquals("keycloak.realm-public-key",
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l" +
+ "BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM" +
+ "uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W" +
+ "ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD" +
+ "P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j" +
+ "QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx" +
+ "jwIDAQAB",
+ this.keycloakAdapterConfig.getRealmKey());
+
+ assertEquals("keycloak.ssl-required", "external",
+ this.keycloakAdapterConfig.getSslRequired());
+
+ assertEquals("keycloak.resource", "test",
+ this.keycloakAdapterConfig.getResource());
+
+ assertEquals("keycloak.cors-allowed-headers", "Authorization",
+ this.keycloakAdapterConfig.getCorsAllowedHeaders());
+
+ assertEquals("keycloak.cors-allowed-methods", "POST, PUT, DELETE, GET",
+ this.keycloakAdapterConfig.getCorsAllowedMethods());
+
+ assertEquals("keycloak.cors-exposed-headers", "WWW-Authenticate, My-custom-exposed-Header",
+ this.keycloakAdapterConfig.getCorsExposedHeaders());
+
+ assertEquals("keycloak.truststore",
+ "classpath:/alfresco/subsystems/tokenAuthentication/keystore.jks",
+ this.keycloakAdapterConfig.getTruststore());
+
+ assertEquals("keycloak.truststore-password", "password",
+ this.keycloakAdapterConfig.getTruststorePassword());
+
+ assertEquals("keycloak.client-keystore",
+ "classpath:/alfresco/subsystems/tokenAuthentication/keystore.jks",
+ this.keycloakAdapterConfig.getClientKeystore());
+
+ assertEquals("keycloak.client-keystore-password", "password",
+ this.keycloakAdapterConfig.getClientKeystorePassword());
+
+ assertEquals("keycloak.client-key-password", "password",
+ this.keycloakAdapterConfig.getClientKeyPassword());
+
+ assertEquals("keycloak.token-store", "SESSION",
+ this.keycloakAdapterConfig.getTokenStore());
+
+ assertEquals("keycloak.principal-attribute", "preferred_username",
+ this.keycloakAdapterConfig.getPrincipalAttribute());
+
+ // check number overrides
+ assertEquals("keycloak.confidential-port", 100,
+ this.keycloakAdapterConfig.getConfidentialPort());
+
+ assertEquals("keycloak.cors-max-age", 1000,
+ this.keycloakAdapterConfig.getCorsMaxAge());
+
+ assertEquals("keycloak.connection-pool-size", 5,
+ this.keycloakAdapterConfig.getConnectionPoolSize());
+
+ assertEquals("keycloak.register-node-period", 50,
+ this.keycloakAdapterConfig.getRegisterNodePeriod());
+
+ assertEquals("keycloak.token-minimum-time-to-live", 10,
+ this.keycloakAdapterConfig.getTokenMinimumTimeToLive());
+
+ assertEquals("keycloak.min-time-between-jwks-requests", 60,
+ this.keycloakAdapterConfig.getMinTimeBetweenJwksRequests());
+
+ assertEquals("keycloak.public-key-cache-ttl", 3600,
+ this.keycloakAdapterConfig.getPublicKeyCacheTtl());
+
+ // check boolean overrides
+ assertFalse("keycloak.public-client",
+ this.keycloakAdapterConfig.isPublicClient());
+
+ assertTrue("keycloak.use-resource-role-mappings",
+ this.keycloakAdapterConfig.isUseResourceRoleMappings());
+
+ assertTrue("keycloak.enable-cors",
+ this.keycloakAdapterConfig.isCors());
+
+ assertTrue("keycloak.expose-token",
+ this.keycloakAdapterConfig.isExposeToken());
+
+ assertTrue("keycloak.bearer-only",
+ this.keycloakAdapterConfig.isBearerOnly());
+
+ assertTrue("keycloak.autodetect-bearer-only",
+ this.keycloakAdapterConfig.isAutodetectBearerOnly());
+
+ assertTrue("keycloak.enable-basic-auth",
+ this.keycloakAdapterConfig.isEnableBasicAuth());
+
+ assertTrue("keycloak.allow-any-hostname",
+ this.keycloakAdapterConfig.isAllowAnyHostname());
+
+ assertTrue("keycloak.disable-trust-manager",
+ this.keycloakAdapterConfig.isDisableTrustManager());
+
+ assertTrue("keycloak.always-refresh-token",
+ this.keycloakAdapterConfig.isAlwaysRefreshToken());
+
+ assertTrue("keycloak.register-node-at-startup",
+ this.keycloakAdapterConfig.isRegisterNodeAtStartup());
+
+ assertTrue("keycloak.enable-pkce",
+ this.keycloakAdapterConfig.isPkce());
+
+ assertTrue("keycloak.ignore-oauth-query-parameter",
+ this.keycloakAdapterConfig.isIgnoreOAuthQueryParameter());
+
+ assertTrue("keycloak.turn-off-change-session-id-on-login",
+ this.keycloakAdapterConfig.getTurnOffChangeSessionIdOnLogin());
+
+ // check credentials overrides
+ Map credentials = this.keycloakAdapterConfig.getCredentials();
+ assertNotNull("Expected a credentials map", credentials);
+ assertFalse("Expected to retrieve a populated credentials map", credentials.isEmpty());
+ assertEquals("keycloak.credentials.secret", "11111", credentials.get("secret"));
+ assertEquals("keycloak.credentials.provider", "secret", credentials.get("provider"));
+ }
+
+ public void testValidToken() throws Exception
+ {
+ // create token
+ String jwt = generateToken(false);
+
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest(jwt);
+
+ // validate correct user was found
+ assertEquals(TEST_USER_USERNAME, ((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testWrongPublicKey() throws Exception
+ {
+ // generate and apply an incorrect public key
+ childApplicationContextFactory.stop();
+ applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
+
+ // create token
+ String jwt = generateToken(false);
+
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest(jwt);
+
+ // ensure null is returned if the public key is wrong
+ assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testWrongPublicKeyWithError() throws Exception
+ {
+ // generate and apply an incorrect public key
+ childApplicationContextFactory.stop();
+ childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
+ applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
+
+ // create token
+ String jwt = generateToken(false);
+
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest(jwt);
+
+ // ensure an exception is thrown with correct description
+ try
+ {
+ ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
+ fail("Expected an AuthenticationException to be thrown");
+ }
+ catch (AuthenticationException ae)
+ {
+ assertTrue("Exception message contains 'Invalid token signature'",
+ ae.getMessage().indexOf("Invalid token signature") != -1);
+ }
+ }
+
+ public void testInvalidJwt() throws Exception
+ {
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest("thisisnotaJWT");
+
+ // ensure null is returned if the JWT is invalid
+ assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testMissingToken() throws Exception
+ {
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest("");
+
+ // ensure null is returned if the token is missing
+ assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testExpiredToken() throws Exception
+ {
+ // create token
+ String jwt = generateToken(true);
+
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest(jwt);
+
+ // ensure null is returned if the token has expired
+ assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testExpiredTokenWithError() throws Exception
+ {
+ // turn on validation failure reporting
+ childApplicationContextFactory.stop();
+ childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
+ applyHardcodedPublicKey(this.keyPair.getPublic());
+
+ // create token
+ String jwt = generateToken(true);
+
+ // create mock request object
+ HttpServletRequest mockRequest = createMockTokenRequest(jwt);
+
+ // ensure an exception is thrown with correct description
+ try
+ {
+ ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
+ fail("Expected an AuthenticationException to be thrown");
+ }
+ catch (AuthenticationException ae)
+ {
+ assertTrue("Exception message contains 'Token is not active'",
+ ae.getMessage().indexOf("Token is not active") != -1);
+ }
+ }
+
+ public void testMissingHeader() throws Exception
+ {
+ // create mock request object with no Authorization header
+ HttpServletRequest mockRequest = createMockTokenRequest(null);
+
+ // ensure null is returned if the header was missing
+ assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ public void testBasicAuthFallback() throws Exception
+ {
+ // create mock objects
+ HttpServletRequest mockRequest = createMockBasicRequest();
+ HttpClient mockHttpClient = createMockHttpClient();
+
+ // override the http client on the keycloak deployment
+ KeycloakDeployment deployment = (KeycloakDeployment)childApplicationContextFactory.getApplicationContext().
+ getBean(KEYCLOAK_DEPLOYMENT_BEAN_NAME);
+ deployment.setClient(mockHttpClient);
+
+ // validate correct user was found
+ assertEquals(TEST_USER_USERNAME, ((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
+ REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
+ }
+
+ /**
+ * Utility method for creating a mocked Servlet request with a token.
+ *
+ * @param token The token to add to the Authorization header
+ * @return The mocked request object
+ */
+ private HttpServletRequest createMockTokenRequest(String token)
+ {
+ // Mock a request with the token in the Authorization header (if supplied)
+ HttpServletRequest mockRequest = mock(HttpServletRequest.class);
+
+ Vector authHeaderValues = new Vector<>(1);
+ if (token != null)
+ {
+ authHeaderValues.add(BEARER_PREFIX + token);
+ }
+
+ when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements());
+
+ return mockRequest;
+ }
+
+ /**
+ * Utility method for creating a mocked Servlet request with basic auth.
+ *
+ * @return The mocked request object
+ */
+ @SuppressWarnings("unchecked")
+ private HttpServletRequest createMockBasicRequest()
+ {
+ // Mock a request with the token in the Authorization header (if supplied)
+ HttpServletRequest mockRequest = mock(HttpServletRequest.class);
+
+ Vector authHeaderValues = new Vector<>(1);
+ String userPwd = TEST_USER_USERNAME + ":" + TEST_USER_USERNAME;
+ authHeaderValues.add(BASIC_PREFIX + Base64.encodeBytes(userPwd.getBytes()));
+
+ // NOTE: as getHeaders gets called twice provide two separate Enumeration objects so that
+ // an empty result is not returned for the second invocation.
+ when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements(),
+ authHeaderValues.elements());
+
+ return mockRequest;
+ }
+
+ private HttpClient createMockHttpClient() throws Exception
+ {
+ // mock HttpClient object and set on keycloak deployment to avoid basic auth
+ // attempting to get a token using HTTP POST
+ HttpClient mockHttpClient = mock(HttpClient.class);
+ HttpResponse mockHttpResponse = mock(HttpResponse.class);
+ StatusLine mockStatusLine = mock(StatusLine.class);
+ HttpEntity mockHttpEntity = mock(HttpEntity.class);
+
+ // for the purpose of this test use the same token for access and refresh
+ String token = generateToken(false);
+ String jsonResponse = String.format(PASSWORD_GRANT_RESPONSE, token, token);
+ ByteArrayInputStream jsonResponseStream = new ByteArrayInputStream(jsonResponse.getBytes());
+
+ when(mockHttpClient.execute(any())).thenReturn(mockHttpResponse);
+ when(mockHttpResponse.getStatusLine()).thenReturn(mockStatusLine);
+ when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity);
+ when(mockStatusLine.getStatusCode()).thenReturn(200);
+ when(mockHttpEntity.getContent()).thenReturn(jsonResponseStream);
+
+ return mockHttpClient;
+ }
+
+ /**
+ * Utility method to create tokens for testing.
+ *
+ * @param expired Determines whether to create an expired JWT
+ * @return The string representation of the JWT
+ */
+ private String generateToken(boolean expired) throws Exception
+ {
+ String issuerUrl = this.keycloakAdapterConfig.getAuthServerUrl() + "/realms/" + this.keycloakAdapterConfig.getRealm();
+
+ AccessToken token = new AccessToken();
+ token.type("Bearer");
+ token.id("1234");
+ token.subject("abc123");
+ token.issuer(issuerUrl);
+ token.setPreferredUsername(TEST_USER_USERNAME);
+ token.setEmail(TEST_USER_EMAIL);
+ token.setGivenName("Joe");
+ token.setFamilyName("Bloggs");
+
+ if (expired)
+ {
+ token.expiration(Time.currentTime() - 60);
+ }
+
+ String jwt = new JWSBuilder()
+ .jsonContent(token)
+ .rsa256(keyPair.getPrivate());
+
+ return jwt;
+ }
+
+ /**
+ * Finds the keycloak deployment bean and applies a hardcoded public key locator using the
+ * provided public key.
+ */
+ private void applyHardcodedPublicKey(PublicKey publicKey)
+ {
+ KeycloakDeployment deployment = (KeycloakDeployment)childApplicationContextFactory.getApplicationContext().
+ getBean(KEYCLOAK_DEPLOYMENT_BEAN_NAME);
+ HardcodedPublicKeyLocator publicKeyLocator = new HardcodedPublicKeyLocator(publicKey);
+ deployment.setPublicKeyLocator(publicKeyLocator);
+ }
+}
diff --git a/src/test/resources/alfresco-global.properties b/src/test/resources/alfresco-global.properties
new file mode 100644
index 0000000000..68878bf0aa
--- /dev/null
+++ b/src/test/resources/alfresco-global.properties
@@ -0,0 +1,45 @@
+# Test token authentication overrides
+keycloak.auth-server-url=http://192.168.0.1:8180/auth
+keycloak.realm=test
+keycloak.realm-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l\
+BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM\
+uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W\
+ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD\
+P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j\
+QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx\
+jwIDAQAB
+keycloak.ssl-required=external
+keycloak.resource=test
+keycloak.public-client=false
+keycloak.confidential-port=100
+keycloak.use-resource-role-mappings=true
+keycloak.enable-cors=true
+keycloak.cors-max-age=1000
+keycloak.cors-allowed-headers=Authorization
+keycloak.cors-allowed-methods=POST, PUT, DELETE, GET
+keycloak.cors-exposed-headers=WWW-Authenticate, My-custom-exposed-Header
+keycloak.expose-token=true
+keycloak.bearer-only=true
+keycloak.autodetect-bearer-only=true
+keycloak.enable-basic-auth=true
+keycloak.allow-any-hostname=true
+keycloak.disable-trust-manager=true
+keycloak.truststore=classpath:/alfresco/subsystems/tokenAuthentication/keystore.jks
+keycloak.truststore-password=password
+keycloak.client-keystore=classpath:/alfresco/subsystems/tokenAuthentication/keystore.jks
+keycloak.client-keystore-password=password
+keycloak.client-key-password=password
+keycloak.connection-pool-size=5
+keycloak.always-refresh-token=true
+keycloak.register-node-at-startup=true
+keycloak.register-node-period=50
+keycloak.token-store=SESSION
+keycloak.principal-attribute=preferred_username
+keycloak.turn-off-change-session-id-on-login=true
+keycloak.token-minimum-time-to-live=10
+keycloak.min-time-between-jwks-requests=60
+keycloak.public-key-cache-ttl=3600
+keycloak.enable-pkce=true
+keycloak.ignore-oauth-query-parameter=true
+keycloak.credentials.secret=11111
+keycloak.credentials.provider=secret
\ No newline at end of file
diff --git a/src/test/resources/alfresco/subsystems/tokenAuthentication/keystore.jks b/src/test/resources/alfresco/subsystems/tokenAuthentication/keystore.jks
new file mode 100644
index 0000000000..63ea4cce60
Binary files /dev/null and b/src/test/resources/alfresco/subsystems/tokenAuthentication/keystore.jks differ
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
index d710d1ab11..0e6c8e9dbd 100644
--- a/src/test/resources/log4j.properties
+++ b/src/test/resources/log4j.properties
@@ -254,3 +254,7 @@ log4j.logger.org.alfresco.repo.usage.RepoUsageMonitor=info
log4j.logger.org.alfresco.repo.site.SiteServiceImpl=DEBUG
log4j.logger.org.alfresco.repo.action.ActionServiceImpl=DEBUG
log4j.logger.org.alfresco.repo.security.person.PersonServiceImpl=DEBUG
+
+# token authentication
+log4j.logger.org.alfresco.repo.security.authentication.token=debug
+log4j.logger.org.keycloak=debug
\ No newline at end of file