diff --git a/7.0.0-migration-guide.md b/7.0.0-migration-guide.md new file mode 100644 index 000000000..454ba9698 --- /dev/null +++ b/7.0.0-migration-guide.md @@ -0,0 +1,72 @@ +# 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`) +- `oauth2-logout` is now a map indexed by provider ID instead of an array. Remove `client-registration-id` from each entry and replace it with the matching provider ID used as key for the remaining properties. For instance: +```yaml + oauth2-logout: + - client-registration-id: cognito-confidential-user + uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout + client-id-request-param: client_id + post-logout-uri-request-param: logout_uri + - client-registration-id: auth0-confidential-user + uri: ${auth0-issuer}v2/logout + client-id-request-param: client_id + post-logout-uri-request-param: returnTo +``` +becomes +```yaml + oauth2-logout: + cognito: + uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout + client-id-request-param: client_id + post-logout-uri-request-param: logout_uri + auth0: + uri: ${auth0-issuer}v2/logout + client-id-request-param: client_id + post-logout-uri-request-param: returnTo +``` +where `cognito` and `auth0` are the values of `spring.security.oauth2.client.registration.cognito-confidential-user.provider` and `spring.security.oauth2.client.registration.auth0-confidential-user.provider` \ No newline at end of file diff --git a/README.MD b/README.MD index c3dcf2538..54728d247 100644 --- a/README.MD +++ b/README.MD @@ -1,22 +1,30 @@ -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 4 of the test 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 move from `6.x` to `7.0.0`. There is no urge to do so on existing projects as we might keep publishing 6.2.x versions untill the end of 2023. + +All samples and tutorials sources are migrated to latest starter and test annotations, but some READMEs might still need a refresh. Please make sure you refer to source code for up to date configuration. + +# 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 * basic access control: `permitAll` for a list of path matchers and `authenticated` as default (to be fine tuned with method security or a configuration post-processor bean) * for clients only: - logout success handler for OPs not strictly following the standard (exotic parameter names or missing `end_session_endpoint` in OpenID configuration). Auth0 and Amazon Cognito are samples of such OPs. - - an implementation for client side of the Back-Channel Logout + - an experimental implementation for client side of the Back-Channel Logout - client host and port explicitly provided in login configuration to avoid [redirection issues when the client does not use 8080 (or 8443 if SSL is enabled)](https://github.com/spring-projects/spring-security/issues/12307) - add custom params to authorization-code request (like the `audience` required by Auth0). This parameters are defined in application properties for each client registration. 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,11 +40,12 @@ 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 public class SecuredService { - @PreAuthorize("hasAuthority('NICE')) + @PreAuthorize("hasAuthority('NICE')") String nice() { return "Dear %s, glad to see you!".formatted(SecurityContextHolder.getContext().getAuthentication().getName()); } @@ -51,8 +60,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 { @@ -71,7 +79,7 @@ class MessageServiceTests { @Test @WithJwt("brice.json") void givenUserIsBrice_whenCallNice_thenReturnsGreeting() { - assertThat(securedService.nice()).isEqualTo(Dear brice, glad to see you!); + assertThat(securedService.nice()).isEqualTo("Dear brice, glad to see you!"); } @ParameterizedTest @@ -86,22 +94,253 @@ class MessageServiceTests { } ``` There are we few things worth noting above: -- annotation fit so well with BDD (given-when-then) and are very brief - we are testing a `@Service` having methods decorated with `@PreAuthorize` (without `MockMvc` or `WebTestClient`) +- authorities and username will be coherent with claims during tests (it is not necessarily the case when we declare the 3 separately as done with MockMvc request post-processors and WebTestClient mutators). `WithJwt.AuthenticationFactory` uses the JWT authorities converter found in security configuration. As a consequence, `username` and `authorities` are resolved from claims, just as it is at runtime. - 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 +- 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 do +- annotations can be very brief and expressive + +### 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, you'll probably need to define claim-set details. In this case, `@WithJwt` and `@WithOpaqueToken` can come pretty handy as it uses 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. + +`spring-addons-oidc-starter` does not replace `spring-boot-starter-oauth2-resource-server` and `spring-boot-starter-oauth2-client`, it uses application properties to configure a few beans designed to be picked by Spring Boot official "starters". The aim is to reduce Java code and ease application deployment accross environments. 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. + +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. + +### 2.1. Usage +**If you are not absolutely sure why you need an OAuth2 client or an OAuth2 resource server configuration (and what are the differences between the two), please read the [OAuth2 essentials section](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#1-oauth2-essentials) of the tutorials.** This might save you a lot of time and effort. + +Add `com.c4-soft.springaddons:spring-addons-starter-oidc` to your dependencies, in addition to `org.springframework.boot:spring-boot-starter-oauth2-client` or `org.springframework.boot:spring-boot-starter-oauth2-resource-server`. + +If configuring an Outh2 client (with `oauth2Login`), define the standard Spring Boot `provider` and `registration` properties for OAuth2 clients. + +If configuring an OAuth2 resource server with access token introspection, define the standard Spring Boot `opaquetoken` properties. + +Then, define the relevant `com.c4-soft.springaddons.oidc` properties for your use case. There are many complete [samples](https://github.com/ch4mpy/spring-addons/tree/master/samples) and [tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials) you should refer to, but here are a few demos for different use-cases and OpenID Providers: + +#### 2.1.1. Resource Server with JWT decoder +For a REST API secured with JWT access tokens, you need: +```xml + + org.springframework.boot + + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + +``` +And +```yaml +com: + c4-soft: + springaddons: + oidc: + ops: + - iss: http://localhost:8442/realms/master + username-claim: preferred_username + authorities: + - path: $.realm_access.roles + - path: $.resource_access.*.roles + resourceserver: + permit-all: + - "/greet/public" + cors: + - path: /** + allowed-origin-patterns: http://localhost:4200 +``` +Above configuration will create an application without sessions nor CSRF protection, and 401 will be answered to unauthorized requests to protected resources. -## 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. +#### 2.1.2. Client +For an app serving Thymeleaf templates with login and logout: +```xml + + org.springframework.boot + + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + +``` +And +```yaml +cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl +cognito-client-id: change-me +cognito-secret: change-me + +spring: + security: + oauth2: + client: + provider: + cognito: + issuer-uri: ${cognito-issuer} + registration: + cognito-authorization-code: + authorization-grant-type: authorization_code + client-id: ${cognito-client-id} + client-secret: ${cognito-secret} + provider: cognito + scope: openid,profile,email,offline_access +com: + c4-soft: + springaddons: + oidc: + ops: + - iss: ${cognito-issuer} + username-claim: username + authorities: + - path: cognito:groups + client: + security-matchers: + - /** + permit-all: + - /login/** + - /oauth2/** + - / + # Auth0 and Cognito do not follow strictly the OpenID RP-Initiated Logout spec and need specific configuration + oauth2-logout: + cognito: + uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout + client-id-request-param: client_id + post-logout-uri-request-param: logout_uri +``` +Above configuration will create an application secured with sessions (not access tokens), with CSRF protection enabled, and unauthorized requests to protected resources will be redirected to login. -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** +#### 2.1.3. Client and Resource Server +For an app exposing publicly both +- Thymeleaf templates secured with session (with login and logout), all templates being served with `/ui` prefix (but index which is at `/`) +- a REST API secured with access token +```xml + + org.springframework.boot + + spring-boot-starter-web + + + org.springframework.boot + + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + com.c4-soft.springaddons + spring-addons-starter-oidc + +``` +And +```yaml +auth0-issuer: http://localhost:8442/realms/master +auth0-client-id: change-me +auth0-secret: change-me + +spring: + security: + oauth2: + client: + provider: + auth0: + issuer-uri: ${auth0-issuer} + registration: + auth0-authorization-code: + authorization-grant-type: authorization_code + client-id: ${auth0-client-id} + client-secret: ${auth0-secret} + provider: auth0 + scope: openid,profile,email,offline_access +com: + c4-soft: + springaddons: + oidc: + ops: + - iss: ${auth0-issuer} + username-claim: $['https://c4-soft.com/user']['name'] + authorities: + - path: $['https://c4-soft.com/user']['roles'] + - path: $.permissions + client: + security-matchers: + - /login/** + - /oauth2/** + - /logout + - / + - /ui/** + permit-all: + - /login/** + - /oauth2/** + - / + # Auth0 and Cognito do not follow strictly the OpenID RP-Initiated Logout spec and need specific configuration + oauth2-logout: + auth0: + uri: ${auth0-issuer}v2/logout + client-id-request-param: client_id + post-logout-uri-request-param: returnTo + # Auth0 requires an "audience" parameter in authorization-code request to deliver JWTs + authorization-request-params: + auth0-authorization-code: + - name: audience + value: demo.c4-soft.com + resourceserver: + permit-all: + - "/greet/public" +``` +With the above configuration, two distinct security filter-chains will be defined: +- a client one with sessions (and CSRF protection enabled), intercepting all requests to UI templates as well as those involved in login and logout, and redirecting to login unauthorized requests to protected templates. +- a resource serve one acting as default (with lowest precedence to process all requests that were not matched with client filter-chain `securityMatchers`), without sessions (requests are secured with JWT access tokens) nor CSRF protections, and returning 401 to unauthorized requests to protected resources. + +### 2.3. Customizing Auto-Configuration +First use your IDE auto-completion to check if there isn't an existing application property covering your needs: a lot is configurable from properties, and all properties are documented. + +You can override about any `@Bean` defined by spring-addons (almost all are `@ConditionalOnMissingBean`). Here are a few handy ones: +- `(Reactive)JwtAbstractAuthenticationTokenConverter`: take control on the `Authentication` instance built after a JWT was successfully decoded and validated +- `(Reactive)OpaqueTokenAuthenticationConverter`: take control on the `Authentication` instance built after an access token was successfully introspected +- `ClaimSetAuthoritiesConverter`: opt-out the `ConfigurableClaimSetAuthoritiesConverter`, responsible for authorities mapping, in OAuth2 clients +- `GrantedAuthoritiesMapper`: opt-out the `ConfigurableClaimSetAuthoritiesConverter`, responsible for authorities mapping, in OAuth2 clients +- `(Reactive)AuthenticationManagerResolver`: opt-out the authentication manager implementing static multi-tenancy for resource servers with JWT decoders +- `ResourceServerAuthorizeExchangeSpecPostProcessor`, `ClientAuthorizeExchangeSpecPostProcessor`, `ClientAuthorizeExchangeSpecPostProcessor` or `ResourceServerAuthorizeExchangeSpecPostProcessor`: fine grained access control from configuration (an alternative is using `@Enable(Reactive)MethodSecurity` and `@PreAuthorize` on controller methods) +- `ResourceServerHttpSecurityPostProcessor` or `ClientHttpSecurityPostProcessor`: post-process spring-addons auto-configured `SecurityFilterChains` (this enables to change absolutely anything from it). + +### 2.4. Disabling `spring-addons-oidc-starter` +The easiest way is to exclude it from the classpath, but you may also turn the auto-configuration off by: +- setting `com.c4-soft.springaddons.oidc.resourceserver.enabled` to `false` (this disables the resource server `SecurityFilterChain` bean instantiation, as well as all of its default dependencies) +- leaving `com.c4-soft.springaddons.oidc.client.securityMatcher` empty (this disables the client `SecurityFilterChain` bean instantiation, as well as all of its default dependencies) ## 3. Where to Start [Tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials) which cover: @@ -119,38 +358,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 +415,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/README.md b/samples/tutorials/README.md index cf84e651a..72b3c130b 100644 --- a/samples/tutorials/README.md +++ b/samples/tutorials/README.md @@ -3,7 +3,7 @@ This tutorials are focused on configuring OAuth2 security in Spring Spring Boot **You should carefully read the [OAuth2 essentials](#oauth_essentials) section before rushing to a specific tutorial**. This will save you a lot of time. -Once you have determined if the application to configure is based on WebMVC or WebFlux, if it's a client or resource-server, and [setup at least an OIDC Provider](#prerequisites), then refer the [Tutorials scenarios](#scenarios) and pick one matching your needs. +Once you have determined if the application to configure is an OAuth2 client or an OAuth2 resource server, and [setup at least one OIDC Provider](#prerequisites), then refer the [Tutorials scenarios](#scenarios) and pick one matching your needs. Jump to: - [1. OAuth2 essentials](#oauth_essentials) @@ -12,7 +12,7 @@ Jump to: ## 1. OAuth2 essentials -OAuth2 client and resource-server configuration are quite different. **Spring provides with different starters for a reason**. If you're not sure about the definitions, needs and responsibilities of those two, please please take 5 minutes to read this section before you start. +OAuth2 client and resource-server configuration are very different. **Spring provides with different starters for a reason**. If you're not sure about the definitions, needs and responsibilities of those two, please please take 5 minutes to read this section before you start. ### 1.1 Actors - **resource-owner**: think of it as end-user. Most frequently a physical person, but can be a batch or whatever trusted program authenticated with client-credential (or even a device authenticated with a flow we'll skip) @@ -20,33 +20,37 @@ OAuth2 client and resource-server configuration are quite different. **Spring pr - **client**: a piece of software which needs to access resources on one or more resource-servers. **It is responsible for acquiring tokens from the authorization server and authorizing its requests to resource-servers**, and as so to handle OAuth2 flows. It is sometimes refered to as *Relying Party* (*RP*). - **resource-server**: an API (most frequently REST). **It should not care about login, logout or any OAuth2 flow.** From its point of view, all that matters is if a request is authorized with a valid access token and taking access decisions based on it. +It is important to note that **a front-end is not necessarily an OAuth2 client**: in the **B**ackend **F**or **F**rontend pattern, the OAuth2 client is on the server, between resource server(s) (secured with access tokens) and web (Angular, React, Vue, ...) or mobile applications which are secured with sessions and never see OAuth2 tokens. + ### 1.2. Client VS Resource Server Configuration -As already wrote, the responsibilities and security requirements are quite different for the two. Lets explore that in more details. +As already wrote, the responsibilities and security requirements are quite different. Lets explore that in more details. #### 1.2.1. Need for Sessions -**Resource servers can usually be configured as stateless (without session)**. The "state" is associated with the access token which is enough to restore the security context of a request. This has quite a few valuable benefits for scalabilty and fault tolerance: any resource server instance can process any request without the need of sharing a session. Also, the access token protects against CSRF attacks and, if it is rotated frequently enough (every minute or so), against BREACH attacks too! +**Resource servers can usually be configured as stateless (without session)**. The "state" is associated with the access token which is enough to restore the security context of a request. This has valuable benefits for scalability and fault tolerance: any resource server instance can process any request without the need of sharing a session. Also, the access token protects against CSRF attacks and, if it is rotated frequently enough (every minute or so), against BREACH attacks too! **Clients consumed by browsers are secured with session cookies, not access tokens**. This exposes it to CSRF and BREACH attacks, and we'll have to configure specific mitigations for that. Also, as soon as scalability and fault tolerance are a concern, we'll have to pull the session out of the client instances. #### 1.2.2. Requests Authorization -Resource servers expect requests to protected resources to be authorized: have an `Authorization` header containing a `Bearer` access token. It's responsibilities are to check the validity of this token (issuer, audience, expiration time, etc.) and then decide if it should grant the requested resource based on the token claims (inside the token or introspected from it). +Resource servers expect requests to be authorized with an `Authorization` header containing a `Bearer` access token. + +Clients are responsible for authorizing their requests to resource servers: setting this `Authorization` header. Clients have the choice of different OAuth2 flows to get tokens from the authorization server (see next section for details). To avoid fetching new tokens for each request, it also has to save tokens and should be very careful to use a secured enough place so that tokens can't leak to malicious code (the persistent storage of a remote device is a pretty bad place to that regard). -Clients are responsible for authorizing their requests: set the `Authorization` header of the requests it send to resource server. Clients have the choice of different OAuth2 flows to get tokens from the authorization server (see next section for details). +Resource servers don't care how access tokens were obtained. Its responsibilities are limited to check the validity of this token (issuer, audience, expiration time, etc.) and then decide if it should grant the requested resource based on the token claims (inside the token or introspected from it). User login is part of OAuth2 `authorization-code` flow. As a consequence, **OAuth2 login (and logout) only make sense on OAuth2 clients configured with `authorization-code` flow**. To send requests to a secured resource server, you'll have to use a client capable of sending authorized requests. A few samples: - REST clients with UI like Postman - a "rich" browser application (Angular, React, Vue, etc.) configured as public client with an OAuth2 client library to handle flows, tokens storage and requests authorization -- programmatic REST client (`WebClient`, `@FeignClient`, `RestTemplate`, ...) used to call an OAuth2 secured API (resource server) from another micro-service (any Spring `@Component` like a MVC `@Controller` rendering a Thymeleaf template) +- programmatic REST client (`WebClient`, `@FeignClient`, `RestTemplate`, ...) used to call an OAuth2 secured API from another micro-service - a BFF. **B**ackend **F**or **F**rontend is a pattern in which a middleware (the BFF) on the server is used to hide OAuth2 tokens from the browser. The requests between the browser and the BFF are secured with sessions. The BFF is responsible for login, logout, storing tokens in session and replacing session cookie with OAuth2 access token before forwarding a request from the browser to resource server(s). `spring-cloud-gateway` can be used as BFF with `spring-boot-starter-oauth2-client` and the `TokenRelay` filter. #### 1.2.3. Should I use `spring-boot-starter-oauth2-client` or `spring-boot-starter-oauth2-resource-server`? -If the application is a REST API it's a resource server. Configuring it as a client just to enable OAuth2 login and query its REST endpoints with a browser is a mistake: It breaks its "stateless" nature and would work only for GET endpoints. Use `spring-boot-starter-oauth2-resource-server`, do not configure OAuth2 login and require clients to authorize their requests (use Postman or alike for your tests). +If the application is a REST API it should be configured as a resource server. Configuring it as a client just to enable OAuth2 login and query its REST endpoints with a browser is a mistake: It breaks its "stateless" nature and would work only for GET endpoints. Use `spring-boot-starter-oauth2-resource-server`, do not configure OAuth2 login and require clients to authorize their requests (use Postman or alike for your tests). -Use `spring-boot-starter-oauth2-client` only if the application serves UI templates or is used as BFF. In that case only, will login & logout be configured in Spring application (otherwize, it's managed by Postman or whatever is the OAuth2 client). +Use `spring-boot-starter-oauth2-client` if the application serves UI templates or is used as BFF. In that case only, will login & logout be configured in Spring application (otherwise, it's managed by Postman or whatever is the OAuth2 client). -What if the application matches both cases above (for instance exposes publicly both a REST API and a Thymeleaf UI to manipulate it)? As seen earlier, the configuration requirements are too different to stand in the same security filter-chain, but **it is possible to define more than one filter-chain if the first(s) in `@Order` are defined with `securityMatcher` to define to which request it apply**: the path (or any other request attribute like headers) is checked against each security filter-chain "matchers" in order and the first match defines which `SecurityFilterChain` bean will be applied to the request. +What if the application matches both cases above (for instance exposes publicly both a REST API and a Thymeleaf UI to manipulate it)? As seen earlier, the configuration requirements are too different to stand in the same security filter-chain, but **it is possible to define more than one filter-chain if the first(s) in `@Order` are defined with `securityMatcher` to define to which requests it apply**: the path (or any other request attribute like headers) is checked against each security filter-chain "matchers" in order and the first match defines which `SecurityFilterChain` bean will be applied to the request. ### 1.3. Flows There are quite a few but 3 are of interest for us: authorization-code, client-credentials and refresh-token. @@ -124,19 +128,23 @@ Remember to update the tutorials configuration with the OIDC Providers you set u It is important to work with https when exchanging access tokens, otherwise tokens can be leaked and user identity stolen. For this reason, many tools and libs will complain if you use http. If you don't have one already, [generate a self-signed certificate](https://github.com/ch4mpy/self-signed-certificate-generation) for your dev machine. ## 3. Tutorials Scenarios -In the following, you'll first find tutorials with the "official" Spring Boot starters and then some using the alternate starters proposed by this repository. +In the following, you'll first find tutorials with just the "official" Spring Boot starters and then some using the alternate starters proposed by this repository. -There is a double motivation behind this: -- demo how much simpler OAuth2 configuration is with the alternate starters we propose here +There is a triple motivation behind this: +- demo how much simpler OAuth2 configuration is with `spring-addons-starter-oidc` - explain what is auto-configured (in addition to what already is by the official starters) +- demo test annotations usage with just `spring-addons-oauth2-test`. Tests in projects at `3.1.` and `3.2.` are declined in three versions: + * MockMvc request post-processor or WebTestClient mutator + * `@WithMockAuthentication`, defining authorities and name inline + * `@WithMockJwt`, loading claim-set from a classpath resource and using the `Converter` in the security configuration to turn it into an Authentication instance -### 3.1. Basic OAuth2 Resource Server With `spring-boot-starter-oauth2-resource-server` +### 3.1. OAuth2 Resource Server With Just `spring-boot-starter-oauth2-resource-server` Configure Spring Boot 3 applications as OAuth2 resource server (REST API) with authorities mapping to enable RBAC using roles defined on OIDC Providers. This tutorials are using only the "official" `spring-boot-starter-oauth2-resource-server` and are available for both [servlets](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/servlet-resource-server) and [reactive applications](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/reactive-resource-server). -### 3.2. Basic OAuth2 Client With `spring-boot-starter-oauth2-client` +### 3.2. OAuth2 Client With Just `spring-boot-starter-oauth2-client` Configure Spring Boot 3 applications as OAuth2 clients (Thymeleaf UI) with login, logout and authorities mapping to enable RBAC using roles defined on OIDC Providers. This tutorials are using only the "official" `spring-boot-starter-oauth2-client` and are available for both [servlets](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/servlet-client) and [reactive applications](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/reactive-client) @@ -144,7 +152,7 @@ This tutorials are using only the "official" `spring-boot-starter-oauth2-client` ### 3.3. [`resource-server_with_oauthentication`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/resource-server_with_oauthentication) Demos how to use a custom OAuth2 `Authentication` implementation: `OAthentication` with typed accessors to OpenID claims. -This tutorial uses an alternate Spring Boot starter from `spring-addons`, which greatly simplifies its Java configuration compared to section `3.1.`: all that was configured in Java configuration is now controlled with application properties. +This tutorial introduces `spring-addons-starter-oidc`, which greatly simplifies Java configuration compared to section `3.1.`: all the Java configuration is replaced with application properties. ### 3.4. [`resource-server_with_specialized_oauthentication`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/resource-server_with_specialized_oauthentication) Builds on top of preceding, showing how to diff --git a/samples/tutorials/bff/README.md b/samples/tutorials/bff/README.md index 9da17fe9f..4d76da376 100644 --- a/samples/tutorials/bff/README.md +++ b/samples/tutorials/bff/README.md @@ -8,7 +8,10 @@ In this tutorial, we will implement a n-tier application involving: * requests at `/bff/greetings-api/v1/greeting` authorized with a session cookie. This is the BFF pattern and what the Angular app uses. * requests at `/greetings-api/v1/greeting` authorized with an access token. This is what Postman or any other OAuth2 client would use. -The latest SNAPSHOT is deployed by CI / CD to a publicly available K8s cluster managed by [OVH](https://www.ovhcloud.com/fr/public-cloud/kubernetes/)): https://bff.demo.c4-soft.com/ui/ +The latest SNAPSHOT is deployed by CI / CD to a publicly available K8s cluster managed by [OVH](https://www.ovhcloud.com/fr/public-cloud/kubernetes/)): [https://bff.demo.c4-soft.com/ui/](https://bff.demo.c4-soft.com/ui/) + +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. ## 1. Prerequisites We assume that [tutorials main README prerequisites section](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#prerequisites) has been achieved and that you have a minimum of 1 OIDC Provider (2 would be better) with ID and secret for clients configured with authorization-code flow. @@ -31,7 +34,7 @@ But when it comes to providing with a multi-tenant OAuth2 client with login, log BFF aims at hiding the OAuth2 tokens from the browser. In this pattern, rich applications (Angular, React, Vue, etc.) are secured with sessions on a middle-ware, the BFF, which is the only OAuth2 client and replaces session cookie with an access token before forwarding a request from the browser to the resource server. There is a big trend toward this pattern because it is considered safer than JS applications configured as OAuth2 public clients as access tokens are: -- kept on the server instead of being exposed to the browser (and frequently to Javascript code) +- kept on the server instead of being exposed to the browser (and Javascript code) - delivered to OAuth2 confidential clients (browser apps can't keep a secret and are "public" clients), which reduces the risk that tokens are delivered to programs pretending to be the client we expect Keep in mind that sessions are a common attack vector and that this two conditions must be met: @@ -50,18 +53,19 @@ When user authentication is needed: ### 2.2. Project Initialization From [https://start.spring.io](https://start.spring.io) download a new project with: - Gateway +- OAuth2 client - Spring Boot Actuator - Lombok -Then, we'll add the a dependency to [`spring-addons-webflux-client`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-webflux-client/6.1.5) which is a thin wrapper around `spring-boot-starter-oauth2-client` which pushes auto-configuration from properties one step further. It provides with: -- a `SecurityWebFilterChain` with high precedence which intercepts all requests matched by `com.c4-soft.springaddons.security.client.security-matchers` +Then, we'll add the a dependency to [`spring-addons-starter-oidc`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/starter-oidc/7.0.0) to create for us: +- a `SecurityWebFilterChain` with which intercepts all requests matched by `com.c4-soft.springaddons.security.client.security-matchers` - a logout success handler configured from properties for "almost" OIDC complient providers (Auth0 and Cognito do not implement standrad RP-Initiated Logout) - a client side implementation for Back-Channel Logout - a few other features not important in this tutorial (multi-tenancy, as well as authorities mapping and CORS configuration from properties) ```xml com.c4-soft.springaddons - spring-addons-webflux-client + spring-addons-starter-oidc ${spring-addons.version} ``` @@ -100,6 +104,7 @@ spring: ``` And after that the OAuth2 configuration for an OAuth2 client allowing to users to authenticate (`authorization_code`) from 3 different OIDC Providers ```yaml +spring security: oauth2: client: @@ -110,8 +115,6 @@ And after that the OAuth2 configuration for an OAuth2 client allowing to users t issuer-uri: ${cognito-issuer} auth0: issuer-uri: ${auth0-issuer} - azure-ad: - issuer-uri: ${azure-ad-issuer} registration: keycloak-confidential-user: authorization-grant-type: authorization_code @@ -154,12 +157,15 @@ Next, comes the Gateway configuration itself with: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin - SaveSession routes: + # set a redirection from / to the UI - id: home uri: ${gateway-uri} predicates: - Path=/ filters: - RedirectTo=301,${gateway-uri}/ui/ + # BFF access to greetings API (with TokenRelay replacing session cookies with access tokens) + # To be used by SPAs (Angular app in our case) - id: greetings-api-bff uri: ${greetings-api-uri} predicates: @@ -167,24 +173,28 @@ Next, comes the Gateway configuration itself with: filters: - TokenRelay= - StripPrefix=3 + # direct access to greetings API (without the TokenRelay => requests should be authorized with an access tokens already) + # To be used by OAuth2 clients like Postman or mobile apps configured as OAuth2 (public) clients - id: greetings-api-oauth2-clients uri: ${greetings-api-uri} predicates: - Path=/greetings-api/v1/** filters: - StripPrefix=2 + # access to UI resources (Angular app in our case) - id: ui uri: ${ui-uri} predicates: - Path=/ui/** + # used by the cert manager on K8s - id: letsencrypt uri: https://cert-manager-webhook predicates: - Path=/.well-known/acme-challenge/** ``` -Then comes spring-addons configuration for OAuth2 clients: -- `issuers` properties for each of the OIDC Providers we trust (issuer URI, authorities mapping and claim to use as username) +Then comes `spring-addons-starter-oidc` configuration: +- `ops` properties for each of the OIDC Providers we trust (issuer URI, authorities mapping and claim to use as username) - `client-uri` is used to work with absolute URIs in login process - `security-matchers` is an array of path matchers for routes processed by the auto-configured client security filter-chain. If null auto-configuration is turned off. Here, it will filter all traffic. - `permit-all` is a list of path matchers for resources accessible to all requests, even unauthorized ones (end-points not listed here like `/logout` will be accessible only to authenticated users) @@ -196,28 +206,33 @@ Then comes spring-addons configuration for OAuth2 clients: - `login-path`, `post-login-redirect-path` and `post-logout-redirect-path` are pretty straight forward. this are relative path to the `client-uri` configured earlier - `back-channel-logout-enabled` when set to `true`, a `/backchannel-logout` end-point is added, listening for POST requests from the OIDC Providers when a user logs out from another application the current client (useful in SSO environments). This endpoint is secured by a dedicated filter-chain matching only `/backchannel-logout`. - `oauth2-logout` is the RP-Initiated Logout configuration for OIDC Providers not following the standard (logout endpoint missing from the OpenID configuration or exotic request parameter names) +- as both the UI and REST API are served through the gateway, there are no cross-origin requests and we don't need CORS configuration ```yaml com: c4-soft: springaddons: - security: - issuers: - - location: ${keycloak-issuer} + oidc: + # OpenID Providers configuration (shared by client and resource server filter-chains) + 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 + # OAuth2 client specific configuration: mostly login, logout and CSRF protection client: client-uri: ${gateway-uri} + # For the sake of simplicity, all requests will be processed by the client filterchain + # A resource optimisation could be excluding actuator, swagger and cert-manager endpoints to save the creation of useless sessions security-matchers: /** permit-all: - /login/** @@ -230,20 +245,24 @@ com: - /actuator/health/readiness - /actuator/health/liveness - /.well-known/acme-challenge/** + # The Angular app needs access to the CSRF cookie (to return its value as X-XSRF-TOKEN header) csrf: cookie-accessible-from-js login-path: /ui/ post-login-redirect-path: /ui/ post-logout-redirect-path: /ui/ + # This is an "experemiental" feature, use with caution back-channel-logout-enabled: true + # Auth0 and Cognito do not follow strictly the OpenID RP-Initiated Logout spec and need specific configuration oauth2-logout: - - client-registration-id: cognito-confidential-user + cognito: uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout client-id-request-param: client_id post-logout-uri-request-param: logout_uri - - client-registration-id: auth0-confidential-user + auth0: uri: ${auth0-issuer}v2/logout client-id-request-param: client_id post-logout-uri-request-param: returnTo + # Auth0 requires client to provide with audience in authorization-code request authorization-request-params: auth0-confidential-user: - name: audience @@ -384,11 +403,12 @@ This resource server will expose a single `/greetings` endpoint returning a mess ### 3.1. Project Initialization From [https://start.spring.io](https://start.spring.io) download a new project with: - Spring Web +- OAuth2 Resource Server - Spring Boot Actuator and then add this dependencies: -- [`spring-addons-webmvc-jwt-resource-server`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-webmvc-jwt-resource-server/6.1.5) -- [`spring-addons-webmvc-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-webmvc-test/6.1.5) +- [`spring-addons-starter-oidc`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc/7.0.0) +- [`spring-addons-starter-oidc-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc-test/7.0.0) - [`swagger-annotations-jakarta`](https://central.sonatype.com/artifact/io.swagger.core.v3/swagger-annotations-jakarta/2.2.8) for a cleaner OpenAPI specification (if the maven `openapi` profile, which is omitted in the tutorial but included in the source, is activted) ### 3.2. Application Properties @@ -416,30 +436,28 @@ 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: roles - path: permissions - permit-all: - - "/public/**" - - "/actuator/health/readiness" - - "/actuator/health/liveness" - - "/v3/api-docs/**" + resourceserver: + permit-all: + - "/public/**" + - "/actuator/health/readiness" + - "/actuator/health/liveness" + - "/v3/api-docs/**" logging: level: @@ -483,7 +501,7 @@ A resource server security filter-chain is auto-configured by spring-addons. Her @EnableMethodSecurity static class WebSecurityConfig { @Bean - Converter> jwtAuthenticationConverter( + JwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter( Converter, Collection> authoritiesConverter, SpringAddonsSecurityProperties addonsProperties) { return jwt -> new OAuthentication<>( 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..ddc38d0e4 100644 --- a/samples/tutorials/bff/gateway/src/main/resources/application.yml +++ b/samples/tutorials/bff/gateway/src/main/resources/application.yml @@ -29,6 +29,7 @@ spring: timeout-per-shutdown-phase: 30s security: oauth2: + # When configuring an OAuth2 client with login, Spring Boot standard "provider" and "registration" properties are necessary client: provider: keycloak: @@ -37,8 +38,6 @@ spring: issuer-uri: ${cognito-issuer} auth0: issuer-uri: ${auth0-issuer} - azure-ad: - issuer-uri: ${azure-ad-issuer} registration: keycloak-confidential-user: authorization-grant-type: authorization_code @@ -67,12 +66,15 @@ spring: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin - SaveSession routes: + # set a redirection from / to the UI - id: home uri: ${gateway-uri} predicates: - Path=/ filters: - RedirectTo=301,${gateway-uri}/ui/ + # BFF access to greetings API (with TokenRelay replacing session cookies with access tokens) + # To be used by SPAs (Angular app in our case) - id: greetings-api-bff uri: ${greetings-api-uri} predicates: @@ -80,42 +82,51 @@ spring: filters: - TokenRelay= - StripPrefix=3 + # direct access to greetings API (without the TokenRelay => requests should be authorized with an access tokens already) + # To be used by OAuth2 clients like Postman or mobile apps configured as OAuth2 (public) clients - id: greetings-api-oauth2-clients uri: ${greetings-api-uri} predicates: - Path=/greetings-api/v1/** filters: - StripPrefix=2 + # access to UI resources (Angular app in our case) - id: ui uri: ${ui-uri} predicates: - Path=/ui/** + # used by the cert manager on K8s - id: letsencrypt uri: https://cert-manager-webhook predicates: - Path=/.well-known/acme-challenge/** +# spring-addons-starter-oidc specific configuration com: c4-soft: springaddons: - security: - issuers: - - location: ${keycloak-issuer} + oidc: + # OpenID Providers configuration (shared by client and resource server filter-chains) + 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 + # OAuth2 client specific configuration: mostly login, logout and CSRF protection client: client-uri: ${gateway-uri} + # For the sake of simplicity, all requests will be processed by the client filterchain + # A resource optimisation could be excluding actuator, swagger and cert-manager endpoints to save the creation of useless sessions security-matchers: /** permit-all: - /login/** @@ -128,20 +139,24 @@ com: - /actuator/health/readiness - /actuator/health/liveness - /.well-known/acme-challenge/** + # The Angular app needs access to the CSRF cookie (to return its value as X-XSRF-TOKEN header) csrf: cookie-accessible-from-js login-path: /ui/ post-login-redirect-path: /ui/ post-logout-redirect-path: /ui/ + # This is an "experemiental" feature, use with caution back-channel-logout-enabled: true + # Auth0 and Cognito do not follow strictly the OpenID RP-Initiated Logout spec and need specific configuration oauth2-logout: - - client-registration-id: cognito-confidential-user + cognito: uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout client-id-request-param: client_id post-logout-uri-request-param: logout_uri - - client-registration-id: auth0-confidential-user + auth0: uri: ${auth0-issuer}v2/logout client-id-request-param: client_id post-logout-uri-request-param: returnTo + # Auth0 requires client to provide with audience in authorization-code request authorization-request-params: auth0-confidential-user: - name: audience 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/README.md b/samples/tutorials/reactive-client/README.md index c1a1641cb..8c91e33ca 100644 --- a/samples/tutorials/reactive-client/README.md +++ b/samples/tutorials/reactive-client/README.md @@ -1,5 +1,8 @@ # Reactive OAuth2 Client with Login, Logout and Authorities Mapping -In this tutorial, we'll configure a reactive (WebFlux) Spring Boot 3 application as an OAuth2 client with login, logout and authorities mapping to enable RBAC using roles defined on OIDC Providers. +In this tutorial, we'll configure a reactive (WebFlux) Spring Boot 3 application as an OAuth2 client with login, logout and authorities mapping to enable RBAC using roles defined on OIDC Providers, **without `spring-addons-starter-oidc`**, which makes its security configuration quite more verbose compared to the configuration of the BFF gateway. + +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. ## 1. Project Initialization We start after [prerequisites](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#2-prerequisites), and consider that we have a minimum of 1 OIDC Provider configured (2 would be better) and users with and without `NICE` role declared on eac OPh. 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/README.md b/samples/tutorials/reactive-resource-server/README.md index 2a5afda2e..6d12277cf 100644 --- a/samples/tutorials/reactive-resource-server/README.md +++ b/samples/tutorials/reactive-resource-server/README.md @@ -1,8 +1,11 @@ # Configure a Reactive OAuth2 Resource Server (REST API) -In this tutorial, we'll configure a reactive (WebFlux) Spring Boot 3 application as an OAuth2 resource server with authorities mapping to enable RBAC using roles defined on OIDC Providers. +In this tutorial, we'll configure a reactive (WebFlux) Spring Boot 3 application as an OAuth2 resource server with authorities mapping to enable RBAC using roles defined on OIDC Providers, *without `spring-addons-starter-oidc`**, which makes quite more verbose compared to the "webflux" projects in samples. We'll also see how to accept access tokens issued by several, potentially heterogeneous, OIDC Providers (or Keycloak realms). +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Project Initialization We start after [prerequisites](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#2-prerequisites), and consider that we have a minimum of 1 OIDC Provider configured (2 would be better) and users with and without `NICE` role declared on each OP. As usual, we'll start with http://start.spring.io/ adding the following dependencies: 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/README.md b/samples/tutorials/resource-server_multitenant_dynamic/README.md index 7fb45f549..ee558bc52 100644 --- a/samples/tutorials/resource-server_multitenant_dynamic/README.md +++ b/samples/tutorials/resource-server_multitenant_dynamic/README.md @@ -1,6 +1,9 @@ # How to configure a Spring REST API dynamic tenants Sample of advanced customization of spring-addons auto-configuration: in this tutorial, the resource server should accept access tokens issued by any issuer hosted on a list of servers we trust (for instance dynamically generated Keycloak realms). For that, we'll customize the way issuer properties are resolved and also modify the authentication manager resolver to create a new authentication manager for each new issuer hosted on a server we trust. +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Prerequisites We assume that [tutorials main README prerequisites section](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#prerequisites) has been achieved and that you have a minimum of 1 OIDC Provider (2 would be better) with ID and secret for clients configured with authorization-code flow. @@ -107,21 +110,18 @@ server: 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'] 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/README.md b/samples/tutorials/resource-server_with_additional-header/README.md index d99d0178a..c5e183055 100644 --- a/samples/tutorials/resource-server_with_additional-header/README.md +++ b/samples/tutorials/resource-server_with_additional-header/README.md @@ -1,5 +1,8 @@ # Authentication containing data from both the access token and a custom header +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Overview For this tutorial, we will assume that in addition to to a JWT **access token** in the `Authorization` header, the OAuth2 client provides with a JWT **ID token** in a `X-ID-Token` header. @@ -8,6 +11,8 @@ Be sure your environment meets [tutorials prerequisits](https://github.com/ch4mp ## 2. Project Initialization We'll start a spring-boot 3 project with the help of https://start.spring.io/ Following dependencies will be needed: +- Spring Web +- OAuth2 Resource Server - Lombok Then add dependencies to: @@ -16,14 +21,12 @@ Then add dependencies to: ```xml com.c4-soft.springaddons - - spring-addons-webmvc-jwt-resource-server + spring-addons-starter-oidc ${spring-addons.version} com.c4-soft.springaddons - - spring-addons-webmvc-jwt-test + spring-addons-starter-oidc-test ${spring-addons.version} test @@ -55,7 +58,7 @@ public static class MyAuth extends OAuthentication { } ``` -- provide a `Converter` bean to switch `Authentication` implementation from `JwtAuthenticationToken` to our `MyAuth` +- provide a `JwtAbstractAuthenticationTokenConverter` bean to switch `Authentication` implementation from `JwtAuthenticationToken` to our `MyAuth` ```java public static final String ID_TOKEN_HEADER_NAME = "X-ID-Token"; @@ -75,8 +78,7 @@ private JwtDecoder getJwtDecoder(Map accessClaims) { return idTokenDecoders.get(iss); } -@Bean -Converter jwtAuthenticationConverter(Converter, Collection> authoritiesConverter) { +@BeanJwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(Converter, Collection> authoritiesConverter) { return jwt -> { try { final var jwtDecoder = getJwtDecoder(jwt.getClaims()); @@ -122,7 +124,7 @@ spring: com: c4-soft: springaddons: - security: + oidc: cors: - path: /** allowed-origins: ${origins} 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/README.md b/samples/tutorials/resource-server_with_introspection/README.md index f1b94426a..1c693d7cb 100644 --- a/samples/tutorials/resource-server_with_introspection/README.md +++ b/samples/tutorials/resource-server_with_introspection/README.md @@ -1,5 +1,8 @@ # How to configure a Spring REST API with token introspection +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Overview The aim here is to setup security for a Spring Boot 3 resource server access token introspection on **any OpenID authorization-server**: those exposing an introspection endpoint in their OpenID configuration (like Keycloak), but also those just exposing a `/userinfo` endpoint (like Auth0 and Amazon Cognito). 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/README.md b/samples/tutorials/resource-server_with_oauthentication/README.md index d93a9dd0b..097a4b6be 100644 --- a/samples/tutorials/resource-server_with_oauthentication/README.md +++ b/samples/tutorials/resource-server_with_oauthentication/README.md @@ -1,6 +1,9 @@ # How to configure a Spring REST API with `OAuthentication` The aim here is to setup security for a Spring Boot 3 resource server with JWT decoding and a custom `Authentication` implementation instead of the default `JwtAuthenticationToken` +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Prerequisites We assume that [tutorials main README prerequisites section](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials#prerequisites) has been achieved and that you have a minimum of 1 OIDC Provider (2 would be better) with ID and secret for clients configured with authorization-code flow. 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/README.md b/samples/tutorials/resource-server_with_specialized_oauthentication/README.md index c6eaf2f5a..366eef722 100644 --- a/samples/tutorials/resource-server_with_specialized_oauthentication/README.md +++ b/samples/tutorials/resource-server_with_specialized_oauthentication/README.md @@ -1,5 +1,8 @@ # How to extend `OAuthentication` +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Overview Lets says that we have business requirements where security is not role based only. 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/README.md b/samples/tutorials/resource-server_with_ui/README.md index fa4a3b530..f235cd525 100644 --- a/samples/tutorials/resource-server_with_ui/README.md +++ b/samples/tutorials/resource-server_with_ui/README.md @@ -1,6 +1,9 @@ # Mix OAuth2 Client and Resource Server Configurations in a Single Application The aim here is to **configure a Spring back-end as both OAuth2 client and resource server while allowing users to authenticate among a list of heterogeneous trusted authorization-servers**: a local Keycloak realm as well as remote Auth0 and Cognito instances. +## 0. Disclaimer +There are quite a few samples, and all are part of CI to ensure that source compile and all tests pass. Unfortunately, this README is not automatically updated when source changes. Please use it as a guidance to understand the source. **If you copy some code, be sure to do it from the source, not from this README**. + ## 1. Preamble We'll define two distinct and ordered security filter-chains: - the 1st with client configuration, with login, logout, and a security matcher limiting it to UI resources @@ -182,11 +185,11 @@ com: post-logout-redirect-path: /ui/greet back-channel-logout-enabled: true oauth2-logout: - - client-registration-id: cognito-confidential-user + cognito: uri: https://spring-addons.auth.us-west-2.amazoncognito.com/logout client-id-request-param: client_id post-logout-uri-request-param: logout_uri - - client-registration-id: auth0-confidential-user + auth0: uri: ${auth0-issuer}v2/logout client-id-request-param: client_id post-logout-uri-request-param: returnTo 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 @@ *