├── .gitignore ├── src └── main │ ├── resources │ ├── authorization.yml │ └── application.yml │ └── java │ └── com │ └── github │ └── abhinavrohatgi30 │ ├── models │ ├── UserInfoDTO.java │ ├── AuthorizationPolicy.java │ ├── AuthorizationPolicyClaim.java │ ├── UrlPatternTree.java │ └── UrlPatternTreeNode.java │ ├── constants │ └── FilterConstants.java │ ├── CentralizedAuthGatewayApplication.java │ ├── controller │ ├── TestEndpointV1Controller.java │ ├── TestEndpointV0Controller.java │ └── JWTController.java │ ├── config │ └── AuthorizationConfig.java │ ├── utils │ ├── YamlPropertySourceFactory.java │ └── UrlPatternTreeNodeUtils.java │ ├── filter │ ├── ZuulExceptionResponseMappingFilter.java │ ├── AuthorizationFilter.java │ └── AuthenticationFilter.java │ └── service │ └── JWTService.java ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings/ 4 | bin/ 5 | target/ 6 | -------------------------------------------------------------------------------- /src/main/resources/authorization.yml: -------------------------------------------------------------------------------- 1 | authorization: 2 | policies: 3 | - route: /test/endpoint/v0 4 | claims: 5 | - urlPattern: /test/[0-9]* 6 | urlClaim: /test/:userId 7 | - urlPattern: /test/[0-9]*/ping 8 | urlClaim: /test/:userId/ping 9 | userRoleClaim: admin 10 | - route: /test/endpoint/v1 11 | claims: 12 | - urlPattern: /test/[0-9]* 13 | urlClaim: /test/:userId 14 | - urlPattern: /test/[0-9]*/pong 15 | urlClaim: /test/:userId/pong -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/models/UserInfoDTO.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.models; 2 | 3 | public class UserInfoDTO { 4 | 5 | private Long userId; 6 | private String userRole; 7 | private String userType; 8 | 9 | public UserInfoDTO(Long userId,String userType,String userRole) { 10 | this.userId = userId; 11 | this.userRole = userRole; 12 | this.userType = userType; 13 | } 14 | 15 | public Long getUserId() { 16 | return userId; 17 | } 18 | 19 | public String getUserRole() { 20 | return userRole; 21 | } 22 | 23 | public String getUserType() { 24 | return userType; 25 | } 26 | 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/models/AuthorizationPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.models; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class AuthorizationPolicy { 7 | 8 | private String route; 9 | private List claims = new ArrayList<>(); 10 | 11 | public String getRoute() { 12 | return route; 13 | } 14 | public void setRoute(String route) { 15 | this.route = route; 16 | } 17 | public List getClaims() { 18 | return claims; 19 | } 20 | public void setClaims(List claims) { 21 | this.claims = claims; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/constants/FilterConstants.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.constants; 2 | 3 | public final class FilterConstants { 4 | 5 | public static final String USER_ID_CLAIM = "userId"; 6 | public static final String USER_TYPE_CLAIM = "userType"; 7 | public static final String USER_ROLE_CLAIM = "userRole"; 8 | public static final String AGENT_ID_CLAIM = "agentId"; 9 | public static final String IS_CLAIM_VERIFICATION_REQUIRED = "isClaimVerificationRequired"; 10 | public static final String IS_SESSION_UPDATE_REQUIRED = "isSessionUpdateRequired"; 11 | public static final String IS_ERROR_CODE_AVAILABLE = "error.status_code"; 12 | public static final String IS_EXCEPTION_AVAILABLE = "error.exception"; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: centralized-auth-gateway 4 | mvc: 5 | dispatch-options-request: true 6 | 7 | server: 8 | port: 8082 9 | 10 | ribbon: 11 | eureka: 12 | enabled: false 13 | 14 | zuul: 15 | ignored-headers: Access-Control-Allow-Credentials, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Expose-Headers, Access-Control-Allow-Methods 16 | ignore-security-headers: true 17 | routes: 18 | recruiter-service: 19 | path: /test/endpoint/v0/** 20 | serviceId: forward:/test/endpoint/v0/ 21 | agent-service: 22 | path: /test/endpoint/v1/** 23 | serviceId: forward:/test/endpoint/v1/ 24 | 25 | eureka: 26 | client: 27 | enabled: false -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/CentralizedAuthGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 8 | 9 | @SpringBootApplication 10 | @EnableZuulProxy 11 | @EnableConfigurationProperties 12 | @EnableAutoConfiguration 13 | public class CentralizedAuthGatewayApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(CentralizedAuthGatewayApplication.class); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/models/AuthorizationPolicyClaim.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.models; 2 | 3 | import java.util.List; 4 | 5 | public class AuthorizationPolicyClaim { 6 | 7 | private String urlPattern; 8 | private String urlClaim; 9 | private List userRoleClaim; 10 | 11 | public String getUrlPattern() { 12 | return urlPattern; 13 | } 14 | 15 | public void setUrlPattern(String urlPattern) { 16 | this.urlPattern = urlPattern; 17 | } 18 | 19 | public String getUrlClaim() { 20 | return urlClaim; 21 | } 22 | 23 | public void setUrlClaim(String urlClaim) { 24 | this.urlClaim = urlClaim; 25 | } 26 | 27 | public List getUserRoleClaim() { 28 | return userRoleClaim; 29 | } 30 | 31 | public void setUserRoleClaim(List userRoleClaim) { 32 | this.userRoleClaim = userRoleClaim; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/controller/TestEndpointV1Controller.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.controller; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/test/endpoint/v1") 11 | public class TestEndpointV1Controller { 12 | 13 | @GetMapping("/test/{id}") 14 | public ResponseEntity getDetails(@PathVariable("id") String id){ 15 | return ResponseEntity.ok().build(); 16 | } 17 | 18 | @GetMapping("/test/{id}/pong") 19 | public ResponseEntity pong(@PathVariable("id") String id){ 20 | return ResponseEntity.ok(id); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/controller/TestEndpointV0Controller.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.controller; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/test/endpoint/v0") 11 | public class TestEndpointV0Controller { 12 | 13 | @GetMapping("/test/{id}") 14 | public ResponseEntity getDetails(@PathVariable("id") String id){ 15 | return ResponseEntity.ok().build(); 16 | } 17 | 18 | @GetMapping("/test/{id}/ping") 19 | public ResponseEntity ping(@PathVariable("id") String id){ 20 | return ResponseEntity.ok(id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/config/AuthorizationConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.config; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.PropertySource; 8 | 9 | import com.github.abhinavrohatgi30.models.AuthorizationPolicy; 10 | import com.github.abhinavrohatgi30.utils.YamlPropertySourceFactory; 11 | 12 | @Configuration 13 | @PropertySource(factory = YamlPropertySourceFactory.class,value="classpath:authorization.yml") 14 | @ConfigurationProperties(prefix="authorization") 15 | public class AuthorizationConfig { 16 | 17 | private List policies; 18 | 19 | public List getPolicies() { 20 | return policies; 21 | } 22 | 23 | public void setPolicies(List policies) { 24 | this.policies = policies; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/controller/JWTController.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import com.github.abhinavrohatgi30.models.UserInfoDTO; 11 | import com.github.abhinavrohatgi30.service.JWTService; 12 | 13 | @RestController 14 | @RequestMapping("/token/generate") 15 | public class JWTController { 16 | 17 | @Autowired 18 | private JWTService jwtService; 19 | 20 | @PostMapping 21 | public ResponseEntity generateToken(@RequestBody UserInfoDTO userInfoDTO){ 22 | return ResponseEntity.ok(jwtService.createToken(userInfoDTO.getUserId(), userInfoDTO.getUserType(), userInfoDTO.getUserRole())); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/models/UrlPatternTree.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.models; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import com.github.abhinavrohatgi30.config.AuthorizationConfig; 9 | import com.github.abhinavrohatgi30.utils.UrlPatternTreeNodeUtils; 10 | 11 | @Component 12 | public class UrlPatternTree { 13 | 14 | private UrlPatternTreeNode rootNode; 15 | 16 | @Autowired 17 | public UrlPatternTree(AuthorizationConfig authorizationConfig) { 18 | rootNode = new UrlPatternTreeNode(null, "root"); 19 | List claimRoutes = authorizationConfig.getPolicies(); 20 | for(AuthorizationPolicy claimRoute : claimRoutes) { 21 | String prefix = claimRoute.getRoute(); 22 | UrlPatternTreeNode routeNode = UrlPatternTreeNodeUtils.addNode(prefix,rootNode); 23 | List claimPatterns = claimRoute.getClaims(); 24 | for(AuthorizationPolicyClaim claimPattern : claimPatterns) { 25 | String urlPattern = claimPattern.getUrlPattern(); 26 | String urlClaim = claimPattern.getUrlClaim(); 27 | List userRoleClaim = claimPattern.getUserRoleClaim(); 28 | UrlPatternTreeNodeUtils.addUrlNode(urlPattern, userRoleClaim, prefix + urlClaim, routeNode); 29 | } 30 | } 31 | } 32 | 33 | 34 | public UrlPatternTreeNode verifyIfRequestIsAuthorized(String requestURI) { 35 | return UrlPatternTreeNodeUtils.getUrlNode(requestURI, rootNode); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/utils/YamlPropertySourceFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.utils; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.util.Properties; 6 | 7 | import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; 8 | import org.springframework.core.env.PropertiesPropertySource; 9 | import org.springframework.core.env.PropertySource; 10 | import org.springframework.core.io.support.EncodedResource; 11 | import org.springframework.core.io.support.PropertySourceFactory; 12 | import org.springframework.lang.Nullable; 13 | 14 | public class YamlPropertySourceFactory implements PropertySourceFactory { 15 | 16 | @Override 17 | public PropertySource createPropertySource(@Nullable String name, EncodedResource resource) throws IOException { 18 | Properties propertiesFromYaml = loadYamlIntoProperties(resource); 19 | String sourceName = name != null ? name : resource.getResource().getFilename(); 20 | return new PropertiesPropertySource(sourceName, propertiesFromYaml); 21 | } 22 | 23 | private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException { 24 | try { 25 | YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); 26 | factory.setResources(resource.getResource()); 27 | factory.afterPropertiesSet(); 28 | return factory.getObject(); 29 | } catch (IllegalStateException e) { 30 | // for ignoreResourceNotFound 31 | Throwable cause = e.getCause(); 32 | if (cause instanceof FileNotFoundException) 33 | throw (FileNotFoundException) e.getCause(); 34 | throw e; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/filter/ZuulExceptionResponseMappingFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.filter; 2 | 3 | import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.util.ReflectionUtils; 7 | import static com.github.abhinavrohatgi30.constants.FilterConstants.*; 8 | 9 | import com.netflix.zuul.ZuulFilter; 10 | import com.netflix.zuul.context.RequestContext; 11 | import com.netflix.zuul.exception.ZuulException; 12 | 13 | @Component 14 | public class ZuulExceptionResponseMappingFilter extends ZuulFilter{ 15 | 16 | @Override 17 | public String filterType() { 18 | return FilterConstants.POST_TYPE; 19 | } 20 | 21 | @Override 22 | public int filterOrder() { 23 | // Needs to run before SendErrorFilter which has filterOrder == 0 24 | return -1; 25 | } 26 | 27 | @Override 28 | public boolean shouldFilter() { 29 | // only forward to errorPath if it hasn't been forwarded to already 30 | return RequestContext.getCurrentContext().containsKey(IS_ERROR_CODE_AVAILABLE); 31 | } 32 | 33 | @Override 34 | public Object run() { 35 | try { 36 | RequestContext ctx = RequestContext.getCurrentContext(); 37 | Object e = ctx.get(IS_EXCEPTION_AVAILABLE); 38 | 39 | if (e instanceof ZuulException) { 40 | ZuulException zuulException = (ZuulException)e; 41 | 42 | // Remove error code to prevent further error handling in follow up filters 43 | ctx.remove(IS_ERROR_CODE_AVAILABLE); 44 | 45 | // Populate context with new response values 46 | ctx.setResponseBody(zuulException.errorCause); 47 | ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); 48 | ctx.setResponseStatusCode(zuulException.nStatusCode); 49 | } 50 | } 51 | catch (Exception ex) { 52 | ReflectionUtils.rethrowRuntimeException(ex); 53 | } 54 | return null; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/service/JWTService.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.service; 2 | 3 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_ID_CLAIM; 4 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_ROLE_CLAIM; 5 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_TYPE_CLAIM; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.util.Calendar; 9 | import java.util.Date; 10 | import java.util.TimeZone; 11 | 12 | import org.springframework.stereotype.Service; 13 | 14 | import com.auth0.jwt.JWT; 15 | import com.auth0.jwt.JWTCreator; 16 | import com.auth0.jwt.JWTVerifier; 17 | import com.auth0.jwt.algorithms.Algorithm; 18 | import com.auth0.jwt.exceptions.JWTVerificationException; 19 | import com.auth0.jwt.interfaces.DecodedJWT; 20 | 21 | @Service 22 | public class JWTService { 23 | 24 | private static final String ISSUER_NAME = "Abhinav Rohatgi"; 25 | private Algorithm algorithm; 26 | 27 | public JWTService() throws IllegalArgumentException, UnsupportedEncodingException { 28 | algorithm = Algorithm.HMAC512("test token"); 29 | } 30 | 31 | 32 | public DecodedJWT verifyToken(String token) { 33 | final JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUER_NAME).build(); 34 | try { 35 | return jwtVerifier.verify(token); 36 | } catch (JWTVerificationException exception) { 37 | exception.printStackTrace(); 38 | } 39 | return null; 40 | } 41 | 42 | public String createToken(Long userId, String userType, String userRole) { 43 | final JWTCreator.Builder jwtBuilder = JWT.create().withIssuer(ISSUER_NAME) 44 | .withSubject(userId.toString()) 45 | .withClaim(USER_ID_CLAIM, userId) 46 | .withIssuedAt(new Date()) 47 | .withClaim(USER_TYPE_CLAIM, userType) 48 | .withClaim(USER_ROLE_CLAIM, userRole); 49 | 50 | Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 51 | calendar.add(Calendar.SECOND, 120); 52 | jwtBuilder.withExpiresAt(calendar.getTime()); 53 | jwtBuilder.withClaim("tokenType", "accessToken"); 54 | return jwtBuilder.sign(algorithm); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/models/UrlPatternTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.models; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class UrlPatternTreeNode { 9 | 10 | private UrlPatternTreeNode parent; 11 | private String key; 12 | private Map paths = new HashMap<>(); 13 | private Map patterns = new HashMap<>(); 14 | private boolean isUrlNode = false; 15 | private String urlClaimPattern; 16 | private List userRoleClaim = new ArrayList<>(); 17 | 18 | public UrlPatternTreeNode(UrlPatternTreeNode parent, String key) { 19 | this.parent = parent; 20 | this.key = key; 21 | } 22 | 23 | public UrlPatternTreeNode(UrlPatternTreeNode parent, String key, Map paths, Map patterns, boolean isLeafNode, String urlClaimPattern, List userRoleClaim) { 24 | this(parent, key); 25 | this.paths = paths; 26 | this.patterns = patterns; 27 | this.isUrlNode = isLeafNode; 28 | this.urlClaimPattern = urlClaimPattern; 29 | this.userRoleClaim = userRoleClaim; 30 | } 31 | 32 | 33 | public UrlPatternTreeNode getParent() { 34 | return parent; 35 | } 36 | 37 | public void setParent(UrlPatternTreeNode parent) { 38 | this.parent = parent; 39 | } 40 | 41 | public Map getPaths() { 42 | return paths; 43 | } 44 | 45 | public void setPaths(Map paths) { 46 | this.paths = paths; 47 | } 48 | 49 | 50 | public boolean isUrlNode() { 51 | return isUrlNode; 52 | } 53 | 54 | public void setUrlNode(boolean isUrlNode) { 55 | this.isUrlNode = isUrlNode; 56 | } 57 | 58 | public String getUrlClaimPattern() { 59 | return urlClaimPattern; 60 | } 61 | 62 | public void setUrlClaimPattern(String urlClaimPattern) { 63 | this.urlClaimPattern = urlClaimPattern; 64 | } 65 | 66 | 67 | public Map getPatterns() { 68 | return patterns; 69 | } 70 | 71 | public void setPatterns(Map patterns) { 72 | this.patterns = patterns; 73 | } 74 | 75 | public List getUserRoleClaim() { 76 | return userRoleClaim; 77 | } 78 | 79 | public void setUserRoleClaim(List userRoleClaim) { 80 | this.userRoleClaim = userRoleClaim; 81 | } 82 | 83 | public String getKey() { 84 | return key; 85 | } 86 | 87 | public void setKey(String key) { 88 | this.key = key; 89 | } 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | 9 | org.springframework.boot 10 | spring-boot-starter-parent 11 | 2.1.4.RELEASE 12 | 13 | 14 | 15 | com.github.abhinavrohatgi30 16 | custom-auth-gateway 17 | 0.0.1-SNAPSHOT 18 | centralized-auth-gateway 19 | A Centralized Authentication and Authorization API Gateway 20 | 21 | 22 | 8 23 | Greenwich.SR2 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-devtools 30 | runtime 31 | 32 | 33 | org.springframework.cloud 34 | spring-cloud-starter-netflix-ribbon 35 | 36 | 37 | org.springframework.cloud 38 | spring-cloud-starter-netflix-zuul 39 | 40 | 41 | org.json 42 | json 43 | 20180813 44 | 45 | 46 | com.auth0 47 | java-jwt 48 | 3.3.0 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-configuration-processor 58 | true 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-dependencies 67 | ${spring-cloud.version} 68 | pom 69 | import 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/utils/UrlPatternTreeNodeUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.utils; 2 | 3 | import java.util.List; 4 | import java.util.Map.Entry; 5 | import java.util.regex.Pattern; 6 | 7 | import com.github.abhinavrohatgi30.models.UrlPatternTreeNode; 8 | 9 | public class UrlPatternTreeNodeUtils { 10 | 11 | private UrlPatternTreeNodeUtils() { 12 | 13 | } 14 | 15 | public static UrlPatternTreeNode getUrlNode(String url, UrlPatternTreeNode currentNode){ 16 | UrlPatternTreeNode urlNodeCandidate = getNode(url,currentNode); 17 | if(urlNodeCandidate!=null && urlNodeCandidate.isUrlNode()) { 18 | return urlNodeCandidate; 19 | } 20 | return null; 21 | } 22 | 23 | 24 | public static UrlPatternTreeNode getNode(String url, UrlPatternTreeNode currentNode){ 25 | String[] urlParts = url.split("/"); 26 | for(int i=1;i pattern : currentNode.getPatterns().entrySet()) { 39 | if(Pattern.matches(pattern.getKey(), urlPart) || pattern.getKey().equals(urlPart)) { 40 | nextNode = pattern.getValue(); 41 | break; 42 | } 43 | } 44 | } 45 | return nextNode; 46 | } 47 | 48 | 49 | public static void addUrlNode(String url, List userRoleClaim, String urlClaim, UrlPatternTreeNode currentNode){ 50 | UrlPatternTreeNode childNode = addNode(url, currentNode); 51 | childNode.setUrlNode(true); 52 | childNode.setUrlClaimPattern(urlClaim); 53 | childNode.setUserRoleClaim(userRoleClaim); 54 | } 55 | 56 | public static UrlPatternTreeNode addNode(String url, UrlPatternTreeNode currentNode){ 57 | String[] urlParts = url.split("/"); 58 | for(int i=1; i claimNode = Optional 36 | .ofNullable(urlPatternTree.verifyIfRequestIsAuthorized(requestURI)); 37 | if (claimNode.isPresent()) { 38 | String userRoleInToken = requestContext.get("userRole").toString(); 39 | validateUserRoleClaim(userRoleInToken, claimNode.get()); 40 | validateUrlClaim(requestURI, claimNode.get().getUrlClaimPattern(), requestContext); 41 | addClaimsToRequestAsHeaders(requestContext); 42 | }else { 43 | throw new ZuulException("Invalid Token", HttpStatus.FORBIDDEN.value(), "Invalid Route"); 44 | } 45 | requestContext.put(IS_SESSION_UPDATE_REQUIRED, true); 46 | return null; 47 | } 48 | 49 | private void addClaimsToRequestAsHeaders(RequestContext requestContext) { 50 | requestContext.addZuulRequestHeader(USER_ID_CLAIM,requestContext.get(USER_ID_CLAIM).toString()); 51 | requestContext.addZuulRequestHeader(USER_ROLE_CLAIM,requestContext.get(USER_ROLE_CLAIM).toString()); 52 | requestContext.addZuulRequestHeader(USER_TYPE_CLAIM,requestContext.get(USER_TYPE_CLAIM).toString()); 53 | } 54 | 55 | private void validateUrlClaim(String requestURI, String urlClaimPattern, RequestContext requestContext) throws ZuulException { 56 | String[] requestURIParts = requestURI.split("/"); 57 | String[] urlClaimPatternParts = urlClaimPattern.split("/"); 58 | if(requestURIParts.length == urlClaimPatternParts.length) { 59 | for(int i=1; i userRoleClaim = claimNode.getUserRoleClaim(); 78 | if (userRoleClaim!=null && !userRoleClaim.contains(userRoleInToken)) { 79 | throw new ZuulException("Invalid Token", HttpStatus.FORBIDDEN.value(), "User Role is Invalid"); 80 | } 81 | } 82 | 83 | @Override 84 | public String filterType() { 85 | return FilterConstants.PRE_TYPE; 86 | } 87 | 88 | @Override 89 | public int filterOrder() { 90 | return FilterConstants.PRE_DECORATION_FILTER_ORDER - 2; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/github/abhinavrohatgi30/filter/AuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.abhinavrohatgi30.filter; 2 | 3 | import static com.github.abhinavrohatgi30.constants.FilterConstants.IS_CLAIM_VERIFICATION_REQUIRED; 4 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_ID_CLAIM; 5 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_ROLE_CLAIM; 6 | import static com.github.abhinavrohatgi30.constants.FilterConstants.USER_TYPE_CLAIM; 7 | 8 | import java.time.LocalDateTime; 9 | import java.time.ZoneId; 10 | import java.util.Date; 11 | import java.util.Optional; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | 15 | import org.apache.commons.codec.binary.Base64; 16 | import org.apache.commons.codec.binary.StringUtils; 17 | import org.json.JSONObject; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.stereotype.Component; 22 | 23 | import com.auth0.jwt.interfaces.DecodedJWT; 24 | import com.github.abhinavrohatgi30.models.UserInfoDTO; 25 | import com.github.abhinavrohatgi30.service.JWTService; 26 | import com.netflix.zuul.ZuulFilter; 27 | import com.netflix.zuul.context.RequestContext; 28 | import com.netflix.zuul.exception.ZuulException; 29 | 30 | @Component 31 | public class AuthenticationFilter extends ZuulFilter{ 32 | 33 | @Autowired 34 | private JWTService jwtService; 35 | 36 | @Override 37 | public boolean shouldFilter() { 38 | final RequestContext requestContext = RequestContext.getCurrentContext(); 39 | HttpServletRequest request = requestContext.getRequest(); 40 | return !(request.getRequestURI().startsWith("/token")); 41 | } 42 | 43 | @Override 44 | public Object run() throws ZuulException { 45 | final RequestContext requestContext = RequestContext.getCurrentContext(); 46 | HttpServletRequest request = requestContext.getRequest(); 47 | Optional authorizationHeader = Optional.ofNullable(request.getHeader("Authorization")); 48 | Optional authorizationToken = authorizationHeader.map(AuthenticationFilter::getTokenFromAuthorizationHeader); 49 | Optional decodedToken = authorizationToken.map(jwtService::verifyToken); 50 | if (decodedToken.isPresent()) { 51 | DecodedJWT decodedAT = decodedToken.get(); 52 | Date expirationTime = decodedAT.getExpiresAt(); 53 | LocalDateTime ldt = LocalDateTime.ofInstant(expirationTime.toInstant(), ZoneId.systemDefault()); 54 | if (ldt.isBefore(LocalDateTime.now())) { 55 | throw new ZuulException("Invalid Token", HttpStatus.UNAUTHORIZED.value(), "Token Expired"); 56 | } 57 | UserInfoDTO userInfoDTO = extractUserInfoFromClaims(decodedAT); 58 | requestContext.put(USER_ROLE_CLAIM, userInfoDTO.getUserRole()); 59 | requestContext.put(USER_ID_CLAIM, userInfoDTO.getUserId()); 60 | requestContext.put(USER_TYPE_CLAIM, userInfoDTO.getUserType()); 61 | requestContext.put(IS_CLAIM_VERIFICATION_REQUIRED, true); 62 | } else { 63 | throw new ZuulException("Invalid Token", HttpStatus.UNAUTHORIZED.value(), "Token Signature is Invalid"); 64 | } 65 | return null; 66 | } 67 | 68 | @Override 69 | public String filterType() { 70 | return FilterConstants.PRE_TYPE; 71 | } 72 | 73 | @Override 74 | public int filterOrder() { 75 | return FilterConstants.PRE_DECORATION_FILTER_ORDER - 3; 76 | } 77 | 78 | public static String getTokenFromAuthorizationHeader(String header) { 79 | String token = header.replace("Bearer ", ""); 80 | return token.trim(); 81 | } 82 | 83 | public static UserInfoDTO extractUserInfoFromClaims(DecodedJWT decodedJWT) { 84 | JSONObject payloadObject = new JSONObject(StringUtils.newStringUtf8(Base64.decodeBase64(decodedJWT.getPayload()))); 85 | final Long userId = payloadObject.getLong(USER_ID_CLAIM); 86 | final String userType = payloadObject.getString(USER_TYPE_CLAIM); 87 | final String userRole = payloadObject.getString(USER_ROLE_CLAIM); 88 | 89 | return new UserInfoDTO(userId, userType, userRole); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Centralized Auth Gateway 2 | 3 | This is a centralized authentication and authorization gateway which is built on top of Netflix Zuul. 4 | The implementation basically chains an Authentication Filter and an Authorization Filter which precede the Pre Decoration Filter of Zuul. 5 | 6 | ## Authentication Filter 7 | 8 | The Authentication filter is the first filter that runs in the filter chain. As the name suggests it provides an authentication layer by validating the Access Token of the request. 9 | The Filter performs the following actions : 10 | - Extracts the token from the request 11 | - Decodes the JWT 12 | - Verifies the signature, issuer and expiry 13 | - Sets the userRole, userId and userType claims in the Request Context 14 | - Sets the isAuthorizationRequired Flag to true if all the above tasks succeed, else throws an exception indicating what caused the request to fail. 15 | 16 | 17 | ## Authorization Filter 18 | 19 | The Authorization filter is the second filter that runs in the filter chain. As the name suggests it provides an authorization layer by validating the claims set by the Authentication Filter into the request context. 20 | 21 | The properties are configured in a property file called authorization.yml which captures the user defined validations to be processed and in order to perform these validations in an optimized way, it creates a URL Pattern Tree which is basically a tree made up of nodes that represent the path/patterns of the url at different levels, these levels are formed by splitting the url patterns by '/'. Every node that represents a url pattern is designated as a url node. 22 | 23 | It performs 2 mandatory validations and one optional validation : 24 | 25 | 1. Valid Url Pattern 26 | 2. Valid Url Claim 27 | 3. Valid User Role (Optional) 28 | 29 | 30 | ### Valid URL Pattern 31 | The URL Pattern Tree formed above is used to validate if a request matches any valid url pattern. If the tree contains a url pattern node that matches the request it returns a valid response and performs the url claim validation, else it throws an exception indicating an Invalid Route. 32 | 33 | ### Valid URL Claim 34 | The URL Pattern once validated then is checked for claims in the pattern, the user can denote one of the path parameters in the request to match one of the jwt claims. This is achieved by finding out the part of the url that is designated as a claim (identified by the string :claim_name) and then substituting it with the claim value and checking for equality with the path in the request. If the url claim is found to be valid the request is allowed to proceed further, else an exception is thrown that specifies that the url claim was invalid. 35 | 36 | ### Valid User Role Claim 37 | This is an optional validation and is performed after the url claim validation, here the user can designate if the url pattern can only be requested by certain role/roles. If so, we validate the userRole claim against the list of claims/claim denoted by the user for the specific url pattern. If found to be valid the request is allowed to proceed further, else an exception is thrown that specifies that the userRole was not allowed. 38 | 39 | 40 | If all the above validations succeed the request is allowed to proceed further in the filter chain and is reverse proxied to the designated zuul route that matches the request. 41 | 42 | 43 | 44 | ## Playing Around with the Gateway 45 | 46 | Start the Application by building it using Maven and then running the application, the application by default runs on port 8082. 47 | To Play around with the gateway one can make use of the inbuilt endpoint to generate a token by providing a userRole, userId and a userType and generate an access token for the subsequent requests. You can use the following command to do the same : 48 | 49 | ``` 50 | curl -X POST \ 51 | http://localhost:8082/token/generate \ 52 | -H 'Content-Type: application/json' \ 53 | -d '{ 54 | "userId":1, 55 | "userType":"regular", 56 | "userRole":"regular" 57 | }' 58 | ``` 59 | You can then use this token as shown below in the subsequent requests : 60 | 61 | ``` 62 | curl -X GET \ 63 | http://localhost:8082/test/endpoint/v0/test/1/ping \ 64 | -H 'Authorization: Bearer ACCESS_TOKEN_HERE' 65 | ``` 66 | 67 | You can validate the following scenarios: 68 | 1. Invalid Token Signature 69 | 2. Expired Token 70 | 3. Invalid URL Pattern 71 | 4. Invalid URL Claim 72 | 5. Invalid User Role 73 | 74 | --------------------------------------------------------------------------------