Skip to content

Commit

Permalink
✨ : add configuration to handle oauth2 client connections
Browse files Browse the repository at this point in the history
  • Loading branch information
cdubuisson committed Oct 16, 2019
1 parent e0a30bc commit cf25f37
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 0 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@
<version>${jackson.version}</version>
</dependency>

<!-- oauth2 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.codeka.gaia.config.security.oauth2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;

@Configuration
@Order(70)
@Conditional(ClientsConfiguredCondition.class)
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

private OAuth2SuccessHandler oAuth2SuccessHandler;

@Autowired
public OAuth2ClientSecurityConfig(OAuth2SuccessHandler oAuth2SuccessHandler) {
this.oAuth2SuccessHandler = oAuth2SuccessHandler;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
var requestMatcher = new OrRequestMatcher(
// connection to oauth2 client
new AntPathRequestMatcher("/oauth2/authorization/*"),
// oauth2 client callback
new AntPathRequestMatcher("/login/oauth2/code/*")
);
http
.requestMatcher(requestMatcher)
.authorizeRequests()
.anyRequest().permitAll()
.and()
.oauth2Login()
.loginPage("/login")
.successHandler(oAuth2SuccessHandler).permitAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.codeka.gaia.config.security.oauth2;

import io.codeka.gaia.config.security.SuccessHandler;
import io.codeka.gaia.registries.RegistryOAuth2Provider;
import io.codeka.gaia.teams.OAuth2User;
import io.codeka.gaia.teams.User;
import io.codeka.gaia.teams.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Component
@Conditional(ClientsConfiguredCondition.class)
public class OAuth2SuccessHandler extends SuccessHandler {

private List<RegistryOAuth2Provider> registryOAuth2Providers;
private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

@Autowired
public OAuth2SuccessHandler(UserRepository userRepository, List<RegistryOAuth2Provider> registryOAuth2Providers,
OAuth2AuthorizedClientService oAuth2AuthorizedClientService) {
super(userRepository);
this.registryOAuth2Providers = registryOAuth2Providers;
this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// get user if exist, otherwise create a new one
var user = userRepository.findById(authentication.getName())
.orElse(new User(authentication.getName(), null));
// get oauth2 data
user.setOAuth2User(getOAuth2User((OAuth2AuthenticationToken) authentication));
userRepository.save(user);
redirect(request, response);
}

private OAuth2User getOAuth2User(OAuth2AuthenticationToken authentication) {
var client = oAuth2AuthorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
var user = (DefaultOAuth2User) authentication.getPrincipal();
return registryOAuth2Providers.stream()
.filter(p -> p.isAssignableFor(client.getClientRegistration().getRegistrationId()))
.map(p -> p.getOAuth2User(user, client))
.findFirst().orElse(null);
}
}
17 changes: 17 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,20 @@ gaia.dockerDaemonUrl=unix:///var/run/docker.sock

terraform.releases.url=https://releases.hashicorp.com/terraform/
terraform.releases.version.min=0.11.13

## oauth2 for gitlab
#spring.security.oauth2.client.registration.gitlab.client-id=<CLIENT_ID/>
#spring.security.oauth2.client.registration.gitlab.client-secret=<CLIENT_SECRET/>
#spring.security.oauth2.client.registration.gitlab.authorization-grant-type=authorization_code
#spring.security.oauth2.client.registration.gitlab.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
#spring.security.oauth2.client.provider.gitlab.authorization-uri=https://gitlab.com/oauth/authorize
#spring.security.oauth2.client.provider.gitlab.token-uri=https://gitlab.com/oauth/token
#spring.security.oauth2.client.provider.gitlab.user-info-uri=https://gitlab.com/api/v4/user
#spring.security.oauth2.client.provider.gitlab.user-name-attribute=username

## oauth2 for github
#spring.security.oauth2.client.registration.github.client-id=<CLIENT_ID/>
#spring.security.oauth2.client.registration.github.client-secret=<CLIENT_SECRET/>
#spring.security.oauth2.client.registration.github.authorization-grant-type=authorization_code
#spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
#spring.security.oauth2.client.provider.github.user-name-attribute=login
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.codeka.gaia.config.security.oauth2;

import io.codeka.gaia.test.MongoContainer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

@DirtiesContext
@Testcontainers
class OAuth2ClientSecurityConfigIT {

@Container
private static MongoContainer mongoContainer = new MongoContainer();

@Nested
@SpringBootTest
class OAuth2ClientSecurityConfigNotLoadedTest {
@Test
void oauth2ClientSecurityConfig_shouldNotBeInstantiated(
@Autowired(required = false) OAuth2ClientSecurityConfig oauth2ClientSecurityConfig) {
assertNull(oauth2ClientSecurityConfig);
}

@Test
void oAuth2SuccessHandler_shouldNotBeInstantiated(
@Autowired(required = false) OAuth2SuccessHandler oAuth2SuccessHandler) {
assertNull(oAuth2SuccessHandler);
}
}

@Nested
@SpringBootTest(properties = {
"spring.security.oauth2.client.registration.test_oauth2_client.client-id=ID",
"spring.security.oauth2.client.registration.test_oauth2_client.client-secret=SECRET",
"spring.security.oauth2.client.registration.test_oauth2_client.authorization-grant-type=authorization_code",
"spring.security.oauth2.client.registration.test_oauth2_client.redirect-uri=REDIRECT_URI",
"spring.security.oauth2.client.provider.test_oauth2_client.authorization-uri=AUTHORIZATION_URI",
"spring.security.oauth2.client.provider.test_oauth2_client.token-uri=TOKEN_URI",
"spring.security.oauth2.client.provider.test_oauth2_client.user-info-uri=USER_INFO_URI",
})
class OAuth2ClientSecurityConfigLoadedTest {
@Test
void oauth2ClientSecurityConfig_shouldBeInstantiated(
@Autowired(required = false) OAuth2ClientSecurityConfig oauth2ClientSecurityConfig) {
assertNotNull(oauth2ClientSecurityConfig);
}

@Test
void oAuth2SuccessHandler_shouldBeInstantiated(
@Autowired(required = false) OAuth2SuccessHandler oAuth2SuccessHandler) {
assertNotNull(oAuth2SuccessHandler);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package io.codeka.gaia.config.security.oauth2;

import io.codeka.gaia.registries.RegistryOAuth2Provider;
import io.codeka.gaia.teams.OAuth2User;
import io.codeka.gaia.teams.User;
import io.codeka.gaia.teams.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class OAuth2SuccessHandlerTest {

@Mock
UserRepository userRepository;

@Mock
OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

@Mock
HttpServletRequest request;

@Mock
HttpServletResponse response;

@Mock
OAuth2AuthenticationToken authentication;

@Mock
HttpSession httpSession;

private OAuth2SuccessHandler oAuth2SuccessHandler;
private List<RegistryOAuth2Provider> registryOAuth2Providers;

@BeforeEach
void setup() {
registryOAuth2Providers = new ArrayList<>();
oAuth2SuccessHandler = new OAuth2SuccessHandler(userRepository, registryOAuth2Providers, oAuth2AuthorizedClientService);

when(authentication.getName()).thenReturn("django");
when(request.getSession()).thenReturn(httpSession);
}

@Test
void onAuthenticationSuccess_shouldCreateUser_whenNotExists() throws IOException {
// when
when(userRepository.findById(anyString())).thenReturn(Optional.empty());
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

// then
var captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
assertThat(captor.getValue()).isNotNull()
.hasFieldOrPropertyWithValue("username", "django")
.hasFieldOrPropertyWithValue("team", null);
}

@Test
void onAuthenticationSuccess_shouldUpdateUser_whenExists() throws IOException {
// given
var user = new User("calvin", null);

// when
when(userRepository.findById(anyString())).thenReturn(Optional.of(user));
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

// then
verify(userRepository).save(user);
}

@Test
void onAuthenticationSuccess_shouldFillOAuth2User() throws IOException {
// given
var user = new User("frankie", null);
var oauth2User = new OAuth2User("tarantino", "unchained", null);
var client = mock(OAuth2AuthorizedClient.class);
var registration = ClientRegistration
.withRegistrationId("test_registration_id")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientId("test_client_id")
.redirectUriTemplate("test_uri_template")
.authorizationUri("test_authorization_uri")
.tokenUri("test_token_uri")
.build();
var principal = mock(DefaultOAuth2User.class);
var oauth2Provider = mock(RegistryOAuth2Provider.class);
registryOAuth2Providers.add(oauth2Provider);

// when
when(userRepository.findById(anyString())).thenReturn(Optional.of(user));
when(oAuth2AuthorizedClientService.loadAuthorizedClient(any(), anyString())).thenReturn(client);
when(oauth2Provider.isAssignableFor(anyString())).thenReturn(true);
when(oauth2Provider.getOAuth2User(principal, client)).thenReturn(oauth2User);
when(client.getClientRegistration()).thenReturn(registration);
when(authentication.getPrincipal()).thenReturn(principal);
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

// then
assertThat(user).hasFieldOrPropertyWithValue("oAuth2User", oauth2User);
}

@Test
void onAuthenticationSuccess_shouldRedirectToHomePage() throws IOException {
// when
when(userRepository.findById(anyString())).thenReturn(Optional.empty());
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

// then
verify(response).sendRedirect("/");
}

@Test
void onAuthenticationSuccess_shouldRedirectToAskedPage_whenSpecified() throws IOException {
// given
var savedRequest = mock(DefaultSavedRequest.class);

// when
when(userRepository.findById(anyString())).thenReturn(Optional.empty());
when(httpSession.getAttribute("SPRING_SECURITY_SAVED_REQUEST")).thenReturn(savedRequest);
when(savedRequest.getRequestURI()).thenReturn("/test_url");
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);

// then
verify(response).sendRedirect("/test_url");
}
}

0 comments on commit cf25f37

Please sign in to comment.