/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* François Maturel
*/
package org.nuxeo.ecm.platform.ui.web.keycloak;
import static junit.framework.TestCase.assertNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakRequestAuthenticator.KEYCLOAK_ACCESS_TOKEN;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.StandardContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.AccessToken;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.test.PlatformFeature;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.LocalDeploy;
import org.nuxeo.usermapper.test.UserMapperFeature;
@RunWith(FeaturesRunner.class)
@Features({PlatformFeature.class, UserMapperFeature.class})
@Deploy({ "org.nuxeo.usermapper", "org.nuxeo.ecm.platform.web.common" })
@LocalDeploy({ "org.nuxeo.ecm.platform.login.keycloak.test:OSGI-INF/keycloak-descriptor-bundle.xml" })
public class TestKeycloakAuthenticationPlugin {
private KeycloakRequestAuthenticator authenticatorMock = Mockito.mock(KeycloakRequestAuthenticator.class);
private KeycloakAuthenticatorProvider providerMock = Mockito.mock(KeycloakAuthenticatorProvider.class);
private Request requestMock = Mockito.mock(Request.class);
private Response responseMock = Mockito.mock(Response.class);
private RequestFacade requestFacade = new RequestFacade(requestMock);
private ResponseFacade responseFacade = new ResponseFacade(responseMock);
private Connector connectorMock = Mockito.mock(Connector.class);
private org.apache.coyote.Response coyoteResponseMock = new org.apache.coyote.Response();
private static final String INVALID_BEARER_TOKEN = "Bearer invalid";
@Before
public void setUp() throws Exception {
Mockito.when(requestMock.getConnector()).thenReturn(connectorMock);
Mockito.when(requestMock.getMethod()).thenReturn("GET");
Mockito.when(requestMock.getRequestURI()).thenReturn("/foo/path/to/resource");
Mockito.when(requestMock.getRequestURL()).thenReturn(
new StringBuffer().append("https://example.com:443/foo/path/to/resource"));
Mockito.when(requestMock.getScheme()).thenReturn("https");
Mockito.when(requestMock.getServerName()).thenReturn("example.com");
Mockito.when(requestMock.getServerPort()).thenReturn(443);
Mockito.when(requestMock.getContextPath()).thenReturn("/foo");
Mockito.when(requestMock.getContext()).thenReturn(new StandardContext());
Mockito.when(connectorMock.getRedirectPort()).thenReturn(8080);
}
@Test
public void testKeycloakBearerAuthenticationSucceeding() throws Exception {
KeycloakAuthenticationPlugin keycloakAuthenticationPlugin = new KeycloakAuthenticationPlugin();
initPlugin(keycloakAuthenticationPlugin);
AccessToken accessToken = new AccessToken();
accessToken.setEmail("username@example.com");
AccessToken.Access realmAccess = new AccessToken.Access();
realmAccess.addRole("user");
accessToken.setRealmAccess(realmAccess);
Mockito.when(requestMock.getAttribute(KEYCLOAK_ACCESS_TOKEN)).thenReturn(accessToken);
Mockito.when(authenticatorMock.authenticate()).thenReturn(AuthOutcome.AUTHENTICATED);
Mockito.when(providerMock.provide(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(
authenticatorMock);
KeycloakDeployment deployment = new KeycloakDeployment();
deployment.setResourceName("test");
Mockito.when(providerMock.getResolvedDeployment()).thenReturn(deployment);
keycloakAuthenticationPlugin.setKeycloakAuthenticatorProvider(providerMock);
UserIdentificationInfo identity = keycloakAuthenticationPlugin.handleRetrieveIdentity(requestFacade,
responseMock);
assertNotNull(identity);
assertEquals("username@example.com", identity.getUserName());
}
@Test
public void testKeycloakBearerAuthenticationFailing() throws Exception {
KeycloakAuthenticationPlugin keycloakAuthenticationPlugin = new KeycloakAuthenticationPlugin();
initPlugin(keycloakAuthenticationPlugin);
// We'll check the response is marked committed
Mockito.when(responseMock.getCoyoteResponse()).thenReturn(coyoteResponseMock);
// No need to mock, just try the invalid bearer token
Mockito.when(requestMock.getHeaders(Matchers.matches("Authorization"))).thenReturn(
Collections.enumeration(Collections.singletonList(INVALID_BEARER_TOKEN)));
UserIdentificationInfo identity = keycloakAuthenticationPlugin.handleRetrieveIdentity(requestFacade,
responseFacade);
assertNull(identity);
Mockito.verify(responseMock).setStatus(401);
}
@Test
public void testKeycloakSiteAuthenticationFailing() throws Exception {
KeycloakAuthenticationPlugin keycloakAuthenticationPlugin = new KeycloakAuthenticationPlugin();
initPlugin(keycloakAuthenticationPlugin);
// We'll check the response is marked committed
Mockito.when(responseMock.getCoyoteResponse()).thenReturn(coyoteResponseMock);
// No need to mock, just try with NO bearer token
UserIdentificationInfo identity = keycloakAuthenticationPlugin.handleRetrieveIdentity(requestFacade,
responseFacade);
assertNull(identity);
Mockito.verify(responseMock).setStatus(302);
Mockito.verify(responseMock).setHeader(
Matchers.matches("Location"),
Matchers.startsWith("https://127.0.0.1:8443/auth/realms/demo/protocol/openid-connect/auth?"
+ "response_type=code&" + "client_id=customer-portal&"
+ "redirect_uri=https%3A%2F%2Fexample.com%3A443%2Ffoo%2Fpath%2Fto%2Fresource"));
}
@Test
public void testKeycloakSiteLogout() throws Exception {
KeycloakAuthenticationPlugin keycloakAuthenticationPlugin = new KeycloakAuthenticationPlugin();
initPlugin(keycloakAuthenticationPlugin);
// We'll check the response is marked committed
Mockito.when(responseMock.getCoyoteResponse()).thenReturn(coyoteResponseMock);
// No need to mock, just try with NO bearer token
Boolean result = keycloakAuthenticationPlugin.handleLogout(requestFacade, responseFacade);
assertNotNull(result);
assertEquals(true, result);
Mockito.verify(responseMock).sendRedirect(
"https://127.0.0.1:8443/auth/realms/demo/protocol/openid-connect/logout?redirect_uri=https://example.com:443/foo/home.html");
}
private KeycloakAuthenticationPlugin initPlugin(KeycloakAuthenticationPlugin keycloakAuthenticationPlugin) {
Map<String, String> parameters = new HashMap<>();
// Add more configuration parameters in a future version
parameters.put(KeycloakAuthenticationPlugin.KEYCLOAK_CONFIG_FILE_KEY, "keycloak.json");
parameters.put(KeycloakAuthenticationPlugin.KEYCLOAK_MAPPING_NAME_KEY, "keycloakTest");
keycloakAuthenticationPlugin.initPlugin(parameters);
return keycloakAuthenticationPlugin;
}
}