diff --git a/7.0.0-migration-guide.md b/7.0.0-migration-guide.md new file mode 100644 index 000000000..e7a12634b --- /dev/null +++ b/7.0.0-migration-guide.md @@ -0,0 +1,47 @@ +# Migration Guide from 6.x to 7.0.0 + +## Dependencies +- replace any spring-addons starter with `com.c4-soft.springaddons:spring-addons-starter-oidc` +- replace any spring-addons test starter with `com.c4-soft.springaddons:spring-addons-starter-oidc-test` +- depending or your needs, add a dependency to + * `org.springframework.boot:spring-boot-starter-oauth2-resource-server` for a REST API secured with access tokens + * `org.springframework.boot:spring-boot-starter-oauth2-client` when configuring `spring-cloud-gateway` as BFF or exposing server-side rendered templates with frameworks like Thymeleaf + * both of above when exposing publicly both a REST API secured with access tokens and other resources secured with sessions + +## Java Sources + +### Main Code +- rename `SpringAddonsSecurityProperties` to `SpringAddonsOidcProperties`. Also, if using nested properties, rename + * `getIssuers()` to `getOps()` + * `getLocation()` to `getIss()` +- replace `SpringAddonsOAuth2ClientProperties` with `SpringAddonsOidcProperties::getClient` (only `SpringAddonsOidcProperties` can be autowired) +- organize imports + +### Tests +- replace `@AutoConfigureAddonsSecurity` with `@AutoConfigureAddonsMinimalSecurity` +- replace `@AutoConfigureAddonsWebSecurity` with one of: + * `@AutoConfigureAddonsWebmvcSecurity` + * `@AutoConfigureAddonsWefluxSecurity` + +## Application Properties +This is probably the most tedious part of the migration. Hopefully, your IDE auto-completion and syntax highliting should help you there. + +### Common Configuration +- rename `com.c4-soft.springaddons.security` to `com.c4-soft.springaddons.oidc` +- rename `issuers` to `ops` which stands for OpenID Providers (`com.c4-soft.springaddons.security.issuers` becomes `com.c4-soft.springaddons.oidc.ops`) +- rename OpenID Providers `location` to `iss`: if set, the is used to add an "issuer" (tokens `iss` claim) validator to JWT decoder (`com.c4-soft.springaddons.security.issuers[].location` becomes `com.c4-soft.springaddons.oidc.ops[].iss`) +- rename`audience` to `aud`: if set, the is used to add an "audience" (tokens `aud` claim) validator to JWT decoder (`com.c4-soft.springaddons.security.issuers[].aud` becomes `com.c4-soft.springaddons.oidc.ops[].aud`) + +CORS configuration has also improved for both clients and resource servers: `allowed-origin-patterns` is used instead of `allowed-origins`. This is a requirement for using `allow-credentials` and is also more flexible: you can define ant patterns like `https://*.my-domain.pf`. +- rename `allowed-origins` to `allowed-origin-patterns` +- add `allow-credentials` and `max-age` if it makes sens (this are added configuration options) + +### Resource Servers +Resource server `Security(Web)FilterChain` can now be completely disabled with `com.c4-soft.springaddons.security.resourceserver.enabled=false` + +Resource server specific properties are grouped in a new `resourceserver` subset: +- move `cors` down 1 level into `resourceserver` (`com.c4-soft.springaddons.security.cors` becomes `com.c4-soft.springaddons.security.resourceserver.cors`) +- move `permit-all` down one level to `resourceserver` (`com.c4-soft.springaddons.security.permit-all` becomes `com.c4-soft.springaddons.security.resourceserver.permit-all`) + +### Clients +- rename `allowed-origins` to `allowed-origin-patterns` (`com.c4-soft.springaddons.security.client.cors.allowed-origins` becomes `com.c4-soft.springaddons.security.client.cors.allowed-origin-patterns`) diff --git a/README.MD b/README.MD index c3dcf2538..283a08de3 100644 --- a/README.MD +++ b/README.MD @@ -1,9 +1,15 @@ -Do not hesitate to fork this repo and send pull requests, even for things as small as a typo in READMEs or Javadoc. This would promote you as contributor. +7.0.0 is a break through in usability: all 6 `spring-addons` Boot starters are merged into a single one: [`com.c4-soft.springaddons:spring-addons-starter-oidc`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-starter-oidc/), and so are the 4 test libs: [`com.c4-soft.springaddons:spring-addons-starter-oidc-test`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-starter-oidc-test/) (if you are using just the test annotations, without the starter, the dependency is still the same: [`com.c4-soft.springaddons:spring-addons-oauth2-test`](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons-oauth2-test/)). -# Spring-addons +Please follow the [migration guide](https://github.com/ch4mpy/spring-addons/blob/master/7.0.0-migration-guide.md) to bump from `6.x` to `7.0.0`. Also, all samples and tutorials are migrated to latest starter and test annotations. + +# Configuring and Testing OAuth2 / OpenID Spring applications Made Easy The libraries hosted in this repo shine in two domains: -- provide with annotations to mock OAuth2 `Authentication` during tests (`@WithMockJwtAuth`, `@WithOAuth2Login`, `@WithOidcLogin`, `@WithMockBearerTokenAuthentication`, etc.), which allow to test method security on any `@Component`. **New in 6.1.12: `@JwtAuthenticationSource` and alike to work with JUnit 5 `@ParameterizedTest`**. Details below. -- help configuring Spring Boot 3 applications OAuth2 configuration by pushing auto-configuration to the next level. As shown in **[Tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials)**, with 0 Java conf (just properties), we can configure: +- providing with annotations to mock OAuth2 `Authentication` for JUnit `@Test`: + * `@WithMockAuthentication` with `@AuthenticationSource` and `@ParameterizedAuthentication` companions for JUnit 5 @ParameterizedTest + * `@WithJwt` which uses the JWT authentication converter defined in security configuration to build the right type of `Authentication` (with the right authorities and name) based on a JSON file on he classpath (or plain Java String + * `@WithOpaqueToken` same as `@WithJwt` for introspection, using the `OpaqueTokenAuthenticationConverter` in the security configuration + * more specialized annotations for specific authentication implementations (`@WithOAuth2Login`, `@WithOidcLogin`, etc.) or to use as elements for your own test annotations in applications using custom OAuth2 `Authentication` implementations +- pushing OIDC auto-configuration to the next level in Spring Boot 3 applications. As shown in **[Tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials)**, with 0 Java conf (just properties), we can configure: * authorities mapping (source claims, prefix and case transformation), without having to provide authentication converter, user service or `GrantedAuthoritiesMapper` in each app * fine grained CORS configuration (per path matcher), which enables to override allowed origins as environment variable when switching from `localhost` to `dev` or `prod` environments * sessions & CSRF disabled by default on resource server and enabled on clients. If a cookie repo is chosen for CSRF (as required by Angular, React, Vue, etc.), then the right request handler is configured and a filter to actually set the cookie is added @@ -16,7 +22,7 @@ The libraries hosted in this repo shine in two domains: Jump to: - [1. Unit & Integration Testing With Security](#unit-tests) -- [2. Spring Boot OAuth2 Starters](#oauth2-starters) +- [2. Spring Boot OIDC Starter](#oauth2-starters) - [3. Where to Start](#start) - [4. Versions & Requirements](#versions) - [5. Additional Modules](#additional-modules) @@ -32,6 +38,7 @@ An [article covering the usage of OAuth2 test annotations from this lib](https:/ However, since this article was published, test annotations have improved. +### 1.1. Sample Let's consider the following secured `@Service` ```java @Service @@ -51,8 +58,7 @@ Now, let's assume that you have a staging environment with a few representative `@WithJwt` and `@WithOpaqueToken` enable to load those claim-sets and turn it into `Authentication` instances using the authentication converter from your security configuration, and as so, with the same type, authorities, name and claims as at runtime. ```java -@AutoConfigureAddonsSecurity -@EnableAutoConfiguration(exclude = { AddonsWebSecurityBeans.class, SpringAddonsOAuth2ClientBeans.class }) // Only AddonsSecurityBeans is required for this test +@AddonsWebmvcComponentTest @SpringBootTest(classes = { SecurityConfig.class, MessageService.class }) class MessageServiceTests { @@ -86,22 +92,28 @@ class MessageServiceTests { } ``` There are we few things worth noting above: -- annotation fit so well with BDD (given-when-then) and are very brief +- annotations fit so well with BDD (given-when-then): the test pre-conditions (given) are decorating the test instead of cluttering its content like MockMvc request post-processors and WebTestClient mutators +- annotations can be very brief and expressive - we are testing a `@Service` having methods decorated with `@PreAuthorize` (without `MockMvc` or `WebTestClient`) - the claims are loaded from a JSON files in the test classpath - we are using JUnit 5 `@ParameterizedTest`: the test will run once for each of the authentication in the stream provided by the `identities` method -- the claim used as `username` is a potentially a nested claim resolved with JSON-Path +- the claim used as `username` potentially is a nested claim resolved with JSON-Path + +### 1.2. Which Dependency / Annotation to Use +`spring-addons-oauth2-test` is enough to use test annotations, but if you opted for `spring-addons-starter-oidc`, then `spring-addons-starter-oidc-test` is better suited as it comes with tooling to load spring-addons auto-configuration during tests (refer to the many samples for usage). + +`@WithMockAuthentication` should be enough to test applications with RBAC (role-based access control): it allows to easily define `name` and `authorities`, as well as the Authentication an principal types to mock if your application code expects something specific. + +In case your access-control uses more than just name and authorities, and you'll probably need to define claim-set details. `@WithJwt` and `@WithOpaqueToken` can come pretty handy as it uses the respectively the JWT or OpaqueToken authentication converter in your security configuration to build the authentication instance, using a JSON payload from the classpath (or a plain Java String): you might just dump payloads of access tokens for representative users in test resources (use a tool like https://jwt.io to easily get those payloads). + +## 2. Spring Boot Starter +**This starters is designed to push auto-configuration to the next level** and does nothing more than helping you with auto-configuration from application properties. -## 2. Spring Boot Starters -**This starters are designed to push auto-configuration to the next level** (and does nothing more than helping you with auto-configuration from applicationproperties). In most cases, you should need 0 Java conf. An effort was made to make [tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials), Javadoc and modules READMEs as informative as possible. Please refer there for more details. +In most cases, you should need 0 Java conf. An effort was made to make [tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials), Javadoc and modules READMEs as informative as possible. Please refer there for more details. -Spring Boot starters are thin wrappers around `spring-boot-starter-oauth2-resource-server` or `spring-boot-starter-oauth2-client`: -- [spring-addons-webflux-client](https://github.com/ch4mpy/spring-addons/tree/master/webflux/spring-addons-webflux-client) to be used in reactive applications rendering templates on the server (Thymeleaf, JSF, etc.), **or in `spring-cloud-gateway` used as BFF** (server-side OAuth2 confidential client securing a browser application with sessions and replacing session cookies with OAuth2 access tokens before forwarding requests from browsers to resource servers) -- [spring-addons-webflux-introspecting-resource-server](https://github.com/ch4mpy/spring-addons/tree/master/webflux/spring-addons-webflux-introspecting-resource-server) to be used in reactive REST APIs secured with access token introspection -- [spring-addons-webflux-jwt-resource-server](https://github.com/ch4mpy/spring-addons/tree/master/webflux/spring-addons-webflux-jwt-resource-server) to be used in **reactive REST APIs secured with JWT decoders** -- [spring-addons-webmvc-client](https://github.com/ch4mpy/spring-addons/tree/master/webmvc/spring-addons-webmvc-client) to be used in servlet applications rendering templates on the server (Thymeleaf, JSF, etc.) -- [spring-addons-webmvc-introspecting-resource-server](https://github.com/ch4mpy/spring-addons/tree/master/webmvc/spring-addons-webmvc-introspecting-resource-server) to be used in servlet REST APIs secured with access token introspection -- [spring-addons-webmvc-jwt-resource-server](https://github.com/ch4mpy/spring-addons/tree/master/webmvc/spring-addons-webmvc-jwt-resource-server) to be used in **servlet REST APIs secured with JWT decoders** +`spring-addons-oidc-starter` does not replace `spring-boot-starter-oauth2-resource-server` and `spring-boot-starter-oauth2-client`, it defines a few beans designed to be picked by it. + +If you are curious enough, you might inspect what is auto-configured (and under which conditions) by reading the source code, starting from the [org.springframework.boot.autoconfigure.AutoConfiguration.imports](https://github.com/ch4mpy/spring-addons/blob/master/spring-addons-starter-oidc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) file, which is the Spring Boot standard entry-point defining what is loaded when a jar is on the classpath. ## 3. Where to Start [Tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials) which cover: @@ -119,38 +131,34 @@ Spring Boot starters are thin wrappers around `spring-boot-starter-oauth2-resour * `OAuthentication` / Spring default `Authentication` implementation (`JwtAuthenticationToken` for JWT decoder or `BearerTokenAuthentication` for token introspection) ## 4. Versions & Requirements -6.x branch is designed for spring-boot 3 and requires JDK 17 as minimum. - -If locked wtih a lower JDK or spring-boot version, you'll have to use a 5.4.x release wich are made with **JDK 1.8** and spring 2.6 (boot auto loading mechanisms have change with 2.7). But be aware that some of the features documented on main branch can be missing or behave differently. +6.x and 7.X branch are designed for spring-boot 3 and requires JDK 17 as minimum. I could forget to update README before releasing, so please refer to [maven central](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons/) to pick latest available release ```xml - 6.2.0 - webmvc - jwt + 7.0.0 - + com.c4-soft.springaddons - spring-addons-${app-type}-${token}-resource-server + spring-addons-starter-oidc ${springaddons.version} com.c4-soft.springaddons - spring-addons-${app-type}-${token}-test + spring-addons-starter-oidc-test ${springaddons.version} test - - + com.c4-soft.springaddons spring-addons-oauth2-test ${springaddons.version} + test @@ -180,6 +188,11 @@ 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. +### 7.0.0 +See the [migration guide](https://github.com/ch4mpy/spring-addons/blob/master/7.0.0-migration-guide.md) +- merge all 6 starters into a single one +- reduce test libs count to 2: one with just annotations and another to ease testing of apps using the starter + ### 6.2.0 - remove `OAuth2AuthenticationFactory`: instead, use `Converter`, `Converter>`, `OpaqueTokenAuthenticationConverter` or `ReactiveOpaqueTokenAuthenticationConverter` - create `@WithJwt` to build OAuth2 `Authentication` during tests, using a JSON string or file on the classpath and submitting it to the JWT authentication converter. All samples and tutorials are updated with this new annotation. diff --git a/pom.xml b/pom.xml index 11ab9e2f3..32dad78a8 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ scm:git:git://github.com/ch4mpy/spring-addons.git scm:git:git@github.com:ch4mpy/spring-addons.git https://github.com/ch4mpy/spring-addons - spring-addons-6.1.14 + HEAD @@ -101,88 +101,15 @@ spring-addons-oauth2-test ${project.version} - - com.c4-soft.springaddons - spring-addons-web-test - ${project.version} - - - - com.c4-soft.springaddons - spring-addons-webmvc-core - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webmvc-introspecting-resource-server - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webmvc-client - ${project.version} - com.c4-soft.springaddons - spring-addons-webflux-core - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webflux-introspecting-resource-server - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webflux-jwt-resource-server + spring-addons-starter-oidc ${project.version} com.c4-soft.springaddons - spring-addons-webflux-client - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webmvc-introspecting-test - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webmvc-jwt-test - ${project.version} - - - - com.c4-soft.springaddons - spring-addons-webmvc-test - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webflux-test - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webflux-introspecting-test - ${project.version} - - - com.c4-soft.springaddons - spring-addons-webflux-jwt-test - ${project.version} - - - - com.c4-soft.springaddons - spring-addons-keycloak + spring-addons-starter-oidc-test ${project.version} @@ -392,12 +319,11 @@ release - starters spring-addons-oauth2 - spring-addons-web-test spring-addons-oauth2-test - webmvc - webflux + spring-addons-starter-oidc + spring-addons-starter-oidc-test + starters @@ -432,12 +358,11 @@ true - starters spring-addons-oauth2 - spring-addons-web-test spring-addons-oauth2-test - webmvc - webflux + spring-addons-starter-oidc + spring-addons-starter-oidc-test + starters samples diff --git a/samples/pom.xml b/samples/pom.xml index 4eb26df12..955da768f 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -87,7 +87,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc ${project.version} @@ -97,7 +97,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-test + spring-addons-starter-oidc-test ${project.version} diff --git a/samples/tutorials/bff/gateway/pom.xml b/samples/tutorials/bff/gateway/pom.xml index d7ecbbd50..d11686315 100644 --- a/samples/tutorials/bff/gateway/pom.xml +++ b/samples/tutorials/bff/gateway/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 com.c4-soft.springaddons.samples.tutorials.bff @@ -10,7 +12,7 @@ bff-gateway BFF gateway Backend For Frontend - + org.springframework.boot @@ -30,7 +32,7 @@ com.c4-soft.springaddons - spring-addons-webflux-client + spring-addons-starter-oidc io.swagger.core.v3 diff --git a/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java b/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java index 186ae1ebc..3db6a00b2 100644 --- a/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java +++ b/samples/tutorials/bff/gateway/src/main/java/com/c4soft/springaddons/samples/bff/gateway/GatewayController.java @@ -21,9 +21,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; -import com.c4_soft.springaddons.security.oauth2.config.LogoutRequestUriBuilder; -import com.c4_soft.springaddons.security.oauth2.config.SpringAddonsOAuth2ClientProperties; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.starter.LogoutRequestUriBuilder; +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcClientProperties; +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -35,7 +36,7 @@ @Tag(name = "Gateway") public class GatewayController { private final ReactiveClientRegistrationRepository clientRegistrationRepository; - private final SpringAddonsOAuth2ClientProperties addonsClientProps; + private final SpringAddonsOidcClientProperties addonsClientProperties; private final LogoutRequestUriBuilder logoutRequestUriBuilder; private final ServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository(); private final List loginOptions; @@ -43,13 +44,16 @@ public class GatewayController { public GatewayController( OAuth2ClientProperties clientProps, ReactiveClientRegistrationRepository clientRegistrationRepository, - SpringAddonsOAuth2ClientProperties addonsClientProps, + SpringAddonsOidcProperties addonsProperties, LogoutRequestUriBuilder logoutRequestUriBuilder) { - this.addonsClientProps = addonsClientProps; + this.addonsClientProperties = addonsProperties.getClient(); this.clientRegistrationRepository = clientRegistrationRepository; this.logoutRequestUriBuilder = logoutRequestUriBuilder; this.loginOptions = clientProps.getRegistration().entrySet().stream().filter(e -> "authorization_code".equals(e.getValue().getAuthorizationGrantType())) - .map(e -> new LoginOptionDto(e.getValue().getProvider(), "%s/oauth2/authorization/%s".formatted(addonsClientProps.getClientUri(), e.getKey()))) + .map( + e -> new LoginOptionDto( + e.getValue().getProvider(), + "%s/oauth2/authorization/%s".formatted(addonsClientProperties.getClientUri(), e.getKey()))) .toList(); } @@ -83,11 +87,11 @@ public Mono> logout(ServerWebExchange exchange, Authenticat if (authentication instanceof OAuth2AuthenticationToken oauth && oauth.getPrincipal() instanceof OidcUser oidcUser) { uri = clientRegistrationRepository.findByRegistrationId(oauth.getAuthorizedClientRegistrationId()).map(clientRegistration -> { final var uriString = logoutRequestUriBuilder - .getLogoutRequestUri(clientRegistration, oidcUser.getIdToken().getTokenValue(), addonsClientProps.getPostLogoutRedirectUri()); - return StringUtils.hasText(uriString) ? URI.create(uriString) : addonsClientProps.getPostLogoutRedirectUri(); + .getLogoutRequestUri(clientRegistration, oidcUser.getIdToken().getTokenValue(), addonsClientProperties.getPostLogoutRedirectUri()); + return StringUtils.hasText(uriString) ? URI.create(uriString) : addonsClientProperties.getPostLogoutRedirectUri(); }); } else { - uri = Mono.just(addonsClientProps.getPostLogoutRedirectUri()); + uri = Mono.just(addonsClientProperties.getPostLogoutRedirectUri()); } return uri.flatMap(logoutUri -> { return securityContextRepository.save(exchange, null).thenReturn(logoutUri); diff --git a/samples/tutorials/bff/gateway/src/main/resources/application.yml b/samples/tutorials/bff/gateway/src/main/resources/application.yml index 4f05f4a5d..b8e6cee80 100644 --- a/samples/tutorials/bff/gateway/src/main/resources/application.yml +++ b/samples/tutorials/bff/gateway/src/main/resources/application.yml @@ -98,18 +98,18 @@ spring: com: c4-soft: springaddons: - security: - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: ${cognito-issuer} + - iss: ${cognito-issuer} username-claim: username authorities: - path: cognito:groups - - location: ${auth0-issuer} + - iss: ${auth0-issuer} username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] diff --git a/samples/tutorials/bff/greetings-api/pom.xml b/samples/tutorials/bff/greetings-api/pom.xml index bc0c521a5..1828d27e9 100644 --- a/samples/tutorials/bff/greetings-api/pom.xml +++ b/samples/tutorials/bff/greetings-api/pom.xml @@ -20,6 +20,10 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-validation @@ -30,7 +34,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc io.swagger.core.v3 @@ -55,7 +59,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java index 119f4d3f7..5f5436d36 100644 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java +++ b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/UsersApiApplication.java @@ -12,10 +12,10 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.JwtClaimNames; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; -import com.c4_soft.springaddons.security.oauth2.config.JwtAbstractAuthenticationTokenConverter; -import com.c4_soft.springaddons.security.oauth2.config.SpringAddonsSecurityProperties; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Info; @@ -35,9 +35,9 @@ public static class WebSecurityConfig { @Bean JwtAbstractAuthenticationTokenConverter authenticationConverter( Converter, Collection> authoritiesConverter, - SpringAddonsSecurityProperties addonsProperties) { + SpringAddonsOidcProperties addonsProperties) { return jwt -> new OAuthentication<>( - new OpenidClaimSet(jwt.getClaims(), addonsProperties.getIssuerProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()), + new OpenidClaimSet(jwt.getClaims(), addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()), authoritiesConverter.convert(jwt.getClaims()), jwt.getTokenValue()); }; diff --git a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java index b784e2cb8..544233f10 100644 --- a/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java +++ b/samples/tutorials/bff/greetings-api/src/main/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsController.java @@ -5,8 +5,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/samples/tutorials/bff/greetings-api/src/main/resources/application.yml b/samples/tutorials/bff/greetings-api/src/main/resources/application.yml index dee0684b2..4e776418e 100644 --- a/samples/tutorials/bff/greetings-api/src/main/resources/application.yml +++ b/samples/tutorials/bff/greetings-api/src/main/resources/application.yml @@ -24,30 +24,32 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: ${cognito-issuer} + - iss: ${cognito-issuer} username-claim: username authorities: - path: cognito:groups - - location: ${auth0-issuer} + - iss: ${auth0-issuer} username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] - path: $.permissions - permit-all: - - "/public/**" - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" + resourceserver: + enabled: true + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/public/**" + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" logging: level: diff --git a/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java index 15403f9ec..0822c9c0a 100644 --- a/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java +++ b/samples/tutorials/bff/greetings-api/src/test/java/com/c4soft/springaddons/samples/bff/users/web/GreetingsControllerTest.java @@ -5,25 +5,27 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.test.context.support.WithAnonymousUser; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.jwt.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @WebMvcTest(controllers = GreetingsController.class) -@AutoConfigureAddonsWebSecurity +@AutoConfigureAddonsWebmvcResourceServerSecurity class GreetingsControllerTest { @Autowired MockMvcSupport api; @Test - void givenRequestIsNotAuthorized_whenGetMe_thenUnauthorized() throws Exception { + @WithAnonymousUser + void givenRequestIsAnonymous_whenGetGreetings_thenUnauthorized() throws Exception { api.get("/greetings").andExpect(status().isUnauthorized()); } @Test @WithJwt("ch4mp_auth0.json") - void givenUserIsAuthenticated_whenGetMe_thenOk() throws Exception { + void givenUserIsAuthenticated_whenGetGreetings_thenOk() throws Exception { api.get("/greetings").andExpect(status().isOk()); } diff --git a/samples/tutorials/reactive-client/pom.xml b/samples/tutorials/reactive-client/pom.xml index e0a9f640e..918f83d88 100644 --- a/samples/tutorials/reactive-client/pom.xml +++ b/samples/tutorials/reactive-client/pom.xml @@ -64,6 +64,11 @@ spring-boot-configuration-processor true + + com.c4-soft.springaddons + spring-addons-oauth2-test + test + diff --git a/samples/tutorials/reactive-client/src/test/java/com/c4soft/springaddons/tutorials/ReactiveClientApplicationTest.java b/samples/tutorials/reactive-client/src/test/java/com/c4soft/springaddons/tutorials/ReactiveClientApplicationTest.java index dc3818e2e..55ebb07ac 100644 --- a/samples/tutorials/reactive-client/src/test/java/com/c4soft/springaddons/tutorials/ReactiveClientApplicationTest.java +++ b/samples/tutorials/reactive-client/src/test/java/com/c4soft/springaddons/tutorials/ReactiveClientApplicationTest.java @@ -3,9 +3,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.time.Instant; import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; @@ -18,9 +24,19 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.OidcLoginMutator; import org.springframework.test.web.reactive.server.WebTestClient; +import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithOidcLogin; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.OidcLoginAuthenticationSource; + @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureWebTestClient @Import(ReactiveClientApplicationTest.TestSecurityConf.class) @@ -31,24 +47,51 @@ class ReactiveClientApplicationTest { @Autowired WebTestClient webTestClient; - @Test - void givenRequestIsNotAuthorized_whenGetIndex_thenIsOk() throws Exception { - webTestClient.get().uri("/").exchange().expectStatus().isOk(); - } - @Test void givenUserIsAnonymous_whenGetIndex_thenIsOk() throws Exception { webTestClient.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)).get().uri("/").exchange().expectStatus().isOk(); } @Test - void givenUserIsAuthenticated_whenGetIndex_thenIsOk() throws Exception { - webTestClient.mutateWith(SecurityMockServerConfigurers.mockOidcLogin()).get().uri("/").exchange().expectStatus().isOk(); + @WithAnonymousUser + void givenUserIsAnonymousAnnotation_whenGetIndex_thenIsOk() throws Exception { + webTestClient.get().uri("/").exchange().expectStatus().isOk(); } - @Test - void givenRequestIsNotAuthorized_whenGetLogin_thenIsOk() throws Exception { - webTestClient.get().uri("/login").exchange().expectStatus().isOk(); + @ParameterizedTest + @MethodSource("identityMutators") + void givenUserIsAuthenticated_whenGetIndex_thenIsOk(OidcLoginMutator identityMutator) throws Exception { + // @formatter:off + webTestClient.mutateWith(identityMutator) + .get().uri("/").exchange() + .expectStatus().isOk(); + // @formatter:on + } + + static Stream identityMutators() { + Instant iat = Instant.now(); + Instant exp = iat.plusSeconds(42); + return Stream.of( + SecurityMockServerConfigurers.mockOidcLogin().oidcUser( + new DefaultOidcUser( + List.of(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR")), + new OidcIdToken("test.token", iat, exp, Map.of(JwtClaimNames.SUB, "ch4mp")))), + SecurityMockServerConfigurers.mockOidcLogin().oidcUser( + new DefaultOidcUser( + List.of(new SimpleGrantedAuthority("UNCLE"), new SimpleGrantedAuthority("SKIPPER")), + new OidcIdToken("test.token", iat, exp, Map.of(JwtClaimNames.SUB, "tonton-pirate"))))); + } + + @ParameterizedTest + @OidcLoginAuthenticationSource({ + @WithOidcLogin( + authorities = { "NICE", "AUTHOR" }, + claims = @OpenIdClaims(usernameClaim = StandardClaimNames.PREFERRED_USERNAME, preferredUsername = "ch4mp")), + @WithOidcLogin( + authorities = { "UNCLE", "SKIPPER" }, + claims = @OpenIdClaims(usernameClaim = StandardClaimNames.PREFERRED_USERNAME, preferredUsername = "tonton-pirate")) }) + void givenUserIsAuthenticatedWithAnnotation_whenGetIndex_thenIsOk() throws Exception { + webTestClient.get().uri("/").exchange().expectStatus().isOk(); } @Test @@ -56,14 +99,21 @@ void givenUserIsAnonymous_whenGetLogin_thenIsOk() throws Exception { webTestClient.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)).get().uri("/login").exchange().expectStatus().isOk(); } + @Test + @WithAnonymousUser + void givenUserIsAnonymousAnnotation_whenGetLogin_thenIsOk() throws Exception { + webTestClient.get().uri("/login").exchange().expectStatus().isOk(); + } + @Test void givenUserIsAuthenticated_whenGetLogin_thenIsRedirected() throws Exception { webTestClient.mutateWith(SecurityMockServerConfigurers.mockOidcLogin()).get().uri("/login").exchange().expectStatus().is3xxRedirection(); } @Test - void givenRequestIsNotAuthorized_whenGetNice_thenIsRedirected() throws Exception { - webTestClient.get().uri("/nice.html").exchange().expectStatus().is3xxRedirection(); + @WithOidcLogin + void givenUserIsAuthenticatedWithAnnotation_whenGetLogin_thenIsRedirected() throws Exception { + webTestClient.get().uri("/login").exchange().expectStatus().is3xxRedirection(); } @Test @@ -72,17 +122,35 @@ void givenUserIsAnonymous_whenGetNice_thenIsRedirected() throws Exception { .is3xxRedirection(); } + @Test + @WithAnonymousUser + void givenUserIsAnonymousAnnotation_whenGetNice_thenIsRedirected() throws Exception { + webTestClient.get().uri("/nice.html").exchange().expectStatus().is3xxRedirection(); + } + @Test void givenUserIsNice_whenGetNice_thenIsOk() throws Exception { webTestClient.mutateWith(SecurityMockServerConfigurers.mockOidcLogin().authorities(new SimpleGrantedAuthority("NICE"))).get().uri("/nice.html") .exchange().expectStatus().isOk(); } + @Test + @WithOidcLogin("NICE") + void givenUserIsNiceAnnotation_whenGetNice_thenIsOk() throws Exception { + webTestClient.get().uri("/nice.html").exchange().expectStatus().isOk(); + } + @Test void givenUserIsNotNice_whenGetNice_thenIsForbidden() throws Exception { webTestClient.mutateWith(SecurityMockServerConfigurers.mockOidcLogin()).get().uri("/nice.html").exchange().expectStatus().isForbidden(); } + @Test + @WithOidcLogin + void givenUserIsNotNiceAnnotation_whenGetNice_thenIsForbidden() throws Exception { + webTestClient.get().uri("/nice.html").exchange().expectStatus().isForbidden(); + } + @TestConfiguration static class TestSecurityConf { @Bean diff --git a/samples/tutorials/reactive-resource-server/pom.xml b/samples/tutorials/reactive-resource-server/pom.xml index 09f903ceb..36a1142eb 100644 --- a/samples/tutorials/reactive-resource-server/pom.xml +++ b/samples/tutorials/reactive-resource-server/pom.xml @@ -50,6 +50,11 @@ spring-boot-configuration-processor true + + com.c4-soft.springaddons + spring-addons-oauth2-test + test + diff --git a/samples/tutorials/reactive-resource-server/src/main/resources/application.yml b/samples/tutorials/reactive-resource-server/src/main/resources/application.yml index 73a71f5a1..6a136d3b7 100644 --- a/samples/tutorials/reactive-resource-server/src/main/resources/application.yml +++ b/samples/tutorials/reactive-resource-server/src/main/resources/application.yml @@ -30,6 +30,7 @@ spring-addons: claims: - jsonPath: $.cognito:groups - uri: ${auth0-issuer} + username-json-path: $['https://c4-soft.com/user']['name'] claims: - jsonPath: $.roles - jsonPath: $.groups diff --git a/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index 9f34e3072..39a3dc5ec 100644 --- a/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -1,21 +1,46 @@ package com.c4soft.springaddons.tutorials; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.JwtMutator; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.server.ServerWebExchange; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.AuthenticationSource; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; import com.c4soft.springaddons.tutorials.GreetingController.MessageDto; +import reactor.core.publisher.Mono; + @WebFluxTest(controllers = GreetingController.class, properties = "server.ssl.enabled=false") @Import({ WebSecurityConfig.class }) +@TestInstance(Lifecycle.PER_CLASS) // needed only when using non-static @MethodSource class GreetingControllerTest { + static final AnonymousAuthenticationToken ANONYMOUS = + new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); @MockBean ReactiveAuthenticationManagerResolver authenticationManagerResolver; @@ -23,13 +48,31 @@ class GreetingControllerTest { @Autowired WebTestClient api; + // needed only when using @ParameterizedTests with WithJwt.AuthenticationFactory + @Autowired + Converter> authenticationConverter; + + WithJwt.AuthenticationFactory jwtAuthFactory; + + @BeforeEach + public void setUp() { + jwtAuthFactory = new WithJwt.AuthenticationFactory(Optional.empty(), Optional.of(authenticationConverter)); + } + @Test - void givenRequestIsNotAuthorized_whenGreet_thenUnauthorized() throws Exception { - api.get().uri("/greet").exchange().expectStatus().isUnauthorized(); + void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception { + api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)).get().uri("/greet").exchange().expectStatus().isUnauthorized(); } @Test - void givenUserAuthenticated_whenGetGreet_thenOk() throws Exception { + @WithAnonymousUser + void givenUserIsAnonymous_whenGreet_thenUnauthorized() throws Exception { + api.get().uri("/greet").exchange().expectStatus().isUnauthorized(); + } + + @ParameterizedTest + @MethodSource("identityMutators") + void givenUserIsAuthenticated_whenGreet_thenOk(JwtMutator identityMutator) throws Exception { // @formatter:off api.mutateWith(SecurityMockServerConfigurers.mockJwt() .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR"))) @@ -39,8 +82,42 @@ void givenUserAuthenticated_whenGetGreet_thenOk() throws Exception { // @formatter:on } + static Stream identityMutators() { + return Stream.of( + SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.subject("ch4mp")) + .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR")), + SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.subject("tonton-pirate")) + .authorities(new SimpleGrantedAuthority("UNCLE"), new SimpleGrantedAuthority("SKIPPER"))); + } + + @ParameterizedTest + @AuthenticationSource({ + @WithMockAuthentication(name = "ch4mp", authorities = { "NICE", "AUTHOR" }), + @WithMockAuthentication(name = "tonton-pirate", authorities = { "UNCLE", "SKIPPER" }) }) + void givenUserIsAuthenticatedWithMockAuthentication_whenGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getName(), auth.getAuthorities()))); + // @formatter:on + } + + @ParameterizedTest + @MethodSource("jwts") + void givenUserIsAuthenticatedWithJwt_whenGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getName(), auth.getAuthorities()))); + // @formatter:on + } + + Stream jwts() { + return jwtAuthFactory.authenticationsFrom("auth0_badboy.json", "auth0_nice.json"); + } + @Test - void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { + void givenUserHasNiceMutator_whenGetRestricted_thenOk() throws Exception { // @formatter:off api.mutateWith(SecurityMockServerConfigurers.mockJwt() .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR"))) @@ -51,17 +128,66 @@ void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { } @Test - void givenUserIsNotNice_whenGetRestricted_thenForbidden() throws Exception { + @WithMockAuthentication({ "NICE", "AUTHOR" }) + void givenUserHasNiceMockAuthentication_whenGetRestricted_thenOk() throws Exception { // @formatter:off - api.mutateWith(SecurityMockServerConfigurers.mockJwt() - .authorities(new SimpleGrantedAuthority("AUTHOR"))) + api.get().uri("/restricted").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("You are so nice!")); + // @formatter:on + } + + @Test + @WithJwt("auth0_nice.json") + void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { + // @formatter:off + api.get().uri("/restricted").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("You are so nice!")); + // @formatter:on + } + + @Test + void givenUserHasNotNiceMutator_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) .get().uri("/restricted").exchange() .expectStatus().isForbidden(); // @formatter:on } @Test - void givenRequestIsNotAuthorized_whenGetRestricted_thenUnauthorized() throws Exception { + @WithMockAuthentication("AUTHOR") + void givenUserHasNotNiceMockAuthentication_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) + .get().uri("/restricted").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + @WithJwt("auth0_badboy.json") + void givenUserIsBadboy_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) + .get().uri("/restricted").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + void givenUserHasAnonymousMutator_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)) + .get().uri("/restricted").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithAnonymousUser + void givenUserIsAnonymous_whenGetRestricted_thenUnauthorized() throws Exception { api.get().uri("/restricted").exchange().expectStatus().isUnauthorized(); } } diff --git a/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/ReactiveResourceServerApplicationTests.java b/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/ReactiveResourceServerApplicationTests.java index 79fe9424e..2d407892a 100644 --- a/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/ReactiveResourceServerApplicationTests.java +++ b/samples/tutorials/reactive-resource-server/src/test/java/com/c4soft/springaddons/tutorials/ReactiveResourceServerApplicationTests.java @@ -1,23 +1,47 @@ package com.c4soft.springaddons.tutorials; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.JwtMutator; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.server.ServerWebExchange; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.AuthenticationSource; +import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; import com.c4soft.springaddons.tutorials.GreetingController.MessageDto; +import reactor.core.publisher.Mono; + @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureWebTestClient +@TestInstance(Lifecycle.PER_CLASS) // needed only when using non-static @MethodSource class ReactiveResourceServerApplicationTests { + static final AnonymousAuthenticationToken ANONYMOUS = + new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); @MockBean ReactiveAuthenticationManagerResolver authenticationManagerResolver; @@ -25,16 +49,31 @@ class ReactiveResourceServerApplicationTests { @Autowired WebTestClient api; + // needed only when using @ParameterizedTests with WithJwt.AuthenticationFactory @Autowired - ServerProperties serverProperties; + Converter> authenticationConverter; + + WithJwt.AuthenticationFactory jwtAuthFactory; + + @BeforeEach + public void setUp() { + jwtAuthFactory = new WithJwt.AuthenticationFactory(Optional.empty(), Optional.of(authenticationConverter)); + } @Test - void givenRequestIsNotAuthorized_whenGreet_thenUnauthorized() throws Exception { - api.get().uri("/greet").exchange().expectStatus().isUnauthorized(); + void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception { + api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)).get().uri("/greet").exchange().expectStatus().isUnauthorized(); } @Test - void givenUserAuthenticated_whenGreet_thenOk() throws Exception { + @WithAnonymousUser + void givenUserIsAnonymous_whenGreet_thenUnauthorized() throws Exception { + api.get().uri("/greet").exchange().expectStatus().isUnauthorized(); + } + + @ParameterizedTest + @MethodSource("identityMutators") + void givenUserIsAuthenticated_whenGreet_thenOk(JwtMutator identityMutator) throws Exception { // @formatter:off api.mutateWith(SecurityMockServerConfigurers.mockJwt() .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR"))) @@ -44,8 +83,42 @@ void givenUserAuthenticated_whenGreet_thenOk() throws Exception { // @formatter:on } + static Stream identityMutators() { + return Stream.of( + SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.subject("ch4mp")) + .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR")), + SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.subject("tonton-pirate")) + .authorities(new SimpleGrantedAuthority("UNCLE"), new SimpleGrantedAuthority("SKIPPER"))); + } + + @ParameterizedTest + @AuthenticationSource({ + @WithMockAuthentication(name = "ch4mp", authorities = { "NICE", "AUTHOR" }), + @WithMockAuthentication(name = "tonton-pirate", authorities = { "UNCLE", "SKIPPER" }) }) + void givenUserIsAuthenticatedWithMockAuthentication_whenGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getName(), auth.getAuthorities()))); + // @formatter:on + } + + @ParameterizedTest + @MethodSource("jwts") + void givenUserIsAuthenticatedWithJwt_whenGreet_thenOk(@ParameterizedAuthentication Authentication auth) throws Exception { + // @formatter:off + api.get().uri("/greet").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getName(), auth.getAuthorities()))); + // @formatter:on + } + + Stream jwts() { + return jwtAuthFactory.authenticationsFrom("auth0_badboy.json", "auth0_nice.json"); + } + @Test - void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { + void givenUserHasNiceMutator_whenGetRestricted_thenOk() throws Exception { // @formatter:off api.mutateWith(SecurityMockServerConfigurers.mockJwt() .authorities(new SimpleGrantedAuthority("NICE"), new SimpleGrantedAuthority("AUTHOR"))) @@ -56,17 +129,66 @@ void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { } @Test - void givenUserIsNotNice_whenGetRestricted_thenForbidden() throws Exception { + @WithMockAuthentication({ "NICE", "AUTHOR" }) + void givenUserHasNiceMockAuthentication_whenGetRestricted_thenOk() throws Exception { // @formatter:off - api.mutateWith(SecurityMockServerConfigurers.mockJwt() - .authorities(new SimpleGrantedAuthority("AUTHOR"))) + api.get().uri("/restricted").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("You are so nice!")); + // @formatter:on + } + + @Test + @WithJwt("auth0_nice.json") + void givenUserIsNice_whenGetRestricted_thenOk() throws Exception { + // @formatter:off + api.get().uri("/restricted").exchange() + .expectStatus().isOk() + .expectBody(MessageDto.class).isEqualTo(new MessageDto("You are so nice!")); + // @formatter:on + } + + @Test + void givenUserHasNotNiceMutator_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) + .get().uri("/restricted").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + @WithMockAuthentication("AUTHOR") + void givenUserHasNotNiceMockAuthentication_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) .get().uri("/restricted").exchange() .expectStatus().isForbidden(); // @formatter:on } @Test - void givenRequestIsNotAuthorized_whenGetRestricted_thenUnauthorized() throws Exception { + @WithJwt("auth0_badboy.json") + void givenUserIsBadboy_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockJwt().authorities(new SimpleGrantedAuthority("AUTHOR"))) + .get().uri("/restricted").exchange() + .expectStatus().isForbidden(); + // @formatter:on + } + + @Test + void givenUserHasAnonymousMutator_whenGetRestricted_thenForbidden() throws Exception { + // @formatter:off + api.mutateWith(SecurityMockServerConfigurers.mockAuthentication(ANONYMOUS)) + .get().uri("/restricted").exchange() + .expectStatus().isUnauthorized(); + // @formatter:on + } + + @Test + @WithAnonymousUser + void givenUserIsAnonymous_whenGetRestricted_thenUnauthorized() throws Exception { api.get().uri("/restricted").exchange().expectStatus().isUnauthorized(); } diff --git a/samples/tutorials/reactive-resource-server/src/test/resources/auth0_badboy.json b/samples/tutorials/reactive-resource-server/src/test/resources/auth0_badboy.json new file mode 100644 index 000000000..c4810c8ec --- /dev/null +++ b/samples/tutorials/reactive-resource-server/src/test/resources/auth0_badboy.json @@ -0,0 +1,40 @@ +{ + "https://c4-soft.com/user": { + "app_metadata": {}, + "created_at": "2023-06-01T01:21:37.810Z", + "email": "tonton-pirate@c4-soft.com", + "email_verified": true, + "identities": [ + { + "connection": "c4-soft", + "isSocial": true, + "provider": "oauth2", + "userId": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e90", + "user_id": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e90" + } + ], + "multifactor": [], + "name": "tonton-pirate", + "nickname": "Tonton Pirate", + "picture": "https://s.gravatar.com/avatar/f4d00b0a82e9307b1d68b29867fee4e5?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fch.png", + "roles": [ + "SKIPPER" + ], + "updated_at": "2023-06-23T04:53:53.057Z", + "user_id": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", + "user_metadata": {} + }, + "permissions": [ + "AUTHOR" + ], + "iss": "https://dev-ch4mpy.eu.auth0.com/", + "sub": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e90", + "aud": [ + "demo.c4-soft.com", + "https://dev-ch4mpy.eu.auth0.com/userinfo" + ], + "iat": 1687633329, + "exp": 1687719729, + "azp": "pDy3JpZoenbLk9MqXYCfJK1mpxeUwkKL", + "scope": "openid email" +} \ No newline at end of file diff --git a/samples/tutorials/reactive-resource-server/src/test/resources/auth0_nice.json b/samples/tutorials/reactive-resource-server/src/test/resources/auth0_nice.json new file mode 100644 index 000000000..fa7f45fdf --- /dev/null +++ b/samples/tutorials/reactive-resource-server/src/test/resources/auth0_nice.json @@ -0,0 +1,40 @@ +{ + "https://c4-soft.com/user": { + "app_metadata": {}, + "created_at": "2023-06-01T01:21:37.810Z", + "email": "ch4mp@c4-soft.com", + "email_verified": true, + "identities": [ + { + "connection": "c4-soft", + "isSocial": true, + "provider": "oauth2", + "userId": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", + "user_id": "c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94" + } + ], + "multifactor": [], + "name": "ch4mp", + "nickname": "ch4mp", + "picture": "https://s.gravatar.com/avatar/f4d00b0a82e9307b1d68b29867fee4e5?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fch.png", + "roles": [ + "USER_ROLES_EDITOR" + ], + "updated_at": "2023-06-23T04:53:53.057Z", + "user_id": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", + "user_metadata": {} + }, + "permissions": [ + "NICE", "AUTHOR" + ], + "iss": "https://dev-ch4mpy.eu.auth0.com/", + "sub": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", + "aud": [ + "demo.c4-soft.com", + "https://dev-ch4mpy.eu.auth0.com/userinfo" + ], + "iat": 1687633329, + "exp": 1687719729, + "azp": "pDy3JpZoenbLk9MqXYCfJK1mpxeUwkKL", + "scope": "openid email" +} \ No newline at end of file diff --git a/samples/tutorials/resource-server_multitenant_dynamic/pom.xml b/samples/tutorials/resource-server_multitenant_dynamic/pom.xml index e8c922d71..00802b215 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/pom.xml +++ b/samples/tutorials/resource-server_multitenant_dynamic/pom.xml @@ -19,18 +19,27 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.security spring-security-config com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc org.springdoc springdoc-openapi-starter-webmvc-ui + + jakarta.servlet + jakarta.servlet-api + provided + org.springframework.boot @@ -45,7 +54,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java b/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java index f43f8ada7..47376ba6f 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java +++ b/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java @@ -4,8 +4,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; @RestController @PreAuthorize("isAuthenticated()") diff --git a/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java b/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java index 8e1e2c14b..086433301 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java +++ b/samples/tutorials/resource-server_multitenant_dynamic/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java @@ -27,12 +27,12 @@ import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.ResponseStatus; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; -import com.c4_soft.springaddons.security.oauth2.config.JwtAbstractAuthenticationTokenConverter; -import com.c4_soft.springaddons.security.oauth2.config.MissingAuthorizationServerConfigurationException; -import com.c4_soft.springaddons.security.oauth2.config.SpringAddonsSecurityProperties; -import com.c4_soft.springaddons.security.oauth2.config.SpringAddonsSecurityProperties.IssuerProperties; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.starter.properties.MissingAuthorizationServerConfigurationException; +import com.c4_soft.springaddons.security.oidc.starter.properties.OpenidProviderProperties; +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; import jakarta.servlet.http.HttpServletRequest; @@ -44,7 +44,7 @@ JwtAbstractAuthenticationTokenConverter authenticationConverter( Converter, Collection> authoritiesConverter, DynamicTenantProperties addonsProperties) { return jwt -> { - final var issProperties = addonsProperties.getIssuerProperties(jwt.getClaims().get(JwtClaimNames.ISS).toString()); + final var issProperties = addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS).toString()); return new OAuthentication<>( new OpenidClaimSet(jwt.getClaims(), issProperties.getUsernameClaim()), authoritiesConverter.convert(jwt.getClaims()), @@ -65,11 +65,11 @@ private static URI baseUri(URI uri) { @Primary @Component - static class DynamicTenantProperties extends SpringAddonsSecurityProperties { + static class DynamicTenantProperties extends SpringAddonsOidcProperties { @Override - public IssuerProperties getIssuerProperties(String iss) throws MissingAuthorizationServerConfigurationException { - return super.getIssuerProperties(baseUri(URI.create(iss)).toString()); + public OpenidProviderProperties getOpProperties(String iss) throws MissingAuthorizationServerConfigurationException { + return super.getOpProperties(baseUri(URI.create(iss)).toString()); } } @@ -83,9 +83,9 @@ static class DynamicTenantsAuthenticationManagerResolver implements Authenticati new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) this::getAuthenticationManager); public DynamicTenantsAuthenticationManagerResolver( - SpringAddonsSecurityProperties addonsProperties, + SpringAddonsOidcProperties addonsProperties, Converter jwtAuthenticationConverter) { - this.issuerBaseUris = Stream.of(addonsProperties.getIssuers()).map(IssuerProperties::getLocation).map(WebSecurityConfig::baseUri).map(URI::toString) + this.issuerBaseUris = Stream.of(addonsProperties.getOps()).map(OpenidProviderProperties::getIss).map(WebSecurityConfig::baseUri).map(URI::toString) .collect(Collectors.toSet()); this.jwtAuthenticationConverter = jwtAuthenticationConverter; } diff --git a/samples/tutorials/resource-server_multitenant_dynamic/src/main/resources/application.yml b/samples/tutorials/resource-server_multitenant_dynamic/src/main/resources/application.yml index ba216bf08..90b26ff49 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/src/main/resources/application.yml +++ b/samples/tutorials/resource-server_multitenant_dynamic/src/main/resources/application.yml @@ -15,30 +15,31 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${scheme}://localhost:${keycloak-port} + oidc: + ops: + - iss: ${scheme}://localhost:${keycloak-port} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: https://cognito-idp.us-west-2.amazonaws.com + - iss: https://cognito-idp.us-west-2.amazonaws.com username-claim: username authorities: - path: cognito:groups - - location: https://dev-ch4mpy.eu.auth0.com + - iss: https://dev-ch4mpy.eu.auth0.com username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] - path: $.permissions - permit-all: - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" - - "/swagger-ui/**" + resourceserver: + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" + - "/swagger-ui/**" logging: level: diff --git a/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index adf9aa679..6431d857c 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -16,16 +16,16 @@ import org.springframework.security.core.Authentication; import org.springframework.security.test.context.support.WithAnonymousUser; -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.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.jwt.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; @WebMvcTest(controllers = GreetingController.class) -@AutoConfigureAddonsWebSecurity @Import(WebSecurityConfig.class) +@AutoConfigureAddonsWebmvcResourceServerSecurity class GreetingControllerTest { @Autowired diff --git a/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerMultitenantDynamicApplicationTests.java b/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerMultitenantDynamicApplicationTests.java index 69e5b6405..60c9523a5 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerMultitenantDynamicApplicationTests.java +++ b/samples/tutorials/resource-server_multitenant_dynamic/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerMultitenantDynamicApplicationTests.java @@ -20,12 +20,12 @@ import org.springframework.security.core.Authentication; import org.springframework.security.test.context.support.WithAnonymousUser; -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.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.AddonsWebmvcTestConf; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc diff --git a/samples/tutorials/resource-server_with_additional-header/pom.xml b/samples/tutorials/resource-server_with_additional-header/pom.xml index 669399c29..c076175f1 100644 --- a/samples/tutorials/resource-server_with_additional-header/pom.xml +++ b/samples/tutorials/resource-server_with_additional-header/pom.xml @@ -11,6 +11,7 @@ resource-server with additional-header + org.springframework.boot spring-boot-starter @@ -19,9 +20,17 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-web + com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc @@ -42,7 +51,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java index 6a9644f7e..2edf1b82f 100644 --- a/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java +++ b/samples/tutorials/resource-server_with_additional-header/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java @@ -18,12 +18,12 @@ import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtException; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; -import com.c4_soft.springaddons.security.oauth2.config.JwtAbstractAuthenticationTokenConverter; -import com.c4_soft.springaddons.security.oauth2.config.synchronised.HttpServletRequestSupport; -import com.c4_soft.springaddons.security.oauth2.config.synchronised.HttpServletRequestSupport.InvalidHeaderException; -import com.c4_soft.springaddons.security.oauth2.config.synchronised.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.HttpServletRequestSupport; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.HttpServletRequestSupport.InvalidHeaderException; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/samples/tutorials/resource-server_with_additional-header/src/main/resources/application.yml b/samples/tutorials/resource-server_with_additional-header/src/main/resources/application.yml index c9f2defc4..a257b0a05 100644 --- a/samples/tutorials/resource-server_with_additional-header/src/main/resources/application.yml +++ b/samples/tutorials/resource-server_with_additional-header/src/main/resources/application.yml @@ -18,29 +18,30 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: ${cognito-issuer} + - iss: ${cognito-issuer} username-claim: username authorities: - path: cognito:groups - - location: ${auth0-issuer} + - iss: ${auth0-issuer} username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] - path: $.permissions - permit-all: - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" + resourceserver: + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" logging: level: diff --git a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index fcf2ffc5e..b1b30e9fc 100644 --- a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -10,11 +10,11 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.jwt.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @WebMvcTest(controllers = GreetingController.class) -@AutoConfigureAddonsWebSecurity +@AutoConfigureAddonsWebmvcResourceServerSecurity @Import(SecurityConfig.class) class GreetingControllerTest { diff --git a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/ServletResourceServerWithAdditionalHeaderTests.java b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/ServletResourceServerWithAdditionalHeaderTests.java index 1e0ae0190..4031f21c4 100644 --- a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/ServletResourceServerWithAdditionalHeaderTests.java +++ b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/ServletResourceServerWithAdditionalHeaderTests.java @@ -1,5 +1,6 @@ package com.c4soft.springaddons.tutorials; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -14,8 +15,8 @@ import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.AddonsWebmvcTestConf; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc @@ -26,12 +27,12 @@ class ServletResourceServerWithAdditionalHeaderTests { @Test void givenRequestIsAnonymous_whenGetActuatorHealthLiveness_thenOk() throws Exception { - api.get("/actuator/health/liveness").andExpect(status().isOk()).andExpect(jsonPath("$.status").value("UP")); + api.with(anonymous()).get("/actuator/health/liveness").andExpect(status().isOk()).andExpect(jsonPath("$.status").value("UP")); } @Test void givenRequestIsAnonymous_whenGetActuatorHealthReadiness_thenOk() throws Exception { - api.get("/actuator/health/readiness").andExpect(status().isOk()); + api.with(anonymous()).get("/actuator/health/readiness").andExpect(status().isOk()); } @Test diff --git a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/WithMyAuth.java b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/WithMyAuth.java index cae5b7f6d..e456dbb3e 100644 --- a/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/WithMyAuth.java +++ b/samples/tutorials/resource-server_with_additional-header/src/test/java/com/c4soft/springaddons/tutorials/WithMyAuth.java @@ -11,9 +11,9 @@ import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithSecurityContext; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; import com.c4_soft.springaddons.security.oauth2.test.annotations.AbstractAnnotatedAuthenticationBuilder; import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import com.c4soft.springaddons.tutorials.SecurityConfig.MyAuth; @Target({ ElementType.METHOD, ElementType.TYPE }) diff --git a/samples/tutorials/resource-server_with_introspection/pom.xml b/samples/tutorials/resource-server_with_introspection/pom.xml index d4a60fa63..21b635485 100644 --- a/samples/tutorials/resource-server_with_introspection/pom.xml +++ b/samples/tutorials/resource-server_with_introspection/pom.xml @@ -15,9 +15,17 @@ org.springframework.boot spring-boot-starter + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + com.c4-soft.springaddons - spring-addons-webmvc-introspecting-resource-server + spring-addons-starter-oidc @@ -38,7 +46,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-introspecting-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java b/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java index 55699ed83..93420ca94 100644 --- a/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java +++ b/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java @@ -1,5 +1,6 @@ package com.c4soft.springaddons.tutorials; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; @@ -7,7 +8,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/greet") +@RequestMapping(value = "/greet", produces = MediaType.APPLICATION_JSON_VALUE) public class GreetingController { @GetMapping() @@ -16,6 +17,6 @@ public MessageDto getGreeting(Authentication auth) { return new MessageDto("Hi %s! You are granted with: %s.".formatted(auth.getName(), auth.getAuthorities())); } - static record MessageDto(String body) { + public static record MessageDto(String body) { } } diff --git a/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java b/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java index 29c10068a..22a9ffd2b 100644 --- a/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java +++ b/samples/tutorials/resource-server_with_introspection/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java @@ -23,8 +23,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; @Configuration @EnableMethodSecurity diff --git a/samples/tutorials/resource-server_with_introspection/src/main/resources/application.yml b/samples/tutorials/resource-server_with_introspection/src/main/resources/application.yml index 1910be87f..16dc8ff1e 100644 --- a/samples/tutorials/resource-server_with_introspection/src/main/resources/application.yml +++ b/samples/tutorials/resource-server_with_introspection/src/main/resources/application.yml @@ -28,20 +28,21 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - permit-all: - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" + resourceserver: + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" logging: level: diff --git a/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index ae4e6bdcb..95fd1e196 100644 --- a/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -7,16 +7,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.test.context.support.WithAnonymousUser; -import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; -import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockBearerTokenAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.introspecting.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithOpaqueToken; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @WebMvcTest(controllers = GreetingController.class) -@AutoConfigureAddonsWebSecurity +@AutoConfigureAddonsWebmvcResourceServerSecurity @Import(WebSecurityConfig.class) class GreetingControllerTest { @@ -25,18 +23,16 @@ class GreetingControllerTest { // @formatter:off @Test - @WithMockBearerTokenAuthentication( - authorities = { "NICE", "AUTHOR" }, - attributes = @OpenIdClaims(usernameClaim = StandardClaimNames.PREFERRED_USERNAME, preferredUsername = "Tonton Pirate")) + @WithOpaqueToken("ch4mp.json") void givenUserIsGrantedWithNice_whenGreet_thenOk() throws Exception { mockMvc.get("/greet") .andExpect(status().isOk()) - .andExpect(jsonPath("$.body").value("Hi Tonton Pirate! You are granted with: [NICE, AUTHOR].")); + .andExpect(jsonPath("$.body").value("Hi ch4mp! You are granted with: [NICE, AUTHOR, ROLE_AUTHORIZED_PERSONNEL].")); } // @formatter:on @Test - @WithMockBearerTokenAuthentication(authorities = "AUTHOR", attributes = @OpenIdClaims(preferredUsername = "Tonton Pirate")) + @WithOpaqueToken("tonton-pirate.json") void givenUserIsNotGrantedWithNice_whenGreet_thenForbidden() throws Exception { mockMvc.get("/greet").andExpect(status().isForbidden()); } diff --git a/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java b/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java index 109a03922..83ca30e98 100644 --- a/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java +++ b/samples/tutorials/resource-server_with_introspection/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java @@ -9,13 +9,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.test.context.support.WithAnonymousUser; -import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims; -import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockBearerTokenAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.AddonsWebmvcTestConf; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oauth2.test.annotations.WithOpaqueToken; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc @@ -31,20 +29,18 @@ void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception { } @Test - @WithMockBearerTokenAuthentication() - void givenUserIsNotGrantedWithNice_whenGreet_thenForbidden() throws Exception { + @WithOpaqueToken("tonton-pirate.json") + void givenUserIsTontonPirate_whenGreet_thenForbidden() throws Exception { api.get("/greet").andExpect(status().isForbidden()); } // @formatter:off @Test - @WithMockBearerTokenAuthentication( - authorities = { "NICE", "AUTHOR" }, - attributes = @OpenIdClaims(usernameClaim = StandardClaimNames.PREFERRED_USERNAME, preferredUsername = "Tonton Pirate")) + @WithOpaqueToken("ch4mp.json") void givenUserIsGrantedWithNice_whenGreet_thenOk() throws Exception { api.get("/greet") .andExpect(status().isOk()) - .andExpect(jsonPath("$.body").value("Hi Tonton Pirate! You are granted with: [NICE, AUTHOR].")); + .andExpect(jsonPath("$.body").value("Hi ch4mp! You are granted with: [NICE, AUTHOR, ROLE_AUTHORIZED_PERSONNEL].")); } // @formatter:on diff --git a/samples/tutorials/resource-server_with_introspection/src/test/resources/ch4mp.json b/samples/tutorials/resource-server_with_introspection/src/test/resources/ch4mp.json new file mode 100644 index 000000000..3ad337a5f --- /dev/null +++ b/samples/tutorials/resource-server_with_introspection/src/test/resources/ch4mp.json @@ -0,0 +1,22 @@ +{ + "email": "ch4mp@c4-soft.com", + "email_verified": true, + "preferred_username": "ch4mp", + "realm_access": { + "roles": [ + "NICE", + "AUTHOR", + "ROLE_AUTHORIZED_PERSONNEL" + ] + }, + "iss": "http://localhost:8442/realms/master", + "sub": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e94", + "aud": [ + "demo.c4-soft.com", + "http://localhost:8080" + ], + "iat": 1687633329, + "exp": 1687719729, + "azp": "pDy3JpZoenbLk9MqXYCfJK1mpxeUwkKL", + "scope": "openid email" +} \ No newline at end of file diff --git a/samples/tutorials/resource-server_with_introspection/src/test/resources/tonton-pirate.json b/samples/tutorials/resource-server_with_introspection/src/test/resources/tonton-pirate.json new file mode 100644 index 000000000..9604a961c --- /dev/null +++ b/samples/tutorials/resource-server_with_introspection/src/test/resources/tonton-pirate.json @@ -0,0 +1,21 @@ +{ + "email": "tonton-pirate@c4-soft.com", + "email_verified": true, + "preferred_username": "tonton-pirate", + "realm_access": { + "roles": [ + "UNCLE", + "PIRATE" + ] + }, + "iss": "http://localhost:8442/realms/master", + "sub": "oauth2|c4-soft|4dd56dbb-71ef-4fe2-9358-3ae3240a9e90", + "aud": [ + "demo.c4-soft.com", + "http://localhost:8080" + ], + "iat": 1687633329, + "exp": 1687719729, + "azp": "pDy3JpZoenbLk9MqXYCfJK1mpxeUwkKL", + "scope": "openid email" +} \ No newline at end of file diff --git a/samples/tutorials/resource-server_with_oauthentication/pom.xml b/samples/tutorials/resource-server_with_oauthentication/pom.xml index cd4c58a00..ea84c0ddb 100644 --- a/samples/tutorials/resource-server_with_oauthentication/pom.xml +++ b/samples/tutorials/resource-server_with_oauthentication/pom.xml @@ -19,13 +19,17 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.security spring-security-config com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc org.springdoc @@ -45,7 +49,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java b/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java index f43f8ada7..47376ba6f 100644 --- a/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java +++ b/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/GreetingController.java @@ -4,8 +4,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; @RestController @PreAuthorize("isAuthenticated()") diff --git a/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplication.java b/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplication.java index c0c23cc00..9d64b1e42 100644 --- a/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplication.java +++ b/samples/tutorials/resource-server_with_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplication.java @@ -15,11 +15,11 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.JwtClaimNames; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; -import com.c4_soft.springaddons.security.oauth2.config.JwtAbstractAuthenticationTokenConverter; -import com.c4_soft.springaddons.security.oauth2.config.SpringAddonsSecurityProperties; -import com.c4_soft.springaddons.security.oauth2.config.synchronised.ResourceServerExpressionInterceptUrlRegistryPostProcessor; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.OAuthFlow; @@ -48,9 +48,9 @@ public static class SecurityConfig { @Bean JwtAbstractAuthenticationTokenConverter authenticationConverter( Converter, Collection> authoritiesConverter, - SpringAddonsSecurityProperties addonsProperties) { + SpringAddonsOidcProperties addonsProperties) { return jwt -> new OAuthentication<>( - new OpenidClaimSet(jwt.getClaims(), addonsProperties.getIssuerProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()), + new OpenidClaimSet(jwt.getClaims(), addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()), authoritiesConverter.convert(jwt.getClaims()), jwt.getTokenValue()); } diff --git a/samples/tutorials/resource-server_with_oauthentication/src/main/resources/application.yml b/samples/tutorials/resource-server_with_oauthentication/src/main/resources/application.yml index f480f4ffa..b4dda6910 100644 --- a/samples/tutorials/resource-server_with_oauthentication/src/main/resources/application.yml +++ b/samples/tutorials/resource-server_with_oauthentication/src/main/resources/application.yml @@ -19,30 +19,31 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: ${cognito-issuer} + - iss: ${cognito-issuer} username-claim: username authorities: - path: cognito:groups - - location: ${auth0-issuer} + - iss: ${auth0-issuer} username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] - path: $.permissions - permit-all: - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" - - "/swagger-ui/**" + resourceserver: + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" + - "/swagger-ui/**" logging: level: diff --git a/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index ee87e4eb2..533153208 100644 --- a/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -16,16 +16,16 @@ import org.springframework.security.core.Authentication; import org.springframework.security.test.context.support.WithAnonymousUser; -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.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.annotations.parameterized.ParameterizedAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.jwt.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oidc.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import com.c4soft.springaddons.tutorials.ResourceServerWithOAuthenticationApplication.SecurityConfig; @WebMvcTest(controllers = GreetingController.class) -@AutoConfigureAddonsWebSecurity +@AutoConfigureAddonsWebmvcResourceServerSecurity @Import(SecurityConfig.class) class GreetingControllerTest { diff --git a/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java b/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java index cc4500dad..de1c90f16 100644 --- a/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java +++ b/samples/tutorials/resource-server_with_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOAuthenticationApplicationTests.java @@ -15,8 +15,8 @@ import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockAuthentication; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.AddonsWebmvcTestConf; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/pom.xml b/samples/tutorials/resource-server_with_specialized_oauthentication/pom.xml index d78d4e586..eca43ad27 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/pom.xml +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/pom.xml @@ -15,6 +15,10 @@ org.springframework.boot spring-boot-starter + + org.springframework.boot + spring-boot-starter-web + org.springframework.security spring-security-config @@ -23,9 +27,13 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc @@ -46,7 +54,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesAuthentication.java b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesAuthentication.java index a0c62c8bc..72993283b 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesAuthentication.java +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesAuthentication.java @@ -6,7 +6,7 @@ import org.springframework.security.core.GrantedAuthority; -import com.c4_soft.springaddons.security.oauth2.OAuthentication; +import com.c4_soft.springaddons.security.oidc.OAuthentication; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesClaimSet.java b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesClaimSet.java index 26b6c5d32..08b6e3d97 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesClaimSet.java +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/ProxiesClaimSet.java @@ -8,7 +8,7 @@ import org.springframework.core.convert.converter.Converter; -import com.c4_soft.springaddons.security.oauth2.OpenidClaimSet; +import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java index d1ae0e549..79ec018c9 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/java/com/c4soft/springaddons/tutorials/SecurityConfig.java @@ -12,9 +12,9 @@ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.core.GrantedAuthority; -import com.c4_soft.springaddons.security.oauth2.config.JwtAbstractAuthenticationTokenConverter; -import com.c4_soft.springaddons.security.oauth2.spring.C4MethodSecurityExpressionHandler; -import com.c4_soft.springaddons.security.oauth2.spring.C4MethodSecurityExpressionRoot; +import com.c4_soft.springaddons.security.oidc.spring.C4MethodSecurityExpressionHandler; +import com.c4_soft.springaddons.security.oidc.spring.C4MethodSecurityExpressionRoot; +import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter; @Configuration @EnableMethodSecurity diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/resources/application.yml b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/resources/application.yml index 0de95f861..663aea100 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/resources/application.yml +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/main/resources/application.yml @@ -18,30 +18,31 @@ spring: com: c4-soft: springaddons: - security: - cors: - - path: /** - allowed-origins: ${origins} - issuers: - - location: ${keycloak-issuer} + oidc: + ops: + - iss: ${keycloak-issuer} username-claim: preferred_username authorities: - path: $.realm_access.roles - path: $.resource_access.*.roles - - location: ${cognito-issuer} + - iss: ${cognito-issuer} username-claim: username authorities: - path: cognito:groups - - location: ${auth0-issuer} + - iss: ${auth0-issuer} username-claim: $['https://c4-soft.com/user']['name'] authorities: - path: $['https://c4-soft.com/user']['roles'] - path: $.permissions - permit-all: - - "/greet/public" - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" + resourceserver: + cors: + - path: /** + allowed-origin-patterns: ${origins} + permit-all: + - "/greet/public" + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" logging: level: diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java b/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java index 43869718d..d9a6fee8f 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/GreetingControllerTest.java @@ -10,11 +10,11 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.jwt.AutoConfigureAddonsWebSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @WebMvcTest(controllers = GreetingController.class) -@AutoConfigureAddonsWebSecurity +@AutoConfigureAddonsWebmvcResourceServerSecurity @Import({ SecurityConfig.class }) class GreetingControllerTest { diff --git a/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOidcAuthenticationApplicationTests.java b/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOidcAuthenticationApplicationTests.java index ef431deda..eb9d8265d 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOidcAuthenticationApplicationTests.java +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/src/test/java/com/c4soft/springaddons/tutorials/ResourceServerWithOidcAuthenticationApplicationTests.java @@ -12,8 +12,8 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.AddonsWebmvcTestConf; -import com.c4_soft.springaddons.security.oauth2.test.mockmvc.MockMvcSupport; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; @SpringBootTest(webEnvironment = WebEnvironment.MOCK, classes = { ResourceServerWithOAuthenticationApplication.class, SecurityConfig.class }) @AutoConfigureMockMvc diff --git a/samples/tutorials/resource-server_with_ui/pom.xml b/samples/tutorials/resource-server_with_ui/pom.xml index 70fced3d8..bf12f60b5 100644 --- a/samples/tutorials/resource-server_with_ui/pom.xml +++ b/samples/tutorials/resource-server_with_ui/pom.xml @@ -15,17 +15,9 @@ org.springframework.boot spring-boot-starter - - - com.c4-soft.springaddons - spring-addons-webmvc-jwt-resource-server - - - - - com.c4-soft.springaddons - spring-addons-webmvc-client + org.springframework.boot + spring-boot-starter-web org.springframework.boot @@ -33,11 +25,18 @@ org.springframework.boot - spring-boot-starter-thymeleaf + spring-boot-starter-oauth2-resource-server + + + com.c4-soft.springaddons + spring-addons-starter-oidc + + + org.springframework.boot - spring-boot-starter-web + spring-boot-starter-thymeleaf org.springframework.boot @@ -73,7 +72,7 @@ com.c4-soft.springaddons - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test test diff --git a/samples/tutorials/resource-server_with_ui/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java b/samples/tutorials/resource-server_with_ui/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java index 629550df2..eeb237f21 100644 --- a/samples/tutorials/resource-server_with_ui/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java +++ b/samples/tutorials/resource-server_with_ui/src/main/java/com/c4soft/springaddons/tutorials/WebSecurityConfig.java @@ -8,7 +8,7 @@ *