springazurespring-bootspring-securitymutual-authentication

Spring Boot in Azure - Client Certificate in Request Header


We currently implemented mutual authentication in our Spring Boot application and need to deploy it in Azure. Azure's loadbalancer redirects the client certificate (Base64 encoded) in the request header field "X-ARR-ClientCert" and Spring is not able to find it there. => Authentication fails

The microsoft documentation shows how to handle this in a .NET application: https://learn.microsoft.com/en-gb/azure/app-service-web/app-service-web-configure-tls-mutual-auth

I tried to extract the certificate from the header in an OncePerRequestFilter and set it to the request like this:

public class AzureCertificateFilter extends OncePerRequestFilter {
    private static final Logger LOG = LoggerFactory.getLogger(AzureCertifacteFilter.class);
    private static final String AZURE_CLIENT_CERTIFICATE_HEADER = "X-ARR-ClientCert";
    private static final String JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
    private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----\n";
    private static final String END_CERT = "\n-----END CERTIFICATE-----";

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        X509Certificate x509Certificate = extractClientCertificate(httpServletRequest);

        // azure redirects the certificate in a header field
        if (x509Certificate == null && StringUtils.isNotBlank(httpServletRequest.getHeader(AZURE_CLIENT_CERTIFICATE_HEADER))) {
            String x509CertHeader = BEGIN_CERT + httpServletRequest.getHeader(AZURE_CLIENT_CERTIFICATE_HEADER) + END_CERT;

            try (ByteArrayInputStream certificateStream = new ByteArrayInputStream(x509CertHeader.getBytes())) {
                X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificateStream);
                httpServletRequest.setAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, certificate);
            } catch (CertificateException e) {
                LOG.error("X.509 certificate could not be created out of the header field {}. Exception: {}", AZURE_CLIENT_CERTIFICATE_HEADER, e.getMessage());
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private X509Certificate extractClientCertificate(HttpServletRequest request) {
        X509Certificate[] certs = (X509Certificate[]) request.getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);

        if (certs != null && certs.length > 0) {
            LOG.debug("X.509 client authentication certificate:" + certs[0]);
            return certs[0];
        }

        LOG.debug("No client certificate found in request.");
        return null;
    }
}

But this fails later in the Spring filter chain with the following exception:

sun.security.x509.X509CertImpl cannot be cast to [Ljava.security.cert.X509Certificate; /oaa/v1/spaces

The configuration looks like this:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("**/docs/restapi/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
        .disable()
        .addFilterBefore(new AzureCertificateFilter(), X509AuthenticationFilter.class)
        .x509()
        .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
        .userDetailsService(userDetailsService());
}

Solution

  • I should have read the exception more carefully:

    sun.security.x509.X509CertImpl cannot be cast to [Ljava.security.cert.X509Certificate; /oaa/v1/spaces
    

    I had to set an array of certificates like this:

    httpServletRequest.setAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE, new X509Certificate[]{x509Certificate});