Skip to content

Commit

Permalink
feature/annotation-based-public-api-declaration (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
hardikSinghBehl authored Mar 13, 2024
1 parent d2c168e commit 7445748
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 183 deletions.
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,26 @@
### Key Components
* [JwtAuthenticationFilter.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/filter/JwtAuthenticationFilter.java)
* [SecurityConfiguration.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/SecurityConfiguration.java)
* [ApiPathExclusionConfigurationProperties.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/ApiPathExclusionConfigurationProperties.java)
* [ApiEndpointSecurityInspector.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/utility/ApiEndpointSecurityInspector.java)
* [TokenConfigurationProperties.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/TokenConfigurationProperties.java)
* [JwtUtility.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/utility/JwtUtility.java)
* [TokenRevocationService.java](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/service/TokenRevocationService.java)

Any request to a secured endpoint is intercepted by the [JwtAuthenticationFilter](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/filter/JwtAuthenticationFilter.java), which is added to the security filter chain and configured in the [SecurityConfiguration](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/SecurityConfiguration.java). The custom filter holds the responsibility for verifying the authenticity of the incoming access token and populating the security context.

Any API that needs to be made public can be configured in the active `.yml` file, the values will be mapped to [ApiPathExclusionConfigurationProperties](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/ApiPathExclusionConfigurationProperties.java) and referenced by the application. Requests to the configured API paths will not be evaluated by the [JwtAuthenticationFilter](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/filter/JwtAuthenticationFilter.java). Below is a sample snippet declaring public/non-secured APIs in `application.yaml` file.
### Public API declaration

```yaml
com:
behl:
cerberus:
unsecured:
api-path:
swagger-v3: true
post:
- /users
- /auth/login
put:
- /auth/refresh
Any API that needs to be made public can be annotated with [@PublicEndpoint](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/configuration/PublicEndpoint.java). Requests to the configured API paths will not evaluated by the custom security filter with the logic being governed by [ApiEndpointSecurityInspector](https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/blob/master/src/main/java/com/behl/cerberus/utility/ApiEndpointSecurityInspector.java).

Below is a sample controller method declared as public which will be exempted from authentication checks:

```java
@PublicEndpoint
@GetMapping(value = "/api/v1/something")
public ResponseEntity<Something> getSomething() {
var something = someService.fetch();
return ResponseEntity.ok(something);
}
```

### Token Generation and Configuration
Expand Down Expand Up @@ -112,4 +111,4 @@ unset JWT_PUBLIC_KEY

### Visual Walkthrough

https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/assets/69693621/54ef4877-49b9-4112-9f85-9b9abda74068
https://github.com/hardikSinghBehl/jwt-auth-flow-spring-security/assets/69693621/54ef4877-49b9-4112-9f85-9b9abda74068

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ public class OpenApiConfigurationProperties {
@Setter
public class OpenAPI {

/**
* Determines whether Swagger v3 API documentation and related endpoints are
* accessible bypassing Authentication and Authorization checks. Swagger
* endpoints are restricted by default.
*
* Can be used in profile-specific configuration files to control
* access based on current environments.
*/
private boolean enabled;

private String title;
private String description;
private String apiVersion;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/behl/cerberus/configuration/PublicEndpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.behl.cerberus.configuration;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to declare API endpoints as public i.e non-secured, allowing then
* to be accessed without a valid Authorization header in HTTP request.
*
* When applied to a controller method, requests to the method will be exempted
* from authentication checks by the {@link com.behl.cerberus.filter.JwtAuthenticationFilter}
*
* @see com.behl.cerberus.configuration.SecurityConfiguration
* @see com.behl.cerberus.filter.JwtAuthenticationFilter
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PublicEndpoint {

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.behl.cerberus.configuration;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand All @@ -17,6 +12,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.behl.cerberus.filter.JwtAuthenticationFilter;
import com.behl.cerberus.utility.ApiEndpointSecurityInspector;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
Expand All @@ -34,44 +30,32 @@
* authentication verification.</li>
* </ul>
*
* @see com.behl.cerberus.configuration.ApiPathExclusionConfigurationProperties
* @see com.behl.cerberus.filter.JwtAuthenticationFilter
* @see com.behl.cerberus.configuration.CustomAuthenticationEntryPoint
* @see com.behl.cerberus.utility.ApiEndpointSecurityInspector
*/
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
@EnableConfigurationProperties(ApiPathExclusionConfigurationProperties.class)
public class SecurityConfiguration {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final ApiPathExclusionConfigurationProperties apiPathExclusionConfigurationProperties;
private final ApiEndpointSecurityInspector apiEndpointSecurityInspector;

private static final List<String> SWAGGER_V3_PATHS = List.of("/swagger-ui**/**", "/v3/api-docs**/**");

@Bean
@SneakyThrows
public SecurityFilterChain configure(final HttpSecurity http) {
final var unsecuredGetEndpoints = Optional.ofNullable(apiPathExclusionConfigurationProperties.getGet()).orElseGet(ArrayList::new);
final var unsecuredPostEndpoints = Optional.ofNullable(apiPathExclusionConfigurationProperties.getPost()).orElseGet(ArrayList::new);
final var unsecuredPutEndpoints = Optional.ofNullable(apiPathExclusionConfigurationProperties.getPut()).orElseGet(ArrayList::new);

if (Boolean.TRUE.equals(apiPathExclusionConfigurationProperties.isSwaggerV3())) {
unsecuredGetEndpoints.addAll(SWAGGER_V3_PATHS);
apiPathExclusionConfigurationProperties.setGet(unsecuredGetEndpoints);
}

http
.cors(corsConfigurer -> corsConfigurer.disable())
.csrf(csrfConfigurer -> csrfConfigurer.disable())
.exceptionHandling(exceptionConfigurer -> exceptionConfigurer.authenticationEntryPoint(customAuthenticationEntryPoint))
.sessionManagement(sessionConfigurer -> sessionConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authManager -> {
authManager
.requestMatchers(HttpMethod.GET, unsecuredGetEndpoints.toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.POST, unsecuredPostEndpoints.toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.PUT, unsecuredPutEndpoints.toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.GET, apiEndpointSecurityInspector.getPublicGetEndpoints().toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.POST, apiEndpointSecurityInspector.getPublicPostEndpoints().toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.PUT, apiEndpointSecurityInspector.getPublicPutEndpoints().toArray(String[]::new)).permitAll()
.anyRequest().authenticated();
})
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.behl.cerberus.configuration.PublicEndpoint;
import com.behl.cerberus.dto.ExceptionResponseDto;
import com.behl.cerberus.dto.TokenSuccessResponseDto;
import com.behl.cerberus.dto.UserLoginRequestDto;
Expand All @@ -33,6 +34,7 @@ public class AuthenticationController {
private final AuthenticationService authenticationService;
private final RefreshTokenHeaderProvider refreshTokenHeaderProvider;

@PublicEndpoint
@PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Logs in user into the system", description = "Returns Access-token and Refresh-token on successfull authentication which provides access to protected endpoints")
@ApiResponses(value = {
Expand All @@ -44,6 +46,7 @@ public ResponseEntity<TokenSuccessResponseDto> login(@Valid @RequestBody final U
return ResponseEntity.ok(tokenResponse);
}

@PublicEndpoint
@PutMapping(value = "/refresh", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Refreshes Access-Token for a user", description = "Provides a new Access-token against the user for which the non expired refresh-token is provided")
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.behl.cerberus.configuration.PublicEndpoint;
import com.behl.cerberus.dto.ExceptionResponseDto;
import com.behl.cerberus.dto.UserCreationRequestDto;
import com.behl.cerberus.dto.UserDetailDto;
Expand All @@ -37,6 +38,7 @@ public class UserController {
private final UserService userService;
private final AuthenticatedUserIdProvider authenticatedUserIdProvider;

@PublicEndpoint
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Creates a user account", description = "Registers a unique user record in the system corresponding to the provided information")
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.behl.cerberus.utility;

import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -8,26 +12,72 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import com.behl.cerberus.configuration.ApiPathExclusionConfigurationProperties;
import com.behl.cerberus.configuration.OpenApiConfigurationProperties;
import com.behl.cerberus.configuration.PublicEndpoint;

import io.swagger.v3.oas.models.PathItem.HttpMethod;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

/**
* Utility class responsible for inspecting the security of API endpoints by
* determining whether a given HTTP request is destined for a secured or
* unsecured API endpoint. It works in conjunction with the api paths mapped in
* {@link ApiPathExclusionConfigurationProperties}.
* Utility class responsible for evaluating the accessibility of API endpoints
* based on their security configuration. It works in conjunction with the
* mappings of controller methods annotated with {@link PublicEndpoint}.
*
* @see com.behl.overseer.configuration.PublicEndpoint
* @see com.behl.overseer.configuration.OpenApiConfigurationProperties
*/
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(ApiPathExclusionConfigurationProperties.class)
@EnableConfigurationProperties(OpenApiConfigurationProperties.class)
public class ApiEndpointSecurityInspector {

private final ApiPathExclusionConfigurationProperties apiPathExclusionConfigurationProperties;
private final RequestMappingHandlerMapping requestHandlerMapping;
private final OpenApiConfigurationProperties openApiConfigurationProperties;

private static final List<String> SWAGGER_V3_PATHS = List.of("/swagger-ui**/**", "/v3/api-docs**/**");

@Getter
private List<String> publicGetEndpoints = new ArrayList<String>();
@Getter
private List<String> publicPostEndpoints = new ArrayList<String>();
@Getter
private List<String> publicPutEndpoints = new ArrayList<String>();

/**
* Initializes the class by gathering public endpoints for various HTTP methods.
* It identifies designated public endpoints within the application's mappings
* and adds them to separate lists based on their associated HTTP methods.
* If OpenAPI is enabled, Swagger endpoints are also considered as public.
*/
@PostConstruct
public void init() {
final var handlerMethods = requestHandlerMapping.getHandlerMethods();
handlerMethods.forEach((requestInfo, handlerMethod) -> {
if (handlerMethod.hasMethodAnnotation(PublicEndpoint.class)) {
final var httpMethod = requestInfo.getMethodsCondition().getMethods().iterator().next().asHttpMethod();
final var apiPaths = requestInfo.getPathPatternsCondition().getPatternValues();

if (httpMethod.equals(GET)) {
publicGetEndpoints.addAll(apiPaths);
} else if (httpMethod.equals(POST)) {
publicPostEndpoints.addAll(apiPaths);
} else if (httpMethod.equals(PUT)) {
publicPutEndpoints.addAll(apiPaths);
}
}
});

final var openApiEnabled = openApiConfigurationProperties.getOpenApi().isEnabled();
if (Boolean.TRUE.equals(openApiEnabled)) {
publicGetEndpoints.addAll(SWAGGER_V3_PATHS);
}
}

/**
* Checks if the provided HTTP request is directed towards an unsecured API endpoint.
Expand All @@ -44,21 +94,19 @@ public boolean isUnsecureRequest(@NonNull final HttpServletRequest request) {
}

/**
* Retrieves the list of unsecured API paths based on the provided HTTP method.
* The api endpoints paths are configured in the active {@code .yml} file and
* mapped to the {@code ApiPathExclusionConfigurationProperties.java}
*
* Retrieves the list of unsecured API paths based on the provided HTTP method.
*
* @param httpMethod The HTTP method for which unsecured paths are to be retrieved.
* @return A list of unsecured API paths for the specified HTTP method.
* @return A list of unsecured API paths for the specified HTTP method.s
*/
private List<String> getUnsecuredApiPaths(@NonNull final HttpMethod httpMethod) {
switch (httpMethod) {
case GET:
return apiPathExclusionConfigurationProperties.getGet();
return publicGetEndpoints;
case POST:
return apiPathExclusionConfigurationProperties.getPost();
return publicPostEndpoints;
case PUT:
return apiPathExclusionConfigurationProperties.getPut();
return publicPutEndpoints;
default:
return Collections.emptyList();
}
Expand Down
9 changes: 1 addition & 8 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ spring:
com:
behl:
cerberus:
unsecured:
api-path:
swagger-v3: true
post:
- /users
- /auth/login
put:
- /auth/refresh
token:
access-token:
private-key: ${JWT_PRIVATE_KEY}
Expand All @@ -36,6 +28,7 @@ com:
refresh-token:
validity: 120
open-api:
enabled: true
api-version: 1.0.0
title: Cerberus
description: Java Backend application using Spring-security to implement JWT based Authentication and Authorization
Loading

0 comments on commit 7445748

Please sign in to comment.