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/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
new file mode 100644
index 0000000..160bacd
--- /dev/null
+++ b/auth/src/main/java/com/gewia/common/auth/jwt/JwtScopes.java
@@ -0,0 +1,43 @@
+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;
+ }
+
+ public boolean hasScope(String scope) {
+ return this.scopes.contains(scope);
+ }
+
+}
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/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..219c068
--- /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() {
+ this.addAuthenticationInterceptors(interceptors);
+ }
+
+ 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
new file mode 100644
index 0000000..afb427b
--- /dev/null
+++ b/spring-auth/src/main/java/com/gewia/common/spring/auth/SpringAuthenticationWebConfig.java
@@ -0,0 +1,48 @@
+package com.gewia.common.spring.auth;
+
+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;
+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 {
+ 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(Jwt.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;
+ }
+
+}
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();
+
+}