From 81e22da07c45a2dee8eeef6538384885a3e0d5ed Mon Sep 17 00:00:00 2001 From: psevestre Date: Thu, 31 Mar 2022 02:04:02 -0300 Subject: [PATCH] [BAEL-5259] Spring Security - Map Authorities from JWT (#11990) * [BAEL-4849] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Remove extra comments * [BAEL-5259] simple test case * [BAEL-5259] DSL-based rewrite * [BAEL-5259] Code formatting * [BAEL-5259] Test case naming * WIP: Article code * [BAEL-5259] Tests * [BAEL-5259] Tests --- .../spring-security-oidc/pom.xml | 4 + .../SpringOidcJwtAuthoritiesApplication.java | 23 +++++ .../jwtauthorities/config/AccountToken.java | 23 +++++ .../CustomJwtAuthenticationConverter.java | 36 ++++++++ .../config/JwtMappingProperties.java | 49 ++++++++++ ...MappingJwtGrantedAuthoritiesConverter.java | 84 +++++++++++++++++ .../jwtauthorities/config/SecurityConfig.java | 75 +++++++++++++++ .../oidc/jwtauthorities/domain/Account.java | 26 ++++++ .../service/AccountService.java | 14 +++ .../web/controllers/UserRestController.java | 49 ++++++++++ .../src/main/resources/application.yml | 1 - .../resources/jwtauthorities-application.yml | 15 +++ ...tomJwtAuthenticationConverterUnitTest.java | 59 ++++++++++++ ...wtGrantedAuthoritiesConverterUnitTest.java | 91 +++++++++++++++++++ 14 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/SpringOidcJwtAuthoritiesApplication.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/AccountToken.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverter.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/JwtMappingProperties.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverter.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/SecurityConfig.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/domain/Account.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/service/AccountService.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/web/controllers/UserRestController.java create mode 100644 spring-security-modules/spring-security-oidc/src/main/resources/jwtauthorities-application.yml create mode 100644 spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverterUnitTest.java create mode 100644 spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverterUnitTest.java diff --git a/spring-security-modules/spring-security-oidc/pom.xml b/spring-security-modules/spring-security-oidc/pom.xml index 2a413b1d27..70031b7396 100644 --- a/spring-security-modules/spring-security-oidc/pom.xml +++ b/spring-security-modules/spring-security-oidc/pom.xml @@ -24,6 +24,10 @@ org.springframework.boot spring-boot-starter-oauth2-client + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + \ No newline at end of file diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/SpringOidcJwtAuthoritiesApplication.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/SpringOidcJwtAuthoritiesApplication.java new file mode 100644 index 0000000000..4fa58f3578 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/SpringOidcJwtAuthoritiesApplication.java @@ -0,0 +1,23 @@ +/** + * + */ +package com.baeldung.openid.oidc.jwtauthorities; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +import com.baeldung.openid.oidc.discovery.SpringOidcDiscoveryApplication; +import com.baeldung.openid.oidc.utils.YamlLoaderInitializer; + +@SpringBootApplication +public class SpringOidcJwtAuthoritiesApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(SpringOidcJwtAuthoritiesApplication.class); + ApplicationContextInitializer yamlInitializer = new YamlLoaderInitializer("jwtauthorities-application.yml"); + application.addInitializers(yamlInitializer); + application.run(args); + } +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/AccountToken.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/AccountToken.java new file mode 100644 index 0000000000..a48a0889aa --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/AccountToken.java @@ -0,0 +1,23 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import com.baeldung.openid.oidc.jwtauthorities.domain.Account; + +public class AccountToken extends JwtAuthenticationToken { + private static final long serialVersionUID = 1L; + private final Account account; + + public AccountToken(Jwt jwt, Collection authorities, String name, Account account) { + super(jwt, authorities, name); + this.account = account; + } + + public Account getAccount() { + return account; + } +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverter.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverter.java new file mode 100644 index 0000000000..57b9809bc6 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverter.java @@ -0,0 +1,36 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +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.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import com.baeldung.openid.oidc.jwtauthorities.domain.Account; +import com.baeldung.openid.oidc.jwtauthorities.service.AccountService; + +public class CustomJwtAuthenticationConverter implements Converter { + + private final Converter> jwtGrantedAuthoritiesConverter; + private final String principalClaimName; + private AccountService accountService; + + public CustomJwtAuthenticationConverter(AccountService accountService, Converter> jwtGrantedAuthoritiesConverter, String principalClaimName) { + this.accountService = accountService; + this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter; + this.principalClaimName = principalClaimName != null ? principalClaimName : JwtClaimNames.SUB; + } + + @Override + public AbstractAuthenticationToken convert(Jwt source) { + + Collection authorities = jwtGrantedAuthoritiesConverter.convert(source); + String principalClaimValue = source.getClaimAsString(this.principalClaimName); + Account acc = accountService.findAccountByPrincipal(principalClaimValue); + return new AccountToken(source, authorities, principalClaimValue, acc); + } + +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/JwtMappingProperties.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/JwtMappingProperties.java new file mode 100644 index 0000000000..71a7004300 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/JwtMappingProperties.java @@ -0,0 +1,49 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "baeldung.jwt.mapping") +public class JwtMappingProperties { + + private String authoritiesPrefix; + private String authoritiesClaimName; + private String principalClaimName; + private Map scopes; + + public String getAuthoritiesClaimName() { + return authoritiesClaimName; + } + + public void setAuthoritiesClaimName(String authoritiesClaimName) { + this.authoritiesClaimName = authoritiesClaimName; + } + + public String getAuthoritiesPrefix() { + return authoritiesPrefix; + } + + public void setAuthoritiesPrefix(String authoritiesPrefix) { + this.authoritiesPrefix = authoritiesPrefix; + } + + + public String getPrincipalClaimName() { + return principalClaimName; + } + + public void setPrincipalClaimName(String principalClaimName) { + this.principalClaimName = principalClaimName; + } + + public Map getScopes() { + return scopes; + } + + public void setScopes(Map scopes) { + this.scopes = scopes; + } + + +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverter.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverter.java new file mode 100644 index 0000000000..0b47082294 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverter.java @@ -0,0 +1,84 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +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; + +public class MappingJwtGrantedAuthoritiesConverter implements Converter> { + + private static final Collection WELL_KNOWN_AUTHORITIES_CLAIM_NAMES = Arrays.asList("scope", "scp"); + + private final Map scopes; + private String authoritiesClaimName = null; + private String authorityPrefix = "SCOPE_"; + + MappingJwtGrantedAuthoritiesConverter(Map scopes) { + this.scopes = scopes == null ? Collections.emptyMap(): scopes; + } + + public void setAuthoritiesClaimName(String authoritiesClaimName) { + this.authoritiesClaimName = authoritiesClaimName; + } + + public void setAuthorityPrefix(String authorityPrefix) { + this.authorityPrefix = authorityPrefix; + } + + @Override + public Collection convert(Jwt jwt) { + + Collection tokenScopes = parseScopesClaim(jwt); + if ( tokenScopes.isEmpty()) { + return Collections.emptyList(); + } + + return tokenScopes.stream() + .map(s -> scopes.getOrDefault(s, s)) + .map(s -> this.authorityPrefix + s) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toCollection(HashSet::new)); + } + + protected Collection parseScopesClaim(Jwt jwt) { + + String scopeClaim; + + if ( this.authoritiesClaimName == null ) { + scopeClaim = WELL_KNOWN_AUTHORITIES_CLAIM_NAMES.stream() + .filter( claim -> jwt.hasClaim(claim)) + .findFirst() + .orElse(null); + + if ( scopeClaim == null ) { + return Collections.emptyList(); + } + } + else { + scopeClaim = this.authoritiesClaimName; + } + + Object v = jwt.getClaim(scopeClaim); + if ( v == null ) { + return Collections.emptyList(); + } + + if ( v instanceof String) { + return Arrays.asList(v.toString().split(" ")); + } + else if ( v instanceof Collection ) { + return ((Collection)v).stream() + .map( s -> s.toString()) + .collect(Collectors.toCollection(HashSet::new)); + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/SecurityConfig.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/SecurityConfig.java new file mode 100644 index 0000000000..7495831b54 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/config/SecurityConfig.java @@ -0,0 +1,75 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import java.util.Collection; +import java.util.List; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +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.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.StringUtils; + +import com.baeldung.openid.oidc.jwtauthorities.service.AccountService; + +@Configuration +@EnableConfigurationProperties(JwtMappingProperties.class) +@EnableMethodSecurity +public class SecurityConfig { + + private final JwtMappingProperties mappingProps; + private final AccountService accountService; + + public SecurityConfig(JwtMappingProperties mappingProps, AccountService accountService) { + this.mappingProps = mappingProps; + this.accountService = accountService; + } + + @Bean + public String jwtGrantedAuthoritiesPrefix() { + return mappingProps.getAuthoritiesPrefix(); + } + + @Bean + public Converter> jwtGrantedAuthoritiesConverter() { + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(mappingProps.getScopes()); + + if (StringUtils.hasText(mappingProps.getAuthoritiesPrefix())) { + converter.setAuthorityPrefix(mappingProps.getAuthoritiesPrefix() + .trim()); + } + + if (StringUtils.hasText(mappingProps.getAuthoritiesClaimName())) { + converter.setAuthoritiesClaimName(mappingProps.getAuthoritiesClaimName()); + } + return converter; + } + + @Bean + public Converter customJwtAuthenticationConverter(AccountService accountService) { + return new CustomJwtAuthenticationConverter( + accountService, + jwtGrantedAuthoritiesConverter(), + mappingProps.getPrincipalClaimName()); + } + + @Bean + SecurityFilterChain customJwtSecurityChain(HttpSecurity http) throws Exception { + // @formatter:off + return http.oauth2ResourceServer(oauth2 -> { + oauth2.jwt() + .jwtAuthenticationConverter(customJwtAuthenticationConverter(accountService)); + }) + .build(); + // @formatter:on + } + +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/domain/Account.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/domain/Account.java new file mode 100644 index 0000000000..f0227c8628 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/domain/Account.java @@ -0,0 +1,26 @@ +package com.baeldung.openid.oidc.jwtauthorities.domain; + +public class Account { + + private final Long id; + private final String branch; + private final String accountNumber; + + public Account(Long id, String branch, String accountNumber) { + this.id = id; + this.branch = branch; + this.accountNumber = accountNumber; + } + + public Long getId() { + return id; + } + + public String getBranch() { + return branch; + } + + public String getAccountNumber() { + return accountNumber; + } +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/service/AccountService.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/service/AccountService.java new file mode 100644 index 0000000000..e10a26209c --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/service/AccountService.java @@ -0,0 +1,14 @@ +package com.baeldung.openid.oidc.jwtauthorities.service; + +import org.springframework.stereotype.Service; + +import com.baeldung.openid.oidc.jwtauthorities.domain.Account; + +@Service +public class AccountService { + + public Account findAccountByPrincipal(String principal) { + // NOTE: real-world code would typically perform some sort of DB lookup + return new Account(1l, "0001", "101888444-0"); + } +} diff --git a/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/web/controllers/UserRestController.java b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/web/controllers/UserRestController.java new file mode 100644 index 0000000000..a3b4d1d5cd --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/java/com/baeldung/openid/oidc/jwtauthorities/web/controllers/UserRestController.java @@ -0,0 +1,49 @@ +package com.baeldung.openid.oidc.jwtauthorities.web.controllers; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.openid.oidc.jwtauthorities.config.AccountToken; +import com.baeldung.openid.oidc.jwtauthorities.domain.Account; + +@RestController +@RequestMapping("/user") +public class UserRestController { + + @GetMapping("/authorities") + @PreAuthorize("hasAuthority(@jwtGrantedAuthoritiesPrefix + 'profile.read')") + public Map getPrincipalInfo( JwtAuthenticationToken principal) { + + Collection authorities = principal.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + Map info = new HashMap<>(); + info.put("name", principal.getName()); + info.put("authorities", authorities); + info.put("tokenAttributes", principal.getTokenAttributes()); + + if ( principal instanceof AccountToken ) { + info.put( "account", ((AccountToken)principal).getAccount()); + } + + return info; + } + + @GetMapping("/account/{accountNumber}") + @PreAuthorize("authentication.account.accountNumber == #accountNumber") + public Account getAccountById(@PathVariable("accountNumber") String accountNumber, AccountToken authentication) { + return authentication.getAccount(); + } +} diff --git a/spring-security-modules/spring-security-oidc/src/main/resources/application.yml b/spring-security-modules/spring-security-oidc/src/main/resources/application.yml index f303fcecd1..bb23047dfe 100644 --- a/spring-security-modules/spring-security-oidc/src/main/resources/application.yml +++ b/spring-security-modules/spring-security-oidc/src/main/resources/application.yml @@ -4,4 +4,3 @@ server: logging: level: org.springframework.web.client.RestTemplate: DEBUG - \ No newline at end of file diff --git a/spring-security-modules/spring-security-oidc/src/main/resources/jwtauthorities-application.yml b/spring-security-modules/spring-security-oidc/src/main/resources/jwtauthorities-application.yml new file mode 100644 index 0000000000..384b406465 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/main/resources/jwtauthorities-application.yml @@ -0,0 +1,15 @@ +spring: + security: + oauth2: + resourceserver: + jwt: +# issuer-uri: https://sts.windows.net/2e9fde3a-38ec-44f9-8bcd-c184dc1e8033/ + issuer-uri: http://localhost:8083/auth/realms/baeldung +baeldung: + jwt: + mapping: + authorities-prefix: "MY_SCOPE_" + principal-claim-name: preferred_username + scopes: + profile: profile.read + "profile_read": profile.read diff --git a/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverterUnitTest.java b/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverterUnitTest.java new file mode 100644 index 0000000000..09f1efa228 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/CustomJwtAuthenticationConverterUnitTest.java @@ -0,0 +1,59 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.jwt.Jwt; + +import com.baeldung.openid.oidc.jwtauthorities.service.AccountService; + +class CustomJwtAuthenticationConverterUnitTest { + + @Test + void testGivenCustomJwtAuthenticationConverter_whenConvert_thenReturnAccountToken() { + + AccountService accountService = new AccountService(); + MappingJwtGrantedAuthoritiesConverter authoritiesConverter = new MappingJwtGrantedAuthoritiesConverter(new HashMap<>()); + + CustomJwtAuthenticationConverter converter = new CustomJwtAuthenticationConverter( + accountService, authoritiesConverter, null); + + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .subject("user") + .claim("scp", "openid email profile") + .build(); + + Object auth = converter.convert(jwt); + assertTrue(auth instanceof AccountToken, "Authentication must be instance of AccountToken"); + AccountToken token = AccountToken.class.cast(auth); + + assertEquals("user", token.getName()); + } + + @Test + void testGivenCustomPrincipalClaimName_whenConvert_thenReturnAccountToken() { + + AccountService accountService = new AccountService(); + MappingJwtGrantedAuthoritiesConverter authoritiesConverter = new MappingJwtGrantedAuthoritiesConverter(new HashMap<>()); + + CustomJwtAuthenticationConverter converter = new CustomJwtAuthenticationConverter( + accountService, authoritiesConverter, "preferred_username"); + + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .claim("preferred_username", "user") + .claim("scp", "openid email profile") + .build(); + + Object auth = converter.convert(jwt); + assertTrue(auth instanceof AccountToken, "Authentication must be instance of AccountToken"); + AccountToken token = AccountToken.class.cast(auth); + + assertEquals("user", token.getName()); + } + +} diff --git a/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverterUnitTest.java b/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverterUnitTest.java new file mode 100644 index 0000000000..90b943ce53 --- /dev/null +++ b/spring-security-modules/spring-security-oidc/src/test/java/com/baeldung/openid/oidc/jwtauthorities/config/MappingJwtGrantedAuthoritiesConverterUnitTest.java @@ -0,0 +1,91 @@ +package com.baeldung.openid.oidc.jwtauthorities.config; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +class MappingJwtGrantedAuthoritiesConverterUnitTest { + + @Test + void testGivenConverterWithScopeMap_whenConvert_thenResultHasMappedAuthorities() { + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .subject("user") + .claim("scp", "openid email profile") + .build(); + + Map scopeMap = new HashMap<>(); + scopeMap.put("profile", "profile.read"); + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(scopeMap); + Collection result = converter.convert(jwt); + + assertTrue("Result must contain the authoriry 'SCOPE_profile.read'", + result.contains(new SimpleGrantedAuthority("SCOPE_profile.read"))); + } + + @Test + void testGivenConverterWithCustomScopeClaim_whenConvert_thenResultHasAuthorities() { + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .subject("user") + .claim("myscope_claim", "openid email profile") + .build(); + + Map scopeMap = new HashMap<>(); + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(scopeMap); + converter.setAuthoritiesClaimName("myscope_claim"); + Collection result = converter.convert(jwt); + + assertTrue("Result must contain the authoriry 'SCOPE_profile'", + result.contains(new SimpleGrantedAuthority("SCOPE_profile"))); + } + + @Test + void testGivenTokenWithNonMappedScope_whenConvert_thenResultHasOriginalScope() { + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .subject("user") + .claim("scp", "openid email profile custom") + .build(); + + Map scopeMap = new HashMap<>(); + scopeMap.put("profile", "profile.read"); + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(scopeMap); + Collection result = converter.convert(jwt); + + assertTrue("Result must contain the authority SCOPE_custom", + result.contains(new SimpleGrantedAuthority("SCOPE_custom"))); + } + + + @Test + void testGivenConverterWithCustomPrefix_whenConvert_thenAllAuthoritiesMustHaveTheCustomPrefix() { + Jwt jwt = Jwt.withTokenValue("NOTUSED") + .header("typ", "JWT") + .subject("user") + .claim("scp", "openid email profile custom") + .build(); + + Map scopeMap = new HashMap<>(); + scopeMap.put("profile", "profile.read"); + MappingJwtGrantedAuthoritiesConverter converter = new MappingJwtGrantedAuthoritiesConverter(scopeMap); + converter.setAuthorityPrefix("MY_SCOPE"); + Collection result = converter.convert(jwt); + + long count = result.stream() + .map(GrantedAuthority::getAuthority) + .filter(s -> !s.startsWith("MY_SCOPE")) + .count(); + + assertTrue("All authorities names must start with custom prefix", count == 0 ); + } + +}