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