feat: Add bearer token authentication filter for bearer authentication (#1872)

pull/1876/head
guqing 2022-04-22 14:36:11 +08:00 committed by GitHub
parent e8d8ef8f28
commit 6de85184b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 283 additions and 0 deletions

View File

@ -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();
}
}

View File

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