mirror of https://github.com/halo-dev/halo
feat: Add bearer token authentication filter for bearer authentication (#1872)
parent
e8d8ef8f28
commit
6de85184b1
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue