Spring Boot. HMAC authentication. How to add custom AuthenticationProvider and Authentication filter?

10,280

I'm not familiar with Spring Boot, but I saw your comment on my question How To Inject AuthenticationManager using Java Configuration in a Custom Filter

In a traditional Spring Security XML configuration, you would specify your custom RestSecurityFilter like so

<http use-expressions="true" create-session="stateless" authentication-manager-ref="authenticationManager" entry-point-ref="restAuthenticationEntryPoint">
       <custom-filter ref="restSecurityFilter" position="FORM_LOGIN_FILTER" />
</http>

More information http://docs.spring.io/spring-security/site/docs/4.0.1.RELEASE/reference/htmlsingle/#ns-custom-filters

Share:
10,280
PaintedRed
Author by

PaintedRed

Updated on July 13, 2022

Comments

  • PaintedRed
    PaintedRed almost 2 years

    To implement HMAC authentication I made my own filter, provider and token. RestSecurityFilter:

    public class RestSecurityFilter extends AbstractAuthenticationProcessingFilter {
    private final Logger LOG = LoggerFactory.getLogger(RestSecurityFilter.class);
    
    private AuthenticationManager authenticationManager;
    
    public RestSecurityFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }
    
    public RestSecurityFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        AuthenticationRequestWrapper request = new AuthenticationRequestWrapper(req);
    
        // Get authorization headers
        String signature = request.getHeader("Signature");
        String principal = request.getHeader("API-Key");
        String timestamp = request.getHeader("timestamp");
        if ((signature == null) || (principal == null) || (timestamp == null))
        unsuccessfulAuthentication(request, response, new BadHMACAuthRequestException("Authentication attempt failed! Request missing mandatory headers."));
    
    
        // a rest credential is composed by request data to sign and the signature
        RestCredentials credentials = new RestCredentials(HMACUtils.calculateContentToSign(request), signature);
    
        // Create an authentication token
        return new RestToken(principal, credentials, Long.parseLong(timestamp));
    }
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        LOG.debug("Filter request: " + req.toString());
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        chain.doFilter(request, response);
    
        Authentication authResult;
    
        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null)
                unsuccessfulAuthentication(request, response, new BadHMACAuthRequestException("Authentication attempt failed !"));
    
        } catch (InternalAuthenticationServiceException failed) {
            LOG.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);
        } catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);
        }
    }
    }
    

    Authentication provider:

    @Component
    public class RestAuthenticationProvider implements AuthenticationProvider {
    private final Logger LOG = LoggerFactory.getLogger(RestAuthenticationProvider.class);
    
    private ApiKeysService apiKeysService;
    
    @Autowired
    public void setApiKeysService(ApiKeysService apiKeysService) {
        this.apiKeysService = apiKeysService;
    }
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        RestToken restToken = (RestToken) authentication;
    
        // api key (aka username)
        String principal = restToken.getPrincipal();
    
        LOG.info("Authenticating api key: '" + principal + "'");
    
        // check request time, 60000 is one minute
        long interval = Clock.systemUTC().millis() - restToken.getTimestamp();
        if ((interval < 0) && (interval > 60000))
            throw new BadHMACAuthRequestException("Auth Failed: old request.");
    
        // hashed blob
        RestCredentials credentials = restToken.getCredentials();
    
        // get secret access key from api key
        ApiKey apiKey = apiKeysService.getKeyByName(principal).orElseThrow(() -> new NotFoundException("Key not found for: '" + principal + "'"));
        String secret = apiKey.getApiKey();
    
        // calculate the hmac of content with secret key
        String hmac = HMACUtils.calculateHMAC(secret, credentials.getRequestData());
        LOG.debug("Api Key '{}', calculated hmac '{}'");
    
        // check if signatures match
        if (!credentials.getSignature().equals(hmac)) {
            throw new BadHMACAuthRequestException("Auth Failed: invalid HMAC signature.");
        }
    
        return new RestToken(principal, credentials, restToken.getTimestamp(), apiKeysService.getPermissions(apiKey));
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return RestToken.class.equals(authentication);
    
    }
    }
    

    I don't know how to configure WebSecurityConfig to authenticate every request with my filter and Authentication Provider. I assume I need to create @Bean to initialize RestSecurityFilter. Also JavaDoc for AbstractAuthenticationProcessingFilter says I need to the authenticationManager property. I would appreciate working solution with custom filter, provider and token.