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; + } + +}