-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ : add configuration to handle oauth2 client connections
- Loading branch information
1 parent
e0a30bc
commit cf25f37
Showing
6 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
src/main/java/io/codeka/gaia/config/security/oauth2/OAuth2ClientSecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
src/main/java/io/codeka/gaia/config/security/oauth2/OAuth2SuccessHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
src/test/java/io/codeka/gaia/config/security/oauth2/OAuth2ClientSecurityConfigIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
148 changes: 148 additions & 0 deletions
148
src/test/java/io/codeka/gaia/config/security/oauth2/OAuth2SuccessHandlerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |