From ed95d4339791a1973bf07b3ef7f60215bef066fa Mon Sep 17 00:00:00 2001 From: PAException <35733278+PAException@users.noreply.github.com> Date: Tue, 19 May 2020 17:45:23 +0200 Subject: [PATCH 1/3] Added spring-auth module (#14) --- pom.xml | 1 + spring-auth/pom.xml | 77 ++++++++++++++++++ .../gewia/common/spring/auth/AuthScope.java | 18 +++++ .../common/spring/auth/Authentication.java | 14 ++++ .../spring/auth/IgnoreServiceToken.java | 12 +++ .../spring/auth/SpringAuthentication.java | 23 ++++++ .../auth/SpringAuthenticationWebConfig.java | 42 ++++++++++ .../auth/interceptor/ScopeInterceptor.java | 78 +++++++++++++++++++ .../interceptor/ServiceTokenInterceptor.java | 36 +++++++++ 9 files changed, 301 insertions(+) create mode 100644 spring-auth/pom.xml create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/AuthScope.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/Authentication.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/IgnoreServiceToken.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ScopeInterceptor.java create mode 100644 spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ServiceTokenInterceptor.java diff --git a/pom.xml b/pom.xml index c4a279a..c8ff076 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ scope auth util + spring-auth diff --git a/spring-auth/pom.xml b/spring-auth/pom.xml new file mode 100644 index 0000000..96526e7 --- /dev/null +++ b/spring-auth/pom.xml @@ -0,0 +1,77 @@ + + + + gewia-common + com.gewia.common + 1.0 + + 4.0.0 + spring-auth + + + + + org.springframework.boot + spring-boot-dependencies + 2.1.10.RELEASE + pom + import + + + + + + + com.gewia.common + auth + ${project.parent.version} + compile + + + com.gewia.common + scope + ${project.parent.version} + compile + + + + com.auth0 + java-jwt + 3.10.3 + compile + + + + javax.validation + validation-api + 2.0.1.Final + compile + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + \ No newline at end of file diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/AuthScope.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/AuthScope.java new file mode 100644 index 0000000..8a1116e --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/AuthScope.java @@ -0,0 +1,18 @@ +package com.gewia.common.spring.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(Authentication.class) +public @interface AuthScope { + + String value() default ""; + + String scope() default ""; + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/Authentication.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/Authentication.java new file mode 100644 index 0000000..cb596e9 --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/Authentication.java @@ -0,0 +1,14 @@ +package com.gewia.common.spring.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Authentication { + + AuthScope[] value(); + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/IgnoreServiceToken.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/IgnoreServiceToken.java new file mode 100644 index 0000000..3216d4c --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/IgnoreServiceToken.java @@ -0,0 +1,12 @@ +package com.gewia.common.spring.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface IgnoreServiceToken { + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java new file mode 100644 index 0000000..594a060 --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java @@ -0,0 +1,23 @@ +package com.gewia.common.spring.auth; + +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +@ComponentScan("com.gewia.common.spring.auth") +public abstract class SpringAuthentication implements InitializingBean { + + @Getter(AccessLevel.PACKAGE) private static List interceptors = new ArrayList<>(); + + @Override + public void afterPropertiesSet() { + interceptors = this.addAuthenticationInterceptors(interceptors); + } + + abstract public List addAuthenticationInterceptors(List authenticationInterceptors); + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java new file mode 100644 index 0000000..4daa459 --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java @@ -0,0 +1,42 @@ +package com.gewia.common.spring.auth; + +import com.auth0.jwt.interfaces.DecodedJWT; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +@Configuration +@EnableWebMvc +public class SpringAuthenticationWebConfig implements WebMvcConfigurer, HandlerMethodArgumentResolver { + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("accessToken"); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(DecodedJWT.class); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(this); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + for (HandlerInterceptorAdapter interceptors : SpringAuthentication.getInterceptors()) + registry.addInterceptor(interceptors).addPathPatterns("/**/*"); + } + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ScopeInterceptor.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ScopeInterceptor.java new file mode 100644 index 0000000..e89c751 --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ScopeInterceptor.java @@ -0,0 +1,78 @@ +package com.gewia.common.spring.auth.interceptor; + +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.gewia.common.auth.jwt.JwtUtil; +import com.gewia.common.spring.auth.AuthScope; +import com.gewia.common.spring.auth.Authentication; +import com.gewia.common.util.Pair; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +@AllArgsConstructor +public class ScopeInterceptor extends HandlerInterceptorAdapter { + + private final JwtUtil jwtUtil; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + HandlerMethod method = (HandlerMethod) handler; + + AuthScope[] authScopes; + Authentication auth = method.getMethodAnnotation(Authentication.class); + AuthScope methodAuthScope = method.getMethodAnnotation(AuthScope.class); + if (auth != null) authScopes = auth.value(); + else { + if (methodAuthScope == null) return true; + authScopes = new AuthScope[]{methodAuthScope}; + } + + + String jwt = request.getHeader("Authorization"); + if (jwt == null || jwt.isBlank()) return false; + + Pair result = this.jwtUtil.verify(jwt); + switch (result.getRight()) { + case EXPIRED: + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + case INVALID: + response.setStatus(HttpStatus.NOT_ACCEPTABLE.value()); + return false; + case FAILED: + response.setStatus(HttpStatus.EXPECTATION_FAILED.value()); + return false; + case UNKNOWN: + response.setStatus(HttpStatus.FORBIDDEN.value()); + return false; + default: + response.setStatus(HttpStatus.OK.value()); + } + + Claim claim = result.getLeft().getClaim("scopes"); + List userScopes = claim.asList(String.class); + for (AuthScope authScope : authScopes) { + String scope = authScope.scope(); + if (scope.isBlank()) scope = authScope.value(); + if (!scope.isBlank()) { + boolean isPresent = false; + for (String userScope : userScopes) + if (userScope.equalsIgnoreCase(scope)) { + isPresent = true; + break; + } + if (!isPresent) return false; + } + } + + request.setAttribute("accessToken", result.getLeft()); + + return true; + } + +} diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ServiceTokenInterceptor.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ServiceTokenInterceptor.java new file mode 100644 index 0000000..42212a4 --- /dev/null +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/interceptor/ServiceTokenInterceptor.java @@ -0,0 +1,36 @@ +package com.gewia.common.spring.auth.interceptor; + +import com.gewia.common.spring.auth.IgnoreServiceToken; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +@AllArgsConstructor +public class ServiceTokenInterceptor extends HandlerInterceptorAdapter { + + private final String serviceToken; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + response.setStatus(HttpStatus.FORBIDDEN.value()); + + HandlerMethod method = (HandlerMethod) handler; + if (method.hasMethodAnnotation(IgnoreServiceToken.class) || + method.getMethod().getDeclaringClass().getAnnotation(IgnoreServiceToken.class) != null) { + response.setStatus(HttpStatus.OK.value()); + return true; + } + + String serviceToken = request.getHeader("X-ServiceToken"); + + if (serviceToken == null) return false; + if (!this.serviceToken.equals(serviceToken)) return false; + + response.setStatus(HttpStatus.OK.value()); + return true; + } + +} From 87e295429c46dcdd5fc0f1b1d112df42a7d401aa Mon Sep 17 00:00:00 2001 From: PAException Date: Mon, 11 May 2020 19:40:21 +0200 Subject: [PATCH 2/3] Created a class to map JWTScopes (#18) --- auth/pom.xml | 5 ++ .../com/gewia/common/auth/jwt/JwtScopes.java | 39 ++++++++++++ .../gewia/common/auth/jwt/JwtScopesTest.java | 60 +++++++++++++++++++ .../java/com/gewia/common/util/Executor.java | 8 +++ 4 files changed, 112 insertions(+) create mode 100644 auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java create mode 100644 auth/src/test/java/com/gewia/common/auth/jwt/JwtScopesTest.java create mode 100644 util/src/main/java/com/gewia/common/util/Executor.java diff --git a/auth/pom.xml b/auth/pom.xml index 087976b..5e66cfb 100644 --- a/auth/pom.xml +++ b/auth/pom.xml @@ -18,6 +18,11 @@ ${project.parent.version} compile + + com.gewia.common + scope + ${project.parent.version} + com.auth0 diff --git a/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java b/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java new file mode 100644 index 0000000..3a5bfcb --- /dev/null +++ b/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java @@ -0,0 +1,39 @@ +package com.gewia.common.auth.jwt; + +import com.gewia.common.util.Executor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtScopes { + + @Getter private List scopes = new ArrayList<>(); + private boolean containing = false; + + public JwtScopes(List scopes) { + this.scopes = Collections.unmodifiableList(scopes); + } + + public JwtScopes includes(String scope, Executor executor) { + if (this.scopes.contains(scope)) { + containing = true; + if (executor != null) executor.action(); + } else containing = false; + + return this; + } + + public void orElse(Executor executor) { + if (!containing && executor != null) executor.action(); + containing = false; + } + + public boolean getResult() { + return containing; + } + +} diff --git a/auth/src/test/java/com/gewia/common/auth/jwt/JwtScopesTest.java b/auth/src/test/java/com/gewia/common/auth/jwt/JwtScopesTest.java new file mode 100644 index 0000000..c0c2c44 --- /dev/null +++ b/auth/src/test/java/com/gewia/common/auth/jwt/JwtScopesTest.java @@ -0,0 +1,60 @@ +package com.gewia.common.auth.jwt; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Assert; +import org.junit.Test; + +public class JwtScopesTest { + + @Test + public void testScopesUnmodifiable() { + JwtScopes jwtScopes = this.getExampleJwtScopes(); + + Assert.assertThrows(UnsupportedOperationException.class, () -> jwtScopes.getScopes().add("another.example.scope")); + } + + @Test + public void testResultOperation() { + JwtScopes jwtScopes = this.getExampleJwtScopes(); + + Assert.assertTrue(jwtScopes.includes("microservice.topic.mode.limitation.extra", null).getResult()); + Assert.assertFalse(jwtScopes.includes("another.exmaple.scope", null).getResult()); + } + + @Test + public void testOrElseOperation() { + JwtScopes jwtScopes = this.getExampleJwtScopes(); + + AtomicInteger i = new AtomicInteger(); + jwtScopes.includes("another.example.scope", i::getAndIncrement).orElse(i::getAndDecrement); + Assert.assertEquals(-1, i.get()); + } + + @Test + public void testIncludeOperation() { + JwtScopes jwtScopes = this.getExampleJwtScopes(); + + AtomicInteger i = new AtomicInteger(); + jwtScopes.includes("microservice.topic.mode.limitation.extra", i::getAndIncrement).orElse(i::getAndDecrement); + Assert.assertEquals(1, i.get()); + } + + @Test + public void testInitializing() { + List exampleScopes = new ArrayList<>(); + exampleScopes.add("microservice.topic.mode.limitation.extra"); + JwtScopes jwtScopes = new JwtScopes(exampleScopes); + + Assert.assertEquals(exampleScopes, jwtScopes.getScopes()); + } + + private JwtScopes getExampleJwtScopes() { + List exampleScopes = new ArrayList<>(); + exampleScopes.add("microservice.topic.mode.limitation.extra"); + + return new JwtScopes(exampleScopes); + } + +} diff --git a/util/src/main/java/com/gewia/common/util/Executor.java b/util/src/main/java/com/gewia/common/util/Executor.java new file mode 100644 index 0000000..06ad4e2 --- /dev/null +++ b/util/src/main/java/com/gewia/common/util/Executor.java @@ -0,0 +1,8 @@ +package com.gewia.common.util; + +@FunctionalInterface +public interface Executor { + + void action(); + +} From 84f23317c9ec1ca4a67c33e3133e0b9384969be8 Mon Sep 17 00:00:00 2001 From: PAException <35733278+PAException@users.noreply.github.com> Date: Sat, 23 May 2020 15:54:21 +0200 Subject: [PATCH 3/3] Added Jwt as parameter of methods --- .../main/java/com/gewia/common/auth/jwt/Jwt.java | 14 ++++++++++++++ .../java/com/gewia/common/auth/jwt/JwtScopes.java | 4 ++++ .../common/spring/auth/SpringAuthentication.java | 4 ++-- .../spring/auth/SpringAuthenticationWebConfig.java | 12 +++++++++--- 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 auth/src/main/java/com/gewia/common/auth/jwt/Jwt.java diff --git a/auth/src/main/java/com/gewia/common/auth/jwt/Jwt.java b/auth/src/main/java/com/gewia/common/auth/jwt/Jwt.java new file mode 100644 index 0000000..fd4f8f3 --- /dev/null +++ b/auth/src/main/java/com/gewia/common/auth/jwt/Jwt.java @@ -0,0 +1,14 @@ +package com.gewia.common.auth.jwt; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import java.util.UUID; + +@Getter +@AllArgsConstructor +public class Jwt { + + private final UUID userId; + private final JwtScopes userScopes; + +} diff --git a/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java b/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java index 3a5bfcb..160bacd 100644 --- a/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java +++ b/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java @@ -36,4 +36,8 @@ public boolean getResult() { return containing; } + public boolean hasScope(String scope) { + return this.scopes.contains(scope); + } + } diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java index 594a060..219c068 100644 --- a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthentication.java @@ -15,9 +15,9 @@ public abstract class SpringAuthentication implements InitializingBean { @Override public void afterPropertiesSet() { - interceptors = this.addAuthenticationInterceptors(interceptors); + this.addAuthenticationInterceptors(interceptors); } - abstract public List addAuthenticationInterceptors(List authenticationInterceptors); + abstract public void addAuthenticationInterceptors(List authenticationInterceptors); } diff --git a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java index 4daa459..afb427b 100644 --- a/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java +++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java @@ -1,8 +1,11 @@ package com.gewia.common.spring.auth; -import com.auth0.jwt.interfaces.DecodedJWT; import java.util.List; +import java.util.UUID; import javax.servlet.http.HttpServletRequest; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.gewia.common.auth.jwt.Jwt; +import com.gewia.common.auth.jwt.JwtScopes; import org.springframework.context.annotation.Configuration; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -20,12 +23,15 @@ public class SpringAuthenticationWebConfig implements WebMvcConfigurer, HandlerM @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - return ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("accessToken"); + DecodedJWT decodedJWT = (DecodedJWT) ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("accessToken"); + + return new Jwt(UUID.fromString(decodedJWT.getClaim("userId").asString()), + new JwtScopes(decodedJWT.getClaim("scopes").asList(String.class))); } @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(DecodedJWT.class); + return parameter.getParameterType().equals(Jwt.class); } @Override