/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package grails.plugin.springsecurity.rest

import grails.plugin.springsecurity.rest.authentication.RestAuthenticationEventPublisher
import grails.plugin.springsecurity.rest.token.AccessToken
import grails.plugin.springsecurity.rest.token.reader.TokenReader
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.web.filter.GenericFilterBean

import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse

/**
 * This filter starts the token validation flow. It extracts the token from the configured header name, and pass it to
 * the {@link RestAuthenticationProvider}.
 *
 * This filter, when applied, is incompatible with traditional browser-based Spring Security Core redirects. Users have
 * to make sure it's applied only to REST endpoint URL's.
 *
 * If the authentication is successful, the result is stored in the security context and the response is generated by the
 * {@link AuthenticationSuccessHandler}. Otherwise, an {@link AuthenticationFailureHandler} is called.
 */
@Slf4j
@CompileStatic
class RestTokenValidationFilter extends GenericFilterBean {

    String headerName

    RestAuthenticationProvider restAuthenticationProvider

    AuthenticationSuccessHandler authenticationSuccessHandler
    AuthenticationFailureHandler authenticationFailureHandler
    RestAuthenticationEventPublisher authenticationEventPublisher

    SpringSecurityRestFilterRequestMatcher requestMatcher

    TokenReader tokenReader
    String validationEndpointUrl
    Boolean active

    Boolean enableAnonymousAccess

    @Override
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = request as HttpServletRequest
        HttpServletResponse httpResponse = response as HttpServletResponse
        AccessToken accessToken

        try {
            accessToken = tokenReader.findToken(httpRequest)
            if (accessToken) {
                log.debug "Token found: ${accessToken.accessToken}"

                log.debug "Trying to authenticate the token"
                accessToken = restAuthenticationProvider.authenticate(accessToken) as AccessToken

                if (accessToken.authenticated) {
                    log.debug "Token authenticated. Storing the authentication result in the security context"
                    log.debug "Authentication result: ${accessToken}"
                    SecurityContextHolder.context.setAuthentication(accessToken)

                    authenticationEventPublisher.publishAuthenticationSuccess(accessToken)

                    processFilterChain(request, response, chain, accessToken)
                }

            } else {
                log.debug "Token not found"
                processFilterChain(request, response, chain, accessToken)
            }
        } catch (AuthenticationException ae) {
            log.debug "Authentication failed: ${ae.message}"
            authenticationEventPublisher.publishAuthenticationFailure(ae, accessToken)
            authenticationFailureHandler.onAuthenticationFailure(httpRequest, httpResponse, ae)
        }

    }

    @CompileDynamic
    private void processFilterChain(ServletRequest request, ServletResponse response, FilterChain chain, AccessToken authenticationResult) {
        HttpServletRequest httpRequest = request as HttpServletRequest
        HttpServletResponse httpResponse = response as HttpServletResponse

        if (!active) {
            log.debug "Token validation is disabled. Continuing the filter chain"
            chain.doFilter(request, response)
            return
        }

        if (authenticationResult?.accessToken) {
            if (requestMatcher.matches(httpRequest)) {
                log.debug "Validation endpoint called. Generating response."
                authenticationSuccessHandler.onAuthenticationSuccess(httpRequest, httpResponse, authenticationResult)
            } else {
                log.debug "Continuing the filter chain"
                chain.doFilter(request, response)
            }
        } else {
            log.debug "Request does not contain any token. Letting it continue through the filter chain"
            chain.doFilter(request, response)
        }

    }
}
