3 * Copyright (C) 2009-2019 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.authentication;
22 import com.google.common.collect.ImmutableSet;
23 import java.util.Optional;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
27 import javax.servlet.http.HttpSession;
28 import org.junit.Before;
29 import org.junit.Rule;
30 import org.junit.Test;
31 import org.junit.rules.ExpectedException;
32 import org.mockito.ArgumentCaptor;
33 import org.sonar.api.platform.Server;
34 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
35 import org.sonar.api.server.authentication.UserIdentity;
36 import org.sonar.db.user.UserDto;
37 import org.sonar.server.authentication.OAuth2ContextFactory.OAuthContextImpl;
38 import org.sonar.server.authentication.UserRegistration.ExistingEmailStrategy;
39 import org.sonar.server.user.TestUserSessionFactory;
40 import org.sonar.server.user.ThreadLocalUserSession;
41 import org.sonar.server.user.UserSession;
43 import static org.assertj.core.api.Assertions.assertThat;
44 import static org.mockito.ArgumentMatchers.any;
45 import static org.mockito.ArgumentMatchers.eq;
46 import static org.mockito.Mockito.mock;
47 import static org.mockito.Mockito.verify;
48 import static org.mockito.Mockito.when;
50 public class OAuth2ContextFactoryTest {
52 private static final String PROVIDER_KEY = "github";
53 private static final String SECURED_PUBLIC_ROOT_URL = "https://mydomain.com";
54 private static final String PROVIDER_NAME = "provider name";
55 private static final UserIdentity USER_IDENTITY = UserIdentity.builder()
56 .setProviderId("ABCD")
57 .setProviderLogin("johndoo")
58 .setLogin("id:johndoo")
60 .setEmail("john@email.com")
64 public ExpectedException thrown = ExpectedException.none();
66 private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
67 private TestUserRegistrar userIdentityAuthenticator = new TestUserRegistrar();
68 private Server server = mock(Server.class);
69 private OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class);
70 private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
71 private TestUserSessionFactory userSessionFactory = TestUserSessionFactory.standalone();
72 private OAuth2AuthenticationParameters oAuthParameters = mock(OAuth2AuthenticationParameters.class);
73 private HttpServletRequest request = mock(HttpServletRequest.class);
74 private HttpServletResponse response = mock(HttpServletResponse.class);
75 private HttpSession session = mock(HttpSession.class);
76 private OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class);
78 private OAuth2ContextFactory underTest = new OAuth2ContextFactory(threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler, userSessionFactory,
82 public void setUp() throws Exception {
83 when(request.getSession()).thenReturn(session);
84 when(identityProvider.getKey()).thenReturn(PROVIDER_KEY);
85 when(identityProvider.getName()).thenReturn(PROVIDER_NAME);
89 public void create_context() {
90 when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
92 OAuth2IdentityProvider.InitContext context = newInitContext();
94 assertThat(context.getRequest()).isEqualTo(request);
95 assertThat(context.getResponse()).isEqualTo(response);
96 assertThat(context.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
100 public void generate_csrf_state() {
101 OAuth2IdentityProvider.InitContext context = newInitContext();
103 context.generateCsrfState();
105 verify(csrfVerifier).generateState(request, response);
109 public void redirect_to() throws Exception {
110 OAuth2IdentityProvider.InitContext context = newInitContext();
112 context.redirectTo("/test");
114 verify(response).sendRedirect("/test");
118 public void create_callback() {
119 when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
121 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
123 assertThat(callback.getRequest()).isEqualTo(request);
124 assertThat(callback.getResponse()).isEqualTo(response);
125 assertThat(callback.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
129 public void authenticate() {
130 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
132 callback.authenticate(USER_IDENTITY);
134 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
135 verify(threadLocalUserSession).set(any(UserSession.class));
136 ArgumentCaptor<UserDto> userArgumentCaptor = ArgumentCaptor.forClass(UserDto.class);
137 verify(jwtHttpHandler).generateToken(userArgumentCaptor.capture(), eq(request), eq(response));
138 assertThat(userArgumentCaptor.getValue().getLogin()).isEqualTo(USER_IDENTITY.getLogin());
139 assertThat(userArgumentCaptor.getValue().getExternalId()).isEqualTo(USER_IDENTITY.getProviderId());
140 assertThat(userArgumentCaptor.getValue().getExternalLogin()).isEqualTo(USER_IDENTITY.getProviderLogin());
141 assertThat(userArgumentCaptor.getValue().getExternalIdentityProvider()).isEqualTo(PROVIDER_KEY);
145 public void authenticate_with_allow_email_shift() {
146 when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(true));
147 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
149 callback.authenticate(USER_IDENTITY);
151 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
152 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.ALLOW);
156 public void authenticate_without_email_shift() {
157 when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(false));
158 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
160 callback.authenticate(USER_IDENTITY);
162 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
163 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.WARN);
167 public void authenticate_with_organization_alm_ids() {
168 OAuthContextImpl callback = (OAuthContextImpl) newCallbackContext();
169 Set<String> organizationAlmIds = ImmutableSet.of("ABCD", "EFGH");
171 callback.authenticate(USER_IDENTITY, organizationAlmIds);
173 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getOrganizationAlmIds()).containsAll(organizationAlmIds);
177 public void redirect_to_home() throws Exception {
178 when(server.getContextPath()).thenReturn("");
179 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
180 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
182 callback.redirectToRequestedPage();
184 verify(response).sendRedirect("/");
188 public void redirect_to_home_with_context() throws Exception {
189 when(server.getContextPath()).thenReturn("/sonarqube");
190 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
191 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
193 callback.redirectToRequestedPage();
195 verify(response).sendRedirect("/sonarqube/");
199 public void redirect_to_requested_page() throws Exception {
200 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
201 when(server.getContextPath()).thenReturn("");
202 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
204 callback.redirectToRequestedPage();
206 verify(response).sendRedirect("/settings");
210 public void redirect_to_requested_page_does_not_need_context() throws Exception {
211 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/sonarqube/settings"));
212 when(server.getContextPath()).thenReturn("/other");
213 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
215 callback.redirectToRequestedPage();
217 verify(response).sendRedirect("/sonarqube/settings");
221 public void verify_csrf_state() {
222 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
224 callback.verifyCsrfState();
226 verify(csrfVerifier).verifyState(request, response, identityProvider);
230 public void delete_oauth2_parameters_during_redirection() {
231 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
232 when(server.getContextPath()).thenReturn("");
233 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
235 callback.redirectToRequestedPage();
237 verify(oAuthParameters).delete(eq(request), eq(response));
240 private OAuth2IdentityProvider.InitContext newInitContext() {
241 return underTest.newContext(request, response, identityProvider);
244 private OAuth2IdentityProvider.CallbackContext newCallbackContext() {
245 return underTest.newCallback(request, response, identityProvider);