Skip to content

Commit

Permalink
gh-122 parametrized OAuth2 Authentications for @ParameterizedTest
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Jun 14, 2023
1 parent 4123d5f commit 36ca94f
Show file tree
Hide file tree
Showing 27 changed files with 946 additions and 310 deletions.
17 changes: 17 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ Using such libs is dead simple: just declare depedency on one of those libs and

2.0 comes with a noticeable amount of breaking changes. So lets start tracking features.

### 6.1.12
- [gh-122](https://github.com/ch4mpy/spring-addons/issues/122) Support for parametrized OAuth2 Authentications in `@ParameterizedTest`. Sample usage (mind the `@JwtAuthenticationSource` and `@ParameterizedJwtAuth`):
```java
@ParameterizedTest
@JwtAuthenticationSource({ @WithMockJwtAuth("NICE"), @WithMockJwtAuth("VERY_NICE") })
void givenUserIsGrantedWithAnyJwtAuthentication_whenGetRestricted_thenOk(@ParameterizedJwtAuth JwtAuthenticationToken auth) throws Exception {
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
}
```
The above will run two distinct tests in sequence, one with each of the provided `@WithMockJwtAuth`. Same for:
* `@WithMockBearerTokenAuthentication` with `@BearerAuthenticationSource` and `@ParameterizedBearerAuth`
* `@OpenId` with `@OpenIdAuthenticationSource` and `@ParameterizedOpenId`
* `@WithOAuth2Login` with `@OAuth2LoginAuthenticationSource` and `@ParameterizedOAuth2Login`
* `@WithOidcLogin` with `@OidcLoginAuthenticationSource` and `@ParameterizedOidcLogin`

### 6.1.11
- Spring Boot 3.1.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ public MyAuth authentication(WithMyAuth annotation) {
final var accessClaims = new OpenidClaimSet(super.claims(annotation.accessClaims()));
final var idClaims = new OpenidClaimSet(super.claims(annotation.idClaims()));

return new MyAuth(super.authorities(annotation.authorities()), annotation.accessTokenString(), accessClaims, annotation.idTokenString(), idClaims);
return new MyAuth(
super.authorities(annotation.authorities(), annotation.value()),
annotation.accessTokenString(),
accessClaims,
annotation.idTokenString(),
idClaims);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ public ProxiesAuthentication authentication(ProxiesAuth annotation) {
});
openidClaims.put("proxies", proxiesClaim);

return new ProxiesAuthentication(new ProxiesClaimSet(openidClaims), super.authorities(annotation.authorities()), annotation.bearerString());
return new ProxiesAuthentication(
new ProxiesClaimSet(openidClaims),
super.authorities(annotation.authorities(), annotation.value()),
annotation.bearerString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ scheme: http
keycloak-port: 8442
keycloak-issuer: ${scheme}://localhost:${keycloak-port}/realms/master
keycloak-secret: change-me
keycloak-client-id: spring-addons-confidential
cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
cognito-secret: change-me
auth0-issuer: https://dev-ch4mpy.eu.auth0.com/
Expand Down Expand Up @@ -33,13 +34,13 @@ spring:
keycloak-user:
authorization-grant-type: authorization_code
client-name: a local Keycloak instance
client-id: spring-addons-confidential
client-id: ${keycloak-client-id}
client-secret: ${keycloak-secret}
provider: keycloak
scope: openid,profile,email,offline_access
keycloak-programmatic:
authorization-grant-type: client_credentials
client-id: spring-addons-confidential
client-id: ${keycloak-client-id}
client-secret: ${keycloak-secret}
provider: keycloak
scope: openid,offline_access
Expand Down
10 changes: 5 additions & 5 deletions samples/tutorials/servlet-resource-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public class WebSecurityConfig {

@Bean
SecurityFilterChain
filterChain(HttpSecurity http, ServerProperties serverProperties, @Value("origins") String[] origins, @Value("permit-all") String[] permitAll)
filterChain(HttpSecurity http, ServerProperties serverProperties, @Value("${origins:[]}") String[] origins, @Value("${permit-all:[]}") String[] permitAll)
throws Exception {

http.oauth2ResourceServer(resourceServer -> resourceServer.jwt());
Expand Down Expand Up @@ -316,8 +316,8 @@ The last missing configuration piece is an update of the security filter-chain:
SecurityFilterChain filterChain(
HttpSecurity http,
ServerProperties serverProperties,
@Value("origins") String[] origins,
@Value("permit-all") String[] permitAll,
@Value("${origins:[]}") String[] origins,
@Value("${permit-all:[]}") String[] permitAll,
SpringAddonsProperties springAddonsProperties,
SpringAddonsJwtAuthenticationConverter authenticationConverter)
throws Exception {
Expand Down Expand Up @@ -373,8 +373,8 @@ Last, when configuring the resource server within the security filter-chain, we'
SecurityFilterChain filterChain(
HttpSecurity http,
ServerProperties serverProperties,
@Value("origins") String[] origins,
@Value("permit-all") String[] permitAll,
@Value("${origins:[]}") String[] origins,
@Value("${permit-all:[]}") String[] permitAll,
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver)
throws Exception {

Expand Down
13 changes: 10 additions & 3 deletions samples/tutorials/servlet-resource-server/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.c4-soft.springaddons.samples.tutorials</groupId>
Expand All @@ -9,7 +11,7 @@
</parent>
<artifactId>servlet-resource-server</artifactId>
<name>servlet-resource-server</name>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand All @@ -27,7 +29,7 @@
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
Expand All @@ -49,6 +51,11 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-oauth2-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public MessageDto getGreeting(Authentication auth) {
}

@GetMapping("/restricted")
@PreAuthorize("hasAuthority('NICE')")
@PreAuthorize("hasAnyAuthority('NICE', 'VERY_NICE')")
public MessageDto getRestricted() {
return new MessageDto("You are so nice!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,41 +54,38 @@ public class WebSecurityConfig {
SecurityFilterChain filterChain(
HttpSecurity http,
ServerProperties serverProperties,
@Value("origins") String[] origins,
@Value("permit-all") String[] permitAll,
@Value("${origins:[]}") String[] origins,
@Value("${permit-all:[]}") String[] permitAll,
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver)
throws Exception {

http.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));

// Enable anonymous
http.anonymous();

// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource(origins));
http.cors(cors -> cors.configurationSource(corsConfigurationSource(origins)));

// State-less session (state in access-token only)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// Disable CSRF because of state-less session-management
http.csrf().disable();
http.csrf(csrf -> csrf.disable());

// Return 401 (unauthorized) instead of 302 (redirect to login) when
// authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
http.exceptionHandling(eh -> eh.authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
}));

// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
}

// @formatter:off
http.authorizeHttpRequests()
http.authorizeHttpRequests(requests -> requests
.requestMatchers(permitAll).permitAll()
.anyRequest().authenticated();
.anyRequest().authenticated());
// @formatter:on

return http.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,38 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.test.web.servlet.MockMvc;

import com.c4_soft.springaddons.security.oauth2.OAuthentication;
import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenId;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockBearerTokenAuthentication;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithOAuth2Login;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithOidcLogin;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.BearerAuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.JwtAuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.OAuth2LoginAuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.OidcLoginAuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.OpenIdAuthenticationSource;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedBearerAuth;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedJwtAuth;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedOAuth2Login;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedOidcLogin;
import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedOpenId;

import jakarta.servlet.http.HttpServletRequest;

@WebMvcTest(controllers = GreetingController.class)
Expand Down Expand Up @@ -55,10 +78,70 @@ void givenUserIsNice_whenGetRestricted_thenOk() throws Exception {
}

@Test
void givenUserIsNotNicewhenGetRestricted_thenForbidden() throws Exception {
void givenUserIsNotNice_whenGetRestricted_thenForbidden() throws Exception {
// @formatter:off
api.perform(get("/restricted").with(SecurityMockMvcRequestPostProcessors.jwt().authorities(new SimpleGrantedAuthority("AUTHOR"))))
.andExpect(status().isForbidden());
// @formatter:on
}

@ParameterizedTest
@ValueSource(strings = { "NICE", "VERY_NICE" })
void givenUserIsGrantedWithAnyNiceAuthority_whenGetRestricted_thenOk(String authority) throws Exception {
// @formatter:off
api.perform(get("/restricted").with(SecurityMockMvcRequestPostProcessors.jwt().authorities(new SimpleGrantedAuthority(authority))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}

@ParameterizedTest
@JwtAuthenticationSource({ @WithMockJwtAuth("NICE"), @WithMockJwtAuth("VERY_NICE") })
void givenUserIsGrantedWithAnyJwtAuthentication_whenGetRestricted_thenOk(@ParameterizedJwtAuth JwtAuthenticationToken auth) throws Exception {
// @formatter:off
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}

@ParameterizedTest
@BearerAuthenticationSource({ @WithMockBearerTokenAuthentication("NICE"), @WithMockBearerTokenAuthentication("VERY_NICE") })
void givenUserIsGrantedWithAnyBearerAuthentication_whenGetRestricted_thenOk(@ParameterizedBearerAuth BearerTokenAuthentication auth) throws Exception {
// @formatter:off
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}

@ParameterizedTest
@OpenIdAuthenticationSource({ @OpenId("NICE"), @OpenId("VERY_NICE") })
void givenUserIsGrantedWithAnyOAuthentication_whenGetRestricted_thenOk(@ParameterizedOpenId OAuthentication<OpenidClaimSet> auth) throws Exception {
// @formatter:off
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}

@ParameterizedTest
@OAuth2LoginAuthenticationSource({ @WithOAuth2Login("NICE"), @WithOAuth2Login("VERY_NICE") })
void givenUserIsGrantedWithAnyOAuth2Login_whenGetRestricted_thenOk(@ParameterizedOAuth2Login OAuth2AuthenticationToken auth) throws Exception {
// @formatter:off
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}

@ParameterizedTest
@OidcLoginAuthenticationSource({ @WithOidcLogin("NICE"), @WithOidcLogin("VERY_NICE") })
void givenUserIsGrantedWithAnyOidcLogin_whenGetRestricted_thenOk(@ParameterizedOidcLogin OAuth2AuthenticationToken auth) throws Exception {
// @formatter:off
api.perform(get("/restricted"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.body").value("You are so nice!"));
// @formatter:on
}
}
4 changes: 4 additions & 0 deletions spring-addons-oauth2-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/*
* Copyright 2020 Jérôme Wacongne.
*
* 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
* 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
*
* https://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.
* 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.
*/
package com.c4_soft.springaddons.security.oauth2.test.annotations;

Expand All @@ -26,29 +25,28 @@

import com.c4_soft.springaddons.security.oauth2.test.OpenidClaimSetBuilder;

public abstract class AbstractAnnotatedAuthenticationBuilder<A extends Annotation, T extends Authentication>
implements WithSecurityContextFactory<A> {
public abstract class AbstractAnnotatedAuthenticationBuilder<A extends Annotation, T extends Authentication> implements WithSecurityContextFactory<A> {

protected abstract T authentication(A annotation);
protected abstract T authentication(A annotation);

@Override
public SecurityContext createSecurityContext(A annotation) {
final var context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication(annotation));
@Override
public SecurityContext createSecurityContext(A annotation) {
final var context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication(annotation));

return context;
}
return context;
}

public Set<GrantedAuthority> authorities(String... authorities) {
return Stream.of(authorities).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}
public Set<GrantedAuthority> authorities(String[] source1, String[] source2) {
return Stream.concat(Stream.of(source1), Stream.of(source2)).distinct().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
}

public OpenidClaimSetBuilder claims(OpenIdClaims annotation) {
return OpenIdClaims.Builder.of(annotation).usernameClaim(annotation.usernameClaim());
}
public OpenidClaimSetBuilder claims(OpenIdClaims annotation) {
return OpenIdClaims.Builder.of(annotation).usernameClaim(annotation.usernameClaim());
}

@SuppressWarnings("unchecked")
protected T downcast() {
return (T) this;
}
@SuppressWarnings("unchecked")
protected T downcast() {
return (T) this;
}
}
Loading

0 comments on commit 36ca94f

Please sign in to comment.