diff --git a/.gitignore b/.gitignore index 9e08ff3..9cbfa41 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties +*.classpath +*.project +*.settings/ \ No newline at end of file diff --git a/README.md b/README.md index 914fd6f..f5c04ae 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ implementation 'com.auth0:auth0-spring-security-api:1.2.6' ## Usage -Inside a `WebSecurityConfigurerAdapter` you can configure your API to only accept `RS256` signed JWTs +Inside a `WebSecurityConfigurerAdapter` you can configure your API to only accept `RS256` signed JWTs: ```java @EnableWebSecurity @@ -46,7 +46,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } ``` -or for `HS256` signed JWTs +or for `HS256` signed JWTs: ```java @EnableWebSecurity @@ -65,20 +65,29 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { > If you need further customization (like a leeway for JWT verification) use the `JwtWebSecurityConfigurer` signatures which accept a `JwtAuthenticationProvider`. -Then using Spring Security `HttpSecurity` you can specify which paths requires authentication +Then using Spring Security `HttpSecurity` you can specify which paths requires authentication: ```java http.authorizeRequests() .antMatchers("/api/**").fullyAuthenticated(); ``` -and you can even specify that the JWT should have a single or several scopes +To restrict access based on the presence of a specific scope or permission claim, you can use the `hasAuthority` method. +Scope and permissions claim values are prefixed with `SCOPE_` and `PERMISSION_`, respectively. + +To require a specific scope (`read:users` in the example below): ```java http.authorizeRequests() - .antMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("read:users"); + .antMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("SCOPE_read:users"); ``` +To require a specific permission (`admin` in the example below): + +```java + http.authorizeRequests() + .antMatchers(HttpMethod.GET, "/api/admin/**").hasAuthority("PERMISSION_admin"); +``` `JwtWebSecurityConfigurer#configure(HttpSecurity)` also returns `HttpSecurity` so you can do the following: @@ -93,7 +102,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { .forRS256("YOUR_API_AUDIENCE", "YOUR_API_ISSUER") .configure(http) .authorizeRequests() - .antMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("read:users"); + .antMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("SCOPE_read:users") + .antMatchers(HttpMethod.GET, "/api/admin/**").hasAuthority("PERMISSION_admin"); } } ``` diff --git a/lib/src/main/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebToken.java b/lib/src/main/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebToken.java index d64c91d..6c04a5c 100644 --- a/lib/src/main/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebToken.java +++ b/lib/src/main/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebToken.java @@ -8,12 +8,13 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; public class AuthenticationJsonWebToken implements Authentication, JwtAuthentication { + private final static String SCOPE_AUTHORITY_PREFIX = "SCOPE_"; + private final static String PERMISSION_AUTHORITY_PREFIX = "PERMISSION_"; + private final DecodedJWT decoded; private boolean authenticated; @@ -39,15 +40,13 @@ public Authentication verify(JWTVerifier verifier) throws JWTVerificationExcepti @Override public Collection getAuthorities() { - String scope = decoded.getClaim("scope").asString(); - if (scope == null || scope.trim().isEmpty()) { - return new ArrayList<>(); - } - final String[] scopes = scope.split(" "); - List authorities = new ArrayList<>(scopes.length); - for (String value : scopes) { - authorities.add(new SimpleGrantedAuthority(value)); - } + List scopes = getScopeAuthorities(); + List permissions = getPermissionAuthorities(); + + List authorities = new ArrayList<>(); + authorities.addAll(scopes); + authorities.addAll(permissions); + return authorities; } @@ -83,4 +82,34 @@ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentExce public String getName() { return decoded.getSubject(); } + + private List getScopeAuthorities() { + String scope = decoded.getClaim("scope").asString(); + if (scope == null || scope.trim().isEmpty()) { + return Collections.emptyList(); + } + final String[] scopes = scope.split(" "); + List authorities = new ArrayList<>(scopes.length * 2); + for (String value : scopes) { + // For backwards-compatibility, create authority without scope prefix + authorities.add(new SimpleGrantedAuthority(value)); + authorities.add(new SimpleGrantedAuthority(SCOPE_AUTHORITY_PREFIX + value)); + } + return authorities; + } + + private List getPermissionAuthorities() { + String[] permissions = decoded.getClaim("permissions").asArray(String.class); + + if (permissions == null || permissions.length == 0) { + return Collections.emptyList(); + } + + List authorities = new ArrayList<>(permissions.length); + for (String value : permissions) { + authorities.add(new SimpleGrantedAuthority(PERMISSION_AUTHORITY_PREFIX + value)); + } + + return authorities; + } } diff --git a/lib/src/test/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebTokenTest.java b/lib/src/test/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebTokenTest.java index be6d4af..b6ab9c2 100644 --- a/lib/src/test/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebTokenTest.java +++ b/lib/src/test/java/com/auth0/spring/security/api/authentication/AuthenticationJsonWebTokenTest.java @@ -4,7 +4,6 @@ import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.spring.security.api.authentication.AuthenticationJsonWebToken; import org.hamcrest.Matchers; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.collection.IsEmptyCollection; @@ -15,6 +14,7 @@ import org.springframework.security.core.GrantedAuthority; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -206,15 +206,49 @@ public void shouldGetScopeAsAuthorities() throws Exception { .withClaim("scope", "auth0 auth10") .sign(hmacAlgorithm); + AuthenticationJsonWebToken auth = new AuthenticationJsonWebToken(token, verifier); + assertThat(auth, is(notNullValue())); + + Collection authorities = auth.getAuthorities(); + assertThat(authorities, is(notNullValue())); + assertThat(authorities, is(IsCollectionWithSize.hasSize(4))); + assertThat(authorities, containsInAnyOrder( + hasProperty("authority", is("auth0")), + hasProperty("authority", is("auth10")), + hasProperty("authority", is("SCOPE_auth0")), + hasProperty("authority", is("SCOPE_auth10")) + )); + } + + @Test + public void shouldGetEmptyAuthoritiesOnEmptyPermissionsClaim() throws Exception { + String token = JWT.create() + .withArrayClaim("permissions", new String[]{}) + .sign(hmacAlgorithm); + + AuthenticationJsonWebToken auth = new AuthenticationJsonWebToken(token, verifier); + assertThat(auth, is(notNullValue())); + assertThat(auth.getAuthorities(), is(notNullValue())); + assertThat(auth.getAuthorities(), is(IsEmptyCollection.empty())); + } + + @Test + public void shouldGetPermissionsAsAuthorities() throws Exception { + String[] permissionsClaim = {"read:permission", "write:permission"}; + String token = JWT.create() + .withArrayClaim("permissions", permissionsClaim) + .sign(hmacAlgorithm); + AuthenticationJsonWebToken auth = new AuthenticationJsonWebToken(token, verifier); assertThat(auth, is(notNullValue())); assertThat(auth.getAuthorities(), is(notNullValue())); assertThat(auth.getAuthorities(), is(IsCollectionWithSize.hasSize(2))); - ArrayList authorities = new ArrayList<>(auth.getAuthorities()); - assertThat(authorities.get(0), is(notNullValue())); - assertThat(authorities.get(0).getAuthority(), is("auth0")); - assertThat(authorities.get(1), is(notNullValue())); - assertThat(authorities.get(1).getAuthority(), is("auth10")); + Collection authorities = auth.getAuthorities(); + assertThat(authorities, IsCollectionWithSize.hasSize(2)); + assertThat(authorities, containsInAnyOrder( + hasProperty("authority", is("PERMISSION_" + permissionsClaim[0])), + hasProperty("authority", is("PERMISSION_" + permissionsClaim[1])) + )); } } \ No newline at end of file