Skip to content

Commit

Permalink
Refresh tutorials READMEs
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Nov 28, 2024
1 parent f58a49b commit e549331
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 604 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
scheme: http
keycloak-port: 8080
keycloak-host: ${scheme}://localhost:${keycloak-port}

server:
error:
Expand All @@ -17,7 +18,7 @@ com:
springaddons:
oidc:
ops:
- iss: ${scheme}://localhost:${keycloak-port}
- iss: ${keycloak-host}
username-claim: preferred_username
authorities:
- path: $.realm_access.roles
Expand Down
142 changes: 53 additions & 89 deletions samples/tutorials/resource-server_with_additional-header/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Following dependencies will be needed:
- Lombok

Then add dependencies to:
- [`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-jwt-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-webmvc-jwt-test/6.1.5) with `test` scope
- [`spring-addons-starter-oidc`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc)
- [`spring-addons-starter-oidc-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc-test) with `test` scope
```xml
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
Expand All @@ -35,127 +35,91 @@ Then add dependencies to:
If for whatever reason you don't want to use `spring-addons-starter-oidc`, see [`servlet-resource-server`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/servlet-resource-server) or [`reactive-resource-server`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/reactive-resource-server) for basic configuration with `spring-boot-starter-oauth2-resource-server`. Spoiler, it is quite more verbose and error-prone.

## 3. Web-Security Configuration
This configuration will use the pretty convenient [`com.c4_soft.springaddons.security.oauth2.config.synchronised.HttpServletRequestSupport`](https://github.com/ch4mpy/spring-addons/blob/master/webmvc/spring-addons-webmvc-core/src/main/java/com/c4_soft/springaddons/security/oauth2/config/synchronised/HttpServletRequestSupport.java) which provides tooling to access the current request, and in our case, its headers. If we were writing a WebFlux application, we'd use is reactive equivalent: [`com.c4_soft.springaddons.security.oauth2.config.reactive.ServerHttpRequestSupport`](https://github.com/ch4mpy/spring-addons/blob/master/webflux/spring-addons-webflux-core/src/main/java/com/c4_soft/springaddons/security/oauth2/config/reactive/ServerHttpRequestSupport.java). If you don't use `spring-addons-starter-oidc`, you might need to copy some code from one of this support classes.
This configuration will use the pretty convenient [`HttpServletRequestSupport`](https://github.com/ch4mpy/spring-addons/blob/master/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/HttpServletRequestSupport.java) which provides tooling to access the current request, and in our case, its headers. If we were writing a WebFlux application, we'd use is reactive equivalent: [`ServerHttpRequestSupport`](https://github.com/ch4mpy/spring-addons/blob/master/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/ServerHttpRequestSupport.java). If you don't use `spring-addons-starter-oidc`, you might need to copy some code from one of this support classes.

`spring-oauth2-addons` comes with `@AutoConfiguration` for web-security config adapted to REST API projects. We'll just add:
- `@EnableMethodSecurity` to activate `@PreAuthorize` on components methods.
- create an authentication of our own designed to hold ID token string and claims in addition to access token ones: `MyAuth`

- an authentication of our own designed to hold ID token string and claims in addition to access token ones:
```java
@Data
@EqualsAndHashCode(callSuper = true)
public static class MyAuth extends OAuthentication<OpenidClaimSet> {
private static final long serialVersionUID = 1734079415899000362L;
private final String idTokenString;
private final OpenidClaimSet idClaims;

public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(accessClaims, authorities, accessTokenString);
this.idTokenString = idTokenString;
this.idClaims = idClaims;
}
public static class MyAuth extends OAuthentication<OpenidToken> {
private static final long serialVersionUID = 1734079415899000362L;
private final OpenidToken idToken;

public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(new OpenidToken(accessClaims, accessTokenString), authorities);
this.idToken = new OpenidToken(idClaims, idTokenString);
}

}
```
- provide a `JwtAbstractAuthenticationTokenConverter` bean to switch `Authentication` implementation from `JwtAuthenticationToken` to our `MyAuth`

- a `JwtAbstractAuthenticationTokenConverter` bean to switch `Authentication` implementation from `JwtAuthenticationToken` to `MyAuth`
```java
@Bean
JwtAbstractAuthenticationTokenConverter authenticationConverter(
// Inject a converter to turn token claims into Spring authorities. A default one is provided by spring-addons-starter-oidc, if you haven't define one
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return jwt -> {
try {
// Resolve the JWT decoder based on token claims (more on that below)
final var jwtDecoder = getJwtDecoder(jwt.getClaims());
final var authorities = authoritiesConverter.convert(jwt.getClaims());
final var idTokenString =
HttpServletRequestSupport.getUniqueRequestHeader(ID_TOKEN_HEADER_NAME);
final var idToken = jwtDecoder == null ? null : jwtDecoder.decode(idTokenString);

return new MyAuth(authorities, jwt.getTokenValue(), new OpenidClaimSet(jwt.getClaims()),
idTokenString, new OpenidClaimSet(idToken.getClaims()));
} catch (JwtException e) {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}
};
}
```
- a cash for ID tokens JWT decoders (instantiate only one decoder per ID token issuer). For that, we add the following to the configuration class:
```java
public static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
private static final Map<String, JwtDecoder> idTokenDecoders = new ConcurrentHashMap<>();

private JwtDecoder getJwtDecoder(Map<String, Object> accessClaims) {
if (accessClaims == null) {
return null;
}
final var iss = Optional.ofNullable(accessClaims.get(JwtClaimNames.ISS)).map(Object::toString).orElse(null);
if (iss == null) {
return null;
}
if (!idTokenDecoders.containsKey(iss)) {
idTokenDecoders.put(iss, JwtDecoders.fromIssuerLocation(iss));
}
return idTokenDecoders.get(iss);
}

@BeanJwtAbstractAuthenticationTokenConverter jwtAuthenticationConverter(Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return jwt -> {
try {
final var jwtDecoder = getJwtDecoder(jwt.getClaims());
final var authorities = authoritiesConverter.convert(jwt.getClaims());
final var idTokenString = HttpServletRequestSupport.getUniqueHeader(ID_TOKEN_HEADER_NAME);
final var idToken = jwtDecoder == null ? null : jwtDecoder.decode(idTokenString);

return new MyAuth(
authorities,
jwt.getTokenValue(),
new OpenidClaimSet(jwt.getClaims()),
idTokenString,
new OpenidClaimSet(idToken.getClaims()));
} catch (JwtException e) {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}
};
if (accessClaims == null) {
return null;
}
final var iss =
Optional.ofNullable(accessClaims.get(JwtClaimNames.ISS)).map(Object::toString).orElse(null);
if (iss == null) {
return null;
}
if (!idTokenDecoders.containsKey(iss)) {
idTokenDecoders.put(iss, JwtDecoders.fromIssuerLocation(iss));
}
return idTokenDecoders.get(iss);
}
```

If you don't use `spring-addons-starter-oidc`, you'll have to provide the authentication converter (or an authentication manager resolver) when configuring `http.resourceServer()`.

## 4. Application Properties
Nothing really special here, just the usual Spring Boot and spring-addons configuration (accepting identities from 3 different trusted issuers):
```yaml
scheme: http
origins: ${scheme}://localhost:4200
keycloak-port: 8442
keycloak-issuer: ${scheme}://localhost:${keycloak-port}/realms/master
cognito-issuer: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
auth0-issuer: https://dev-ch4mpy.eu.auth0.com/

server:
error:
include-message: always
ssl:
enabled: false

spring:
lifecycle:
timeout-per-shutdown-phase: 30s

com:
c4-soft:
springaddons:
oidc:
cors:
- path: /**
allowed-origins: ${origins}
issuers:
- location: ${keycloak-issuer}
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

---
scheme: https
keycloak-port: 8443

server:
ssl:
enabled: true

spring:
config:
activate:
on-profile: ssl
```
## 5. Sample `@RestController`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ JwtAbstractAuthenticationTokenConverter authenticationConverter(
@Bean
ResourceServerExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
// @formatter:off
return (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) -> registry
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/actuator/**")).hasAuthority("OBSERVABILITY:read")
.requestMatchers(new AntPathRequestMatcher("/actuator/**")).hasAuthority("OBSERVABILITY:write")
.anyRequest().authenticated();
// @formatter:on
return (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) -> registry
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/actuator/**")).hasAuthority("OBSERVABILITY:read")
.requestMatchers(new AntPathRequestMatcher("/actuator/**")).hasAuthority("OBSERVABILITY:write")
.anyRequest().authenticated();
// @formatter:on
}

@Data
Expand Down
69 changes: 23 additions & 46 deletions samples/tutorials/resource-server_with_introspection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,22 @@ Following dependencies will be needed:
- Lombok

Then add dependencies to spring-addons:
- [`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-jwt-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-webmvc-jwt-test/6.1.5) with `test` scope
- [`spring-addons-starter-oidc`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc)
- [`spring-addons-starter-oidc-test`](https://central.sonatype.com/artifact/com.c4-soft.springaddons/spring-addons-starter-oidc-test) with `test` scope
```xml
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- use spring-addons-webflux-jwt-resource-server instead for reactive apps -->
<artifactId>spring-addons-webmvc-introspecting-resource-server</artifactId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>${spring-addons.version}</version>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- use spring-addons-webflux-test instead for reactive apps -->
<artifactId>spring-addons-webmvc-introspecting-test</artifactId>
<artifactId>spring-addons-starter-oidc-test</artifactId>
<version>${spring-addons.version}</version>
<scope>test</scope>
</dependency>
```

If for whatever reason you don't want to use `spring-addons-starter-oidc`, see [`servlet-resource-server`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/servlet-resource-server) or [`reactive-resource-server`](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials/reactive-resource-server) for basic configuration with `spring-boot-starter-oauth2-resource-server`. Spoiler, it is quite more verbose and error-prone.

## 5. Application Properties
Let's first define some constants later used in configuration, and sometimes overridden in profiles:
```yaml
Expand Down Expand Up @@ -85,20 +81,21 @@ Next is some spring-addons configuration with:
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/**"
```
Last is profile to enable SSL on this server and when talking to the local Keycloak instance:
```yaml
Expand All @@ -118,28 +115,7 @@ spring:
```

## 4. Web-Security Configuration
`spring-addons-webmvc-introspecting-resource-server` auto-configures a security filter-chain for resource server with token introspection and there is nothing we have to do beyond activating method security.

Optionally, we can switch the type of `Authentication` at runtime by providing an`OpaqueTokenAuthenticationConverter`. To press on the fact that this bean is not mandatory, we'll activate it with a profile:
```java
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
@Bean
@Profile("oauthentication")
//This bean is optional as a default one is provided (building a BearerTokenAuthentication)
OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return (String introspectedToken,
OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> new OAuthentication<>(
new OpenidClaimSet(authenticatedPrincipal.getAttributes()),
authoritiesConverter.convert(authenticatedPrincipal.getAttributes()),
introspectedToken);
}
}
```
The reasons why we could prefer `OAuthentication<OpenidClaimSet>` over `BearerTokenAuthentication` are its improved API to access OpenID claims and its versatility (compatible with JWT decoding too).
`spring-addons-starter-oidc` auto-configures a security filter-chain for resource server with token introspection and there is nothing we have to do beyond activating method security.

## 6. Non-Standard Introspection Endpoint
The token introspection we have works just fine with OIDC Providers exposing an `introspection_endpoint` in their OpenID configuration (like Keycloak does), but some just don't provide one (like Auth0 and Amazon Cognito). Hopefully, almost any OP exposes a `/userinfo` endpoint returning the OpenID claims of the user for whom was issued the access token in the Authorization header.
Expand All @@ -151,9 +127,9 @@ Let's first define spring profiles with `introspection-uri` settled with userinf
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${auth0-issuer}
oidc:
ops:
- iss: ${auth0-issuer}
username-claim: $['https://c4-soft.com/user']['name']
authorities:
- path: $['https://c4-soft.com/user']['roles']
Expand All @@ -173,9 +149,9 @@ spring:
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${cognito-issuer}
oidc:
ops:
- iss: ${cognito-issuer}
username-claim: $.username
authorities:
- path: $.cognito:groups
Expand Down Expand Up @@ -219,6 +195,7 @@ public static class UserEndpointOpaqueTokenIntrospector implements OpaqueTokenIn
}
```
Exposing such an `OpaqueTokenIntrospector` and exposing it as a bean is enough with `spring-addons-starter-oidc` which will pick it instead of defining a default one.

## 7. Sample `@RestController`
``` java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ management:
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${auth0-issuer}
oidc:
ops:
- iss: ${auth0-issuer}
username-claim: $['https://c4-soft.com/user']['name']
authorities:
- path: $['https://c4-soft.com/user']['roles']
Expand All @@ -91,9 +91,9 @@ spring:
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${cognito-issuer}
oidc:
ops:
- iss: ${cognito-issuer}
username-claim: $.username
authorities:
- path: $.cognito:groups
Expand Down
Loading

0 comments on commit e549331

Please sign in to comment.