Skip to content

Commit

Permalink
Added keycloak configuration.
Browse files Browse the repository at this point in the history
  • Loading branch information
rbiedrawa committed Apr 6, 2021
1 parent 454ea5b commit 2f8e0d4
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 15 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ext {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = 'spring-oauth2'
rootProject.name = 'spring-webflux-keycloak-demo'
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.rbiedrawa.oauth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.server.SecurityWebFilterChain;

import reactor.core.publisher.Mono;

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter) {
// @formatter:off
http.authorizeExchange()
.pathMatchers("/hello/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter);
// @formatter:on

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.rbiedrawa.oauth.config.keycloak;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;

import reactor.core.publisher.Mono;

@Configuration
public class KeycloakConfiguration {

@Bean
Converter<Jwt, Collection<GrantedAuthority>> keycloakGrantedAuthoritiesConverter(@Value("${app.security.clientId}") String clientId) {
return new KeycloakGrantedAuthoritiesConverter(clientId);
}

@Bean
Converter<Jwt, Mono<AbstractAuthenticationToken>> keycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> converter) {
return new ReactiveKeycloakJwtAuthenticationConverter(converter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.rbiedrawa.oauth.config.keycloak;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.util.ObjectUtils;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class KeycloakGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
private static final String ROLES = "roles";
private static final String CLAIM_REALM_ACCESS = "realm_access";
private static final String RESOURCE_ACCESS = "resource_access";

private final Converter<Jwt, Collection<GrantedAuthority>> defaultAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

private final String clientId;

@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
var realmRoles = realmRoles(jwt);
var clientRoles = clientRoles(jwt, clientId);

Collection<GrantedAuthority> authorities = Stream.concat(realmRoles.stream(), clientRoles.stream())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
authorities.addAll(defaultGrantedAuthorities(jwt));

return authorities;
}

private Collection<GrantedAuthority> defaultGrantedAuthorities(Jwt jwt) {
return Optional.ofNullable(defaultAuthoritiesConverter.convert(jwt))
.orElse(emptySet());
}

@SuppressWarnings("unchecked")
protected List<String> realmRoles(Jwt jwt) {
return Optional.ofNullable(jwt.getClaimAsMap(CLAIM_REALM_ACCESS))
.map(realmAccess -> (List<String>) realmAccess.get(ROLES))
.orElse(emptyList());
}

@SuppressWarnings("unchecked")
protected List<String> clientRoles(Jwt jwt, String clientId) {
if (ObjectUtils.isEmpty(clientId)) {
return emptyList();
}

return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS))
.map(resourceAccess -> (Map<String, List<String>>) resourceAccess.get(clientId))
.map(clientAccess -> clientAccess.get(ROLES))
.orElse(emptyList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.rbiedrawa.oauth.config.keycloak;

import java.util.Collection;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter;
import org.springframework.util.Assert;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* @see org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter
*/
public final class ReactiveKeycloakJwtAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {

private static final String USERNAME_CLAIM = "preferred_username";
private final Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter;

public ReactiveKeycloakJwtAuthenticationConverter(Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null");
this.jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter(
jwtGrantedAuthoritiesConverter);
}

@Override
public Mono<AbstractAuthenticationToken> convert(Jwt jwt) {
// @formatter:off
return this.jwtGrantedAuthoritiesConverter.convert(jwt)
.collectList()
.map((authorities) -> new JwtAuthenticationToken(jwt, authorities, extractUsername(jwt)));
// @formatter:on
}

protected String extractUsername(Jwt jwt) {
return jwt.containsClaim(USERNAME_CLAIM) ? jwt.getClaimAsString(USERNAME_CLAIM) : jwt.getSubject();
}

}
39 changes: 39 additions & 0 deletions src/main/java/com/rbiedrawa/oauth/web/AboutMeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.rbiedrawa.oauth.web;

import java.util.Map;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/me")
public class AboutMeController {

@GetMapping
Mono<Map<String, Object>> claims(@AuthenticationPrincipal JwtAuthenticationToken auth) {
return Mono.just(auth.getTokenAttributes());
}

@GetMapping("/token")
Mono<String> token(@AuthenticationPrincipal JwtAuthenticationToken auth) {
return Mono.just(auth.getToken().getTokenValue());
}

@GetMapping("/role_admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
Mono<String> role_admin() {
return Mono.just("ROLE_ADMIN");
}

@GetMapping("/scope_messages_read")
@PreAuthorize("hasAuthority('SCOPE_MESSAGES:READ')")
Mono<String> scope_api_me_read() {
return Mono.just("You have 'MESSAGES:READ' scope");
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/rbiedrawa/oauth/web/HelloController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.rbiedrawa.oauth.web;

import java.security.Principal;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController
public class HelloController {

@GetMapping("hello")
Mono<String> hello(@AuthenticationPrincipal Principal auth) {
return Mono.just(String.format("Hello %s!", auth.getName()));
}
}
11 changes: 11 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
server:
port: 8080

app:
security:
clientId: "spring_keycloak_demo_client"

spring:
security.oauth2.resourceserver.jwt.issuer-uri: "http://localhost:8081/auth/realms/demo"

logging.level:
org.springframework.security: DEBUG
14 changes: 0 additions & 14 deletions src/test/java/com/rbiedrawa/oauth/ApplicationTest.java

This file was deleted.

0 comments on commit 2f8e0d4

Please sign in to comment.