From 6de85184b19a80041e8a3042258bdd256916e476 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 22 Apr 2022 14:36:11 +0800 Subject: [PATCH] feat: Add bearer token authentication filter for bearer authentication (#1872) --- .../BearerTokenAuthenticationEntryPoint.java | 86 ++++++++ .../BearerTokenAuthenticationFilter.java | 197 ++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationEntryPoint.java create mode 100644 src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationFilter.java diff --git a/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationEntryPoint.java b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationEntryPoint.java new file mode 100644 index 000000000..a273f5778 --- /dev/null +++ b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationEntryPoint.java @@ -0,0 +1,86 @@ +package run.halo.app.identity.authentication.verifier; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.util.StringUtils; + +/** + * @author guqing + * @since 2.0.0 + */ +public class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { + private String realmName; + + /** + * Collect error details from the provided parameters and format according to RFC + * 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and + * {@code scope}. + * + * @param request that resulted in an <code>AuthenticationException</code> + * @param response so that the user agent can begin authentication + * @param authException that caused the invocation + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + HttpStatus status = HttpStatus.UNAUTHORIZED; + Map<String, String> parameters = new LinkedHashMap<>(); + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + parameters.put("error", error.getErrorCode()); + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + if (error instanceof BearerTokenError bearerTokenError) { + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + status = ((BearerTokenError) error).getHttpStatus(); + } + } + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(status.value()); + } + + /** + * Set the default realm name to use in the bearer token error response + * + * @param realmName realm name + */ + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) { + StringBuilder wwwAuthenticate = new StringBuilder(); + wwwAuthenticate.append("Bearer"); + if (!parameters.isEmpty()) { + wwwAuthenticate.append(" "); + int i = 0; + for (Map.Entry<String, String> entry : parameters.entrySet()) { + wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()) + .append("\""); + if (i != parameters.size() - 1) { + wwwAuthenticate.append(", "); + } + i++; + } + } + return wwwAuthenticate.toString(); + } +} diff --git a/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationFilter.java b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationFilter.java new file mode 100644 index 000000000..bd099921f --- /dev/null +++ b/src/main/java/run/halo/app/identity/authentication/verifier/BearerTokenAuthenticationFilter.java @@ -0,0 +1,197 @@ +package run.halo.app.identity.authentication.verifier; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.core.log.LogMessage; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.context.NullSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Authenticates requests that contain an OAuth 2.0 + * <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>. + * <p> + * This filter should be wired with an {@link AuthenticationManager} that can authenticate + * a {@link BearerTokenAuthenticationToken}. + * + * @author guqing + * @see + * <a href="https://tools.ietf.org/html/rfc6750">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a> + * @see JwtAuthenticationProvider + * @since 2.0.0 + */ +public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { + private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver; + + private AuthenticationEntryPoint authenticationEntryPoint = + new BearerTokenAuthenticationEntryPoint(); + + private AuthenticationFailureHandler authenticationFailureHandler = + (request, response, exception) -> { + if (exception instanceof AuthenticationServiceException) { + throw exception; + } + this.authenticationEntryPoint.commence(request, response, exception); + }; + + private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + + private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = + new WebAuthenticationDetailsSource(); + + private SecurityContextRepository securityContextRepository = + new NullSecurityContextRepository(); + + /** + * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) + * + * @param authenticationManagerResolver authentication manager resolver + */ + public BearerTokenAuthenticationFilter( + AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) { + Assert.notNull(authenticationManagerResolver, + "authenticationManagerResolver cannot be null"); + this.authenticationManagerResolver = authenticationManagerResolver; + } + + /** + * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) + * + * @param authenticationManager authentication manager + */ + public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + this.authenticationManagerResolver = (request) -> authenticationManager; + } + + /** + * Extract any + * <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a> + * from the request and attempt an authentication. + * + * @param request http servlet request + * @param response http servlet response + * @param filterChain filter chain + */ + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) + throws ServletException, IOException { + String token; + try { + token = this.bearerTokenResolver.resolve(request); + } catch (OAuth2AuthenticationException invalid) { + this.logger.trace( + "Sending to authentication entry point since failed to resolve bearer token", + invalid); + this.authenticationEntryPoint.commence(request, response, invalid); + return; + } + if (token == null) { + this.logger.trace("Did not process request since did not find bearer token"); + filterChain.doFilter(request, response); + return; + } + + BearerTokenAuthenticationToken authenticationRequest = + new BearerTokenAuthenticationToken(token); + authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + try { + AuthenticationManager authenticationManager = + this.authenticationManagerResolver.resolve(request); + Authentication authenticationResult = + authenticationManager.authenticate(authenticationRequest); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticationResult); + SecurityContextHolder.setContext(context); + this.securityContextRepository.saveContext(context, request, response); + this.logger.debug( + LogMessage.format("Set SecurityContextHolder to %s", authenticationResult)); + filterChain.doFilter(request, response); + } catch (AuthenticationException failed) { + SecurityContextHolder.clearContext(); + this.logger.trace("Failed to process authentication request", failed); + this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed); + } + } + + /** + * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on + * authentication success. The default action is not to save the + * {@link SecurityContext}. + * + * @param securityContextRepository the {@link SecurityContextRepository} to use. + * Cannot be null. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + + /** + * Set the {@link BearerTokenResolver} to use. Defaults to + * {@link DefaultBearerTokenResolver}. + * + * @param bearerTokenResolver the {@code BearerTokenResolver} to use + */ + public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; + } + + /** + * Set the {@link AuthenticationEntryPoint} to use. Defaults to + * {@link BearerTokenAuthenticationEntryPoint}. + * + * @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use + */ + public void setAuthenticationEntryPoint( + final AuthenticationEntryPoint authenticationEntryPoint) { + Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); + this.authenticationEntryPoint = authenticationEntryPoint; + } + + /** + * Set the {@link AuthenticationFailureHandler} to use. Default implementation invokes + * {@link AuthenticationEntryPoint}. + * + * @param authenticationFailureHandler the {@code AuthenticationFailureHandler} to use + * @since 5.2 + */ + public void setAuthenticationFailureHandler( + final AuthenticationFailureHandler authenticationFailureHandler) { + Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); + this.authenticationFailureHandler = authenticationFailureHandler; + } + + /** + * Set the {@link AuthenticationDetailsSource} to use. Defaults to + * {@link WebAuthenticationDetailsSource}. + * + * @param authenticationDetailsSource the {@code AuthenticationConverter} to use + * @since 5.5 + */ + public void setAuthenticationDetailsSource( + AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); + this.authenticationDetailsSource = authenticationDetailsSource; + } +}