3 * Copyright (C) 2009-2021 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")
59 .setEmail("john@email.com")
63 public ExpectedException thrown = ExpectedException.none();
65 private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
66 private TestUserRegistrar userIdentityAuthenticator = new TestUserRegistrar();
67 private Server server = mock(Server.class);
68 private OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class);
69 private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
70 private TestUserSessionFactory userSessionFactory = TestUserSessionFactory.standalone();
71 private OAuth2AuthenticationParameters oAuthParameters = mock(OAuth2AuthenticationParameters.class);
72 private HttpServletRequest request = mock(HttpServletRequest.class);
73 private HttpServletResponse response = mock(HttpServletResponse.class);
74 private HttpSession session = mock(HttpSession.class);
75 private OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class);
77 private OAuth2ContextFactory underTest = new OAuth2ContextFactory(threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler, userSessionFactory,
82 when(request.getSession()).thenReturn(session);
83 when(identityProvider.getKey()).thenReturn(PROVIDER_KEY);
84 when(identityProvider.getName()).thenReturn(PROVIDER_NAME);
88 public void create_context() {
89 when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
91 OAuth2IdentityProvider.InitContext context = newInitContext();
93 assertThat(context.getRequest()).isEqualTo(request);
94 assertThat(context.getResponse()).isEqualTo(response);
95 assertThat(context.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
99 public void generate_csrf_state() {
100 OAuth2IdentityProvider.InitContext context = newInitContext();
102 context.generateCsrfState();
104 verify(csrfVerifier).generateState(request, response);
108 public void redirect_to() throws Exception {
109 OAuth2IdentityProvider.InitContext context = newInitContext();
111 context.redirectTo("/test");
113 verify(response).sendRedirect("/test");
117 public void create_callback() {
118 when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
120 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
122 assertThat(callback.getRequest()).isEqualTo(request);
123 assertThat(callback.getResponse()).isEqualTo(response);
124 assertThat(callback.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
128 public void authenticate() {
129 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
131 callback.authenticate(USER_IDENTITY);
133 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
134 verify(threadLocalUserSession).set(any(UserSession.class));
135 ArgumentCaptor<UserDto> userArgumentCaptor = ArgumentCaptor.forClass(UserDto.class);
136 verify(jwtHttpHandler).generateToken(userArgumentCaptor.capture(), eq(request), eq(response));
137 assertThat(userArgumentCaptor.getValue().getExternalId()).isEqualTo(USER_IDENTITY.getProviderId());
138 assertThat(userArgumentCaptor.getValue().getExternalLogin()).isEqualTo(USER_IDENTITY.getProviderLogin());
139 assertThat(userArgumentCaptor.getValue().getExternalIdentityProvider()).isEqualTo(PROVIDER_KEY);
143 public void authenticate_with_allow_email_shift() {
144 when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(true));
145 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
147 callback.authenticate(USER_IDENTITY);
149 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
150 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.ALLOW);
154 public void authenticate_without_email_shift() {
155 when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(false));
156 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
158 callback.authenticate(USER_IDENTITY);
160 assertThat(userIdentityAuthenticator.isAuthenticated()).isTrue();
161 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getExistingEmailStrategy()).isEqualTo(ExistingEmailStrategy.WARN);
165 public void authenticate_with_organization_alm_ids() {
166 OAuthContextImpl callback = (OAuthContextImpl) newCallbackContext();
167 Set<String> organizationAlmIds = ImmutableSet.of("ABCD", "EFGH");
169 callback.authenticate(USER_IDENTITY, organizationAlmIds);
171 assertThat(userIdentityAuthenticator.getAuthenticatorParameters().getOrganizationAlmIds()).containsAll(organizationAlmIds);
175 public void redirect_to_home() throws Exception {
176 when(server.getContextPath()).thenReturn("");
177 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
178 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
180 callback.redirectToRequestedPage();
182 verify(response).sendRedirect("/");
186 public void redirect_to_home_with_context() throws Exception {
187 when(server.getContextPath()).thenReturn("/sonarqube");
188 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
189 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
191 callback.redirectToRequestedPage();
193 verify(response).sendRedirect("/sonarqube/");
197 public void redirect_to_requested_page() throws Exception {
198 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
199 when(server.getContextPath()).thenReturn("");
200 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
202 callback.redirectToRequestedPage();
204 verify(response).sendRedirect("/settings");
208 public void redirect_to_requested_page_does_not_need_context() throws Exception {
209 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/sonarqube/settings"));
210 when(server.getContextPath()).thenReturn("/other");
211 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
213 callback.redirectToRequestedPage();
215 verify(response).sendRedirect("/sonarqube/settings");
219 public void verify_csrf_state() {
220 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
222 callback.verifyCsrfState();
224 verify(csrfVerifier).verifyState(request, response, identityProvider);
228 public void delete_oauth2_parameters_during_redirection() {
229 when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
230 when(server.getContextPath()).thenReturn("");
231 OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
233 callback.redirectToRequestedPage();
235 verify(oAuthParameters).delete(eq(request), eq(response));
238 private OAuth2IdentityProvider.InitContext newInitContext() {
239 return underTest.newContext(request, response, identityProvider);
242 private OAuth2IdentityProvider.CallbackContext newCallbackContext() {
243 return underTest.newCallback(request, response, identityProvider);