Skip to content

Commit

Permalink
Some documentation and test fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
andifalk committed Mar 9, 2021
1 parent 5db1a58 commit 0f44024
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 101 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ The components you will build (and use) look like this:
__Please check out the [complete documentation](application-architecture) for the sample application before
starting with the first hands-on lab__.

All the code currently is build using:
All the code currently is build using

* [Spring Boot 2.4.x Release](https://spring.io/blog/2020/11/12/spring-boot-2-4-0-available-now)
* [Spring Framework 5.3.x Release](https://spring.io/blog/2020/10/27/spring-framework-5-3-goes-ga)
* [Spring Security 5.4.x Release](https://spring.io/blog/2020/09/10/spring-security-5-4-goes-ga)
* [Spring Batch 4.3.x Release](https://spring.io/blog/2020/10/28/spring-batch-4-3-is-now-ga)

and is verified against the currently supported long-term version 11 of Java (The latest version 14 should work as well).
All code is verified against the currently supported long-term version 11 of Java (The latest version 14 should work as well).

To check system requirements and setup for this workshop please follow the [setup guide](setup).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
Expand All @@ -17,7 +19,7 @@
@SuppressWarnings("unused")
public class LibraryUserJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String GROUPS_CLAIM = "groups";
private static final String SCOPE_CLAIM = "scope";
private static final String ROLE_PREFIX = "ROLE_";

private final LibraryUserDetailsService libraryUserDetailsService;
Expand All @@ -37,19 +39,24 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
}

private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getGroups(jwt).stream()
return this.getScopes(jwt).stream()
.map(authority -> ROLE_PREFIX + authority.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

@SuppressWarnings("unchecked")
private Collection<String> getGroups(Jwt jwt) {
Object groups = jwt.getClaims().get(GROUPS_CLAIM);
if (groups instanceof Collection) {
return (Collection<String>) groups;
private Collection<String> getScopes(Jwt jwt) {
Object scopes = jwt.getClaims().get(SCOPE_CLAIM);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
return Arrays.asList(((String) scopes).split(" "));
}
return Collections.emptyList();
}
if (scopes instanceof Collection) {
return (Collection<String>) scopes;
}

return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
Expand All @@ -17,7 +19,7 @@
@SuppressWarnings("unused")
public class LibraryUserJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String GROUPS_CLAIM = "groups";
private static final String SCOPE_CLAIM = "scope";
private static final String ROLE_PREFIX = "ROLE_";

private final LibraryUserDetailsService libraryUserDetailsService;
Expand All @@ -37,19 +39,24 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
}

private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getGroups(jwt).stream()
return this.getScopes(jwt).stream()
.map(authority -> ROLE_PREFIX + authority.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

@SuppressWarnings("unchecked")
private Collection<String> getGroups(Jwt jwt) {
Object groups = jwt.getClaims().get(GROUPS_CLAIM);
if (groups instanceof Collection) {
return (Collection<String>) groups;
private Collection<String> getScopes(Jwt jwt) {
Object scopes = jwt.getClaims().get(SCOPE_CLAIM);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
return Arrays.asList(((String) scopes).split(" "));
}
return Collections.emptyList();
}
if (scopes instanceof Collection) {
return (Collection<String>) scopes;
}

return Collections.emptyList();
}
}
2 changes: 1 addition & 1 deletion bonus-labs/micronaut-server-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ As this app uses the same Keycloak client configuration you can just use the sam

We will use [Keycloak](https://keycloak.org) as identity provider.
Please again make sure you have set up and running
keycloak as described in [Setup Keycloak](../setup_keycloak/README.md)
keycloak as described in [Setup Keycloak](../setup/README.md)

<hr>

Expand Down
39 changes: 25 additions & 14 deletions lab1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,19 +451,25 @@ If you have a look inside the _com.example.library.server.business.UserService_
you will notice that the corresponding method has the following authorization check:

```java
@PreAuthorize("hasRole('LIBRARY_ADMIN')")
public List<User> findAll() {
return userRepository.findAll();
public class UserService {
//...
@PreAuthorize("hasRole('LIBRARY_ADMIN')")
public List<User> findAll() {
return userRepository.findAll();
}
}
```

The required authority _ROLE_LIBRARY_ADMIN_ does not match the mapped authority _SCOPE_library_admin_.
To solve this we would have to add the _SCOPE_xxx_ authorities to the existing ones like this:

```java
@PreAuthorize("hasRole('LIBRARY_ADMIN') || hasAuthority('SCOPE_library_admin')")
public List<User> findAll() {
return userRepository.findAll();
public class UserService {
//...
@PreAuthorize("hasRole('LIBRARY_ADMIN') || hasAuthority('SCOPE_library_admin')")
public List<User> findAll() {
return userRepository.findAll();
}
}
```

Expand Down Expand Up @@ -506,7 +512,7 @@ In general, you have two choices here:

In this workshop we will use the first approach and...

* ...read the authorization data from the _groups_ claim inside the JWT token
* ...read the authorization data from the _scope_ claim inside the JWT token
* ...map to our local _LibraryUser_ by reusing the _LibraryUserDetailsService_ to search
for a user having the same email as the _email_ claim inside the JWT token

Expand All @@ -533,7 +539,7 @@ import java.util.stream.Collectors;
@SuppressWarnings("unused")
public class LibraryUserJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String GROUPS_CLAIM = "groups";
private static final String SCOPE_CLAIM = "scope";
private static final String ROLE_PREFIX = "ROLE_";

private final LibraryUserDetailsService libraryUserDetailsService;
Expand All @@ -560,12 +566,17 @@ public class LibraryUserJwtAuthenticationConverter
}

@SuppressWarnings("unchecked")
private Collection<String> getGroups(Jwt jwt) {
Object groups = jwt.getClaims().get(GROUPS_CLAIM);
if (groups instanceof Collection) {
return (Collection<String>) groups;
private Collection<String> getScopes(Jwt jwt) {
Object scopes = jwt.getClaims().get(SCOPE_CLAIM);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
return Arrays.asList(((String) scopes).split(" "));
}
return Collections.emptyList();
}
if (scopes instanceof Collection) {
return (Collection<String>) scopes;
}

return Collections.emptyList();
}
}
Expand Down Expand Up @@ -672,7 +683,7 @@ Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 cl
</blockquote>

Despite the fact that the _audience_ claim is not specified or mandatory for access tokens
specifying and validating the _audience_ claim of access tokens is strongly recommended to avoid misusing access tokens for other resource servers.
specifying and validating the _audience_ claim of access tokens is strongly recommended avoiding misusing access tokens for other resource servers.
There is also a new [draft specification](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt)
on the way to provide a standardized and interoperable profile as an alternative to the proprietary JWT access token layouts.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
}

private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getGroups(jwt).stream()
return this.getScopes(jwt).stream()
.map(authority -> ROLE_PREFIX + authority.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

@SuppressWarnings("unchecked")
private Collection<String> getGroups(Jwt jwt) {
private Collection<String> getScopes(Jwt jwt) {
Object scopes = jwt.getClaims().get(SCOPE_CLAIM);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
Expand Down
77 changes: 41 additions & 36 deletions lab4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.jwt.Jwt;

import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
Expand All @@ -84,44 +86,47 @@ import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class LibraryUserJwtAuthenticationConverterTest {

@Mock
private LibraryUserDetailsService libraryUserDetailsService;
@Mock
private LibraryUserDetailsService libraryUserDetailsService;

@Test
void convertWithSuccess() {
Jwt jwt = Jwt.withTokenValue("1234")
.header("typ", "JWT")
.claim("sub", "userid")
.claim("groups", Collections.singletonList("library_user"))
.build();

given(libraryUserDetailsService.loadUserByUsername(any()))
.willReturn(new LibraryUser(UserBuilder.user().build()));

LibraryUserJwtAuthenticationConverter cut = new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService);
AbstractAuthenticationToken authenticationToken = cut.convert(jwt);
assertThat(authenticationToken).isNotNull();
assertThat(authenticationToken.getAuthorities()).isNotEmpty();
assertThat(authenticationToken.getAuthorities()
.iterator().next().getAuthority()).isEqualTo("ROLE_LIBRARY_USER");
}

@Test
void convertWithFailure() {
Jwt jwt = Jwt.withTokenValue("1234")
.header("typ", "JWT")
.claim("sub", "userid")
.claim("groups", Collections.singletonList("library_user"))
.build();

given(libraryUserDetailsService.loadUserByUsername(any()))
.willThrow(new UsernameNotFoundException("No user found"));
@Test
void convertWithSuccess() {
Jwt jwt = Jwt.withTokenValue("1234")
.header("typ", "JWT")
.claim("sub", "userid")
.claim("groups", Collections.singletonList("library_user"))
.claim("scope", "library_user openid profile")
.build();

given(libraryUserDetailsService.loadUserByUsername(any()))
.willReturn(new LibraryUser(UserBuilder.user().build()));

LibraryUserJwtAuthenticationConverter cut = new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService);
AbstractAuthenticationToken authenticationToken = cut.convert(jwt);
assertThat(authenticationToken).isNotNull();
assertThat(authenticationToken.getAuthorities()).isNotEmpty();
assertThat(authenticationToken.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()))
.containsAnyOf("ROLE_LIBRARY_USER");
}

LibraryUserJwtAuthenticationConverter cut = new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService);
assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(
() -> cut.convert(jwt)
);
}
@Test
void convertWithFailure() {
Jwt jwt = Jwt.withTokenValue("1234")
.header("typ", "JWT")
.claim("sub", "userid")
.claim("groups", Collections.singletonList("library_user"))
.claim("scope", "library_user openid profile")
.build();

given(libraryUserDetailsService.loadUserByUsername(any()))
.willThrow(new UsernameNotFoundException("No user found"));

LibraryUserJwtAuthenticationConverter cut = new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService);
assertThatExceptionOfType(UsernameNotFoundException.class).isThrownBy(
() -> cut.convert(jwt)
);
}
}
```
_LibraryUserJwtAuthenticationConverterTest_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
Expand All @@ -17,7 +19,7 @@
@SuppressWarnings("unused")
public class LibraryUserJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String GROUPS_CLAIM = "groups";
private static final String SCOPE_CLAIM = "scope";
private static final String ROLE_PREFIX = "ROLE_";

private final LibraryUserDetailsService libraryUserDetailsService;
Expand All @@ -37,19 +39,24 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
}

private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getGroups(jwt).stream()
return this.getScopes(jwt).stream()
.map(authority -> ROLE_PREFIX + authority.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

@SuppressWarnings("unchecked")
private Collection<String> getGroups(Jwt jwt) {
Object groups = jwt.getClaims().get(GROUPS_CLAIM);
if (groups instanceof Collection) {
return (Collection<String>) groups;
private Collection<String> getScopes(Jwt jwt) {
Object scopes = jwt.getClaims().get(SCOPE_CLAIM);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
return Arrays.asList(((String) scopes).split(" "));
}
return Collections.emptyList();
}
if (scopes instanceof Collection) {
return (Collection<String>) scopes;
}

return Collections.emptyList();
}
}
Loading

0 comments on commit 0f44024

Please sign in to comment.