diff --git a/src/main/java/com/gw2auth/oauth2/server/configuration/OAuth2ServerConfiguration.java b/src/main/java/com/gw2auth/oauth2/server/configuration/OAuth2ServerConfiguration.java index 8f52f4e..a0e954a 100644 --- a/src/main/java/com/gw2auth/oauth2/server/configuration/OAuth2ServerConfiguration.java +++ b/src/main/java/com/gw2auth/oauth2/server/configuration/OAuth2ServerConfiguration.java @@ -2,13 +2,17 @@ import com.gw2auth.oauth2.server.adapt.CustomOAuth2ServerAuthenticationProviders; import com.gw2auth.oauth2.server.service.application.AuthorizationCodeParamAccessor; +import com.gw2auth.oauth2.server.util.ComposedMDCCloseable; import com.gw2auth.oauth2.server.util.JWKHelper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -24,6 +28,7 @@ import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; @@ -34,10 +39,14 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; +import java.util.*; +import java.util.function.Function; @Configuration public class OAuth2ServerConfiguration { @@ -110,7 +119,8 @@ public SecurityFilterChain oauth2ServerHttpSecurityFilterChain(HttpSecurity http .securityContext(securityContextCustomizer) .requestCache(requestCacheCustomizer) .oauth2Login(oauth2LoginCustomizer) - .with(configurer, ignored -> {}); + .with(configurer, ignored -> {}) + .addFilterBefore(new OAuth2ServerLoggingFilter(), SecurityContextHolderFilter.class); return http.build(); } @@ -143,4 +153,141 @@ public AuthorizationServerSettings authorizationServerSettings(@Value("${com.gw2 public AuthorizationCodeParamAccessor authorizationCodeParamAccessor() { return AuthorizationCodeParamAccessor.DEFAULT; } + + private static class OAuth2ServerLoggingFilter extends OncePerRequestFilter { + + private static final Logger LOG = LoggerFactory.getLogger(OAuth2ServerLoggingFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Map requestAttributes; + try { + requestAttributes = buildRequestAttributes(request); + } catch (Exception e) { + // better be safe than sorry + LOG.warn("failed to build request attributes", e); + requestAttributes = Map.of(); + } + + Exception exc = null; + try { + filterChain.doFilter(request, response); + } catch (Exception e) { + exc = e; + } + + Map responseAttributes; + try { + responseAttributes = buildResponseAttributes(response); + } catch (Exception e) { + LOG.warn("failed to build response attributes", e); + responseAttributes = Map.of(); + } + + try (ComposedMDCCloseable _unused = ComposedMDCCloseable.create(requestAttributes, Object::toString)) { + try (ComposedMDCCloseable __unused = ComposedMDCCloseable.create(responseAttributes, Object::toString)) { + if (exc == null) { + LOG.info("oauth2 request handled successfully"); + } else { + LOG.info("oauth2 request failed", exc); + } + } + } + + if (exc != null) { + switch (exc) { + case ServletException e: + throw e; + + case IOException e: + throw e; + + case RuntimeException e: + throw e; + + default: + throw new RuntimeException("Unexpected error occurred while logging request", exc); + } + } + } + + private static Map buildRequestAttributes(HttpServletRequest request) { + final UriComponents uriComponents = UriComponentsBuilder.fromUriString(request.getRequestURI()) + .query(request.getQueryString()) + .build(); + + final Map attributes = new HashMap<>(); + attributes.put("request.method", request.getMethod()); + attributes.put("request.url", uriComponents.getPath()); + uriComponents.getQueryParams().forEach((key, value) -> { + if (!key.equalsIgnoreCase(OAuth2ParameterNames.CLIENT_SECRET) + && !key.equalsIgnoreCase(OAuth2ParameterNames.STATE) + && !key.equalsIgnoreCase("code_challenge") + && !key.equalsIgnoreCase("code_verifier")) { + + addMultiValue(attributes, "request.query." + key, value); + } + }); + + addHeaders( + attributes, + "request", + () -> request.getHeaderNames().asIterator(), + (v) -> () -> request.getHeaders(v).asIterator(), + Set.of( + "cookie", + "authorization" + ) + ); + + return attributes; + } + + private static Map buildResponseAttributes(HttpServletResponse response) { + final Map attributes = new HashMap<>(); + attributes.put("response.status_code", Integer.toString(response.getStatus())); + addHeaders( + attributes, + "response", + response.getHeaderNames(), + response::getHeaders, + Set.of( + "set-cookie", + "pragma", + "x-xss-protection", + "x-content-type-options", + "expires", + "cache-control", + "x-frame-options" + ) + ); + + return attributes; + } + + private static void addHeaders(Map map, String prefix, Iterable names, Function> getHeaders, Set ignore) { + for (String header : names) { + if (!ignore.contains(header.toLowerCase())) { + final List values = new ArrayList<>(); + for (String value : getHeaders.apply(header)) { + values.add(value); + } + + addMultiValue(map, prefix + ".header." + header, values); + } + } + } + + private static void addMultiValue(Map map, String key, List values) { + if (values.isEmpty()) { + map.put(key, ""); + } else if (values.size() == 1) { + map.put(key, values.getFirst()); + } else { + for (int i = 0; i < values.size(); i++) { + map.put(key + "." + i, values.get(i)); + } + } + } + } }