FormAuthConfiguration.java

package cn.home1.oss.lib.security.starter;

import static cn.home1.oss.boot.autoconfigure.AppSecurity.ENABLED;
import static cn.home1.oss.boot.autoconfigure.AppType.MIXED;
import static cn.home1.oss.boot.autoconfigure.AppType.RESOURCE;
import static cn.home1.oss.boot.autoconfigure.AppType.RESTFUL;
import static cn.home1.oss.boot.autoconfigure.AppType.TEMPLATE;
import static cn.home1.oss.lib.errorhandle.api.ResolvedError.RESOLVED_ERROR_COOKIE;
import static cn.home1.oss.lib.security.api.GenericUser.GENERIC_USER_COOKIE;
import static cn.home1.oss.lib.security.api.GenericUser.GENERIC_USER_TOKEN;
import static cn.home1.oss.lib.security.internal.rest.RestfulAuthenticationEntryPoint.restfulEntryPoint;
import static cn.home1.oss.lib.security.internal.template.TemplateAuthenticationEntryPoint.templateEntryPoint;
import static cn.home1.oss.lib.security.internal.template.TemplateAuthenticationFailureHandler.templateFailureHandler;
import static cn.home1.oss.lib.security.internal.template.TemplateAuthenticationLogoutSuccessHandler.templateLogoutSuccessHandler;
import static cn.home1.oss.lib.security.internal.template.TemplateAuthenticationSuccessHandler.templateSuccessHandler;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.springframework.http.HttpMethod.GET;

import cn.home1.oss.boot.autoconfigure.AppProperties;
import cn.home1.oss.boot.autoconfigure.AppSecurityProperties;
import cn.home1.oss.boot.autoconfigure.AppType;
import cn.home1.oss.boot.autoconfigure.ConditionalOnAppSecurity;
import cn.home1.oss.boot.autoconfigure.ConditionalOnAppType;
import cn.home1.oss.boot.autoconfigure.OnAppTypeCondition;
import cn.home1.oss.lib.common.crypto.Cryptos;
import cn.home1.oss.lib.common.crypto.KeyExpression;
import cn.home1.oss.lib.common.crypto.Rsa;
import cn.home1.oss.lib.common.crypto.RsaKey;
import cn.home1.oss.lib.errorhandle.api.ResolvedError;
import cn.home1.oss.lib.errorhandle.internal.RestfulExceptionHandler;
import cn.home1.oss.lib.security.api.BaseUserDetailsAuthenticationProvider;
import cn.home1.oss.lib.security.api.GenericUser;
import cn.home1.oss.lib.security.crypto.ReentrantBCryptPasswordEncoder;
import cn.home1.oss.lib.security.internal.preauth.PreAuthTokenCookieClearingLogoutHandler;
import cn.home1.oss.lib.security.internal.rest.RestfulAuthenticationFailureHandler;
import cn.home1.oss.lib.security.internal.rest.RestfulAuthenticationSuccessHandler;
import cn.home1.oss.lib.security.internal.rest.RestfulLoginDisabledFilter;
import cn.home1.oss.lib.security.internal.rest.RestfulLoginPublicKeyFilter;
import cn.home1.oss.lib.security.internal.rest.RestfulLogoutDisabledFilter;
import cn.home1.oss.lib.security.internal.rest.RestfulLogoutSuccessHandler;
import cn.home1.oss.lib.webmvc.api.TypeSafeCookie;
import cn.home1.oss.lib.webmvc.api.TypeSafeToken;
import cn.home1.oss.lib.webmvc.internal.DefaultHttpEntityMethodProcessor;

import lombok.SneakyThrows;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.EncryptDefaultLoginPageConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
 * Created by zhanghaolun on 16/8/19.
 */
@Order(FormAuthConfiguration.ORDER_FORM_AUTH)
@Configuration
public class FormAuthConfiguration extends SecurityConfigurerAdapter<FormAuthConfiguration> {

  public static final int ORDER_FORM_AUTH = BasicAuthConfiguration.ORDER_BASIC_AUTH - 10;

  public static final String LOGIN_CIPHER = "loginCipher";
  public static final String FORM_AUTHENTICATION_ENTRYPOINT = "formAuthenticationEntryPoint";

  @Autowired
  private AppProperties appProperties;

  @Autowired
  private Environment environment;

  @Autowired
  private RestfulExceptionHandler exceptionHandler;

  @Qualifier(RESOLVED_ERROR_COOKIE)
  @Autowired
  private TypeSafeCookie<ResolvedError> resolvedErrorCookie;

  @Qualifier(GENERIC_USER_COOKIE)
  @Autowired(required = false)
  private TypeSafeCookie<GenericUser> genericUserCookie;

  @Qualifier(GENERIC_USER_TOKEN)
  @Autowired(required = false)
  private TypeSafeToken<GenericUser> genericUserToken;

  @Autowired
  private DefaultHttpEntityMethodProcessor httpEntityMethodProcessor;

  @Autowired
  private ServerProperties serverProperties;

  @Autowired(required = false)
  @SuppressWarnings("rawtypes")
  private BaseUserDetailsAuthenticationProvider userDetailsAuthenticationProvider;

  @SneakyThrows
  @Override
  public void configure(final HttpSecurity http) {
    final AuthenticationEntryPoint formAuthenticationEntryPoint = this.formAuthenticationEntryPoint();
    if (formAuthenticationEntryPoint != null) {
      // TODO accessDeniedHandler
      http.exceptionHandling().authenticationEntryPoint(formAuthenticationEntryPoint);
    }
    // TODO RememberMeAuthenticationFilter

    final AppType appType = this.appProperties.getType();
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();
    if (appSecurityProperties.getEnabled() && appType != RESOURCE) {
      checkState(this.userDetailsAuthenticationProvider != null, //
        "must define a bean that extends " //
          + BaseUserDetailsAuthenticationProvider.class.getName() //
          + " to enable form authentication");

      final KeyExpression loginKey = appSecurityProperties.getLoginKey();
      final String publicKey;
      if (loginKey.isPresent()) {
        final Rsa rsa = Cryptos.cipher(loginKey);
        this.userDetailsAuthenticationProvider.setCipher(rsa);
        publicKey = RsaKey.extractPublicKey(rsa.getEncryptor().getKey().getKeyExpression());
      } else {
        publicKey = "";
      }

      // see: http://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/guides/html5/form-javaconfig.html
      // see: org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
      // see: org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer
      // if loginPage or authenticationEntryPoint is set, there will not be loginPageGeneratingFilter
      final Boolean useDefaultLoginPage = appSecurityProperties.useDefaultLoginPage(this.environment);
      http.apply(new EncryptDefaultLoginPageConfigurer<>( //
        appSecurityProperties.getLoginPage(), //
        publicKey, //
        useDefaultLoginPage, //
        this.exceptionHandler.getExceptionResolver(), //
        this.resolvedErrorCookie //
      ));
      final FormLoginConfigurer<HttpSecurity> formLogin = http.formLogin();
      if (!useDefaultLoginPage) {
        formLogin.loginPage(appSecurityProperties.getLoginPage()); // this will disable default loginPage
        //.permitAll() // permitAll only works with HttpSecurity.authorizeRequests()
      }
      final String loginProcessingUrl = appSecurityProperties.getLoginProcessingUrl();
      final String logoutUrl = appSecurityProperties.getLogoutUrl();
      formLogin
        .loginProcessingUrl(loginProcessingUrl)
        .failureHandler(this.authenticationFailureHandler()) //
        .successHandler(this.authenticationSuccessHandler()) //
        .and() //
        .logout() //
        .logoutUrl(logoutUrl)//.permitAll() // permitAll only works with HttpSecurity.authorizeRequests()
        .addLogoutHandler(this.logoutHandler()) //
        .logoutSuccessHandler(this.logoutSuccessHandler());
    } else {
      http.formLogin().disable() //
        .logout().disable() //
      ;

      if (appType == MIXED || appType == RESTFUL) {
        http
          .addFilterBefore(this.loginDisabledFilter(), UsernamePasswordAuthenticationFilter.class) //
          .addFilterBefore(this.logoutDisabledFilter(), RestfulLoginDisabledFilter.class) //
        ;
      }
    }

    final RestfulLoginPublicKeyFilter restfulLoginPublicKeyFilter = this.restfulLoginPublicKeyFilter();
    if (restfulLoginPublicKeyFilter != null) {
      http.addFilterBefore(restfulLoginPublicKeyFilter, UsernamePasswordAuthenticationFilter.class);
    }
  }

  @Bean
  @ConditionalOnAppSecurity(ENABLED)
  @ConditionalOnAppType({MIXED, RESTFUL, TEMPLATE})
  public PreAuthTokenCookieClearingLogoutHandler logoutHandler() {
    return new PreAuthTokenCookieClearingLogoutHandler();
  }

  public LogoutSuccessHandler logoutSuccessHandler() {
    final LogoutSuccessHandler handler;
    if (this.appProperties.getType() == TEMPLATE) {
      handler = templateLogoutSuccessHandler();
    } else {
      handler = new RestfulLogoutSuccessHandler();
    }
    return handler;
  }

  public RestfulLoginPublicKeyFilter restfulLoginPublicKeyFilter() {
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();

    final RestfulLoginPublicKeyFilter filter;
    if (appSecurityProperties.getEnabled() && //
      OnAppTypeCondition.matches(this.appProperties.getType(), MIXED, RESTFUL) && //
      appSecurityProperties.getLoginKey().isPresent()) {
      final String loginPublicKeyUrl = appSecurityProperties.getLoginPublicKeyUrl();
      final KeyExpression publicKey = new RsaKey(appSecurityProperties.getLoginKey()).getPublicKey();
      filter = new RestfulLoginPublicKeyFilter(publicKey);
      filter.setEnvironment(this.environment);
      filter.setExceptionHandler(this.exceptionHandler);
      filter.setHttpEntityMethodProcessor(this.httpEntityMethodProcessor);
      filter.setRequestMatcher(new AntPathRequestMatcher(loginPublicKeyUrl, GET.name()));
    } else {
      filter = null;
    }
    return filter;
  }

  public RestfulLoginDisabledFilter loginDisabledFilter() {
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();

    final RestfulLoginDisabledFilter filter;
    if (!appSecurityProperties.getEnabled() && //
      OnAppTypeCondition.matches(this.appProperties.getType(), MIXED, RESTFUL)) {
      filter = new RestfulLoginDisabledFilter();
      filter.setEnvironment(this.environment);
      filter.setFilterProcessesUrl(appSecurityProperties.getLoginProcessingUrl());
      filter.setPostOnly(true);
      filter.setExceptionHandler(this.exceptionHandler);
    } else {
      filter = null;
    }
    return filter;
  }

  public RestfulLogoutDisabledFilter logoutDisabledFilter() {
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();

    final RestfulLogoutDisabledFilter filter;
    if (!appSecurityProperties.getEnabled() && //
      OnAppTypeCondition.matches(this.appProperties.getType(), MIXED, RESTFUL)) {

      filter = new RestfulLogoutDisabledFilter();
      filter.setEnvironment(this.environment);
      filter.setFilterProcessesUrl(appSecurityProperties.getLogoutUrl());
      filter.setExceptionHandler(this.exceptionHandler);
    } else {
      filter = null;
    }
    return filter;
  }

  @Bean
  @ConditionalOnAppSecurity(ENABLED)
  @ConditionalOnAppType({MIXED, RESTFUL, TEMPLATE})
  public AuthenticationFailureHandler authenticationFailureHandler() {
    final AppType appType = this.appProperties.getType();
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();
    final String authFailureHandler = appSecurityProperties.getAuthFailureHandler();
    final String loginPage = appSecurityProperties.getLoginPage();

    final AuthenticationFailureHandler failureHandler;
    if (isBlank(authFailureHandler)) {
      if (appType == TEMPLATE) {
        failureHandler = templateFailureHandler( //
          loginPage, this.exceptionHandler.getExceptionResolver(), this.resolvedErrorCookie);
      } else {
        failureHandler = RestfulAuthenticationFailureHandler.restfulFailureHandler(this.exceptionHandler);
      }
    } else {
      if ("restful".equalsIgnoreCase(authFailureHandler)) {
        failureHandler = RestfulAuthenticationFailureHandler.restfulFailureHandler(this.exceptionHandler);
      } else {
        failureHandler = templateFailureHandler( //
          loginPage, this.exceptionHandler.getExceptionResolver(), this.resolvedErrorCookie);
      }
    }

    return failureHandler;
  }

  @Bean
  @ConditionalOnAppSecurity(ENABLED)
  @ConditionalOnAppType({MIXED, RESTFUL, TEMPLATE})
  public AuthenticationSuccessHandler authenticationSuccessHandler() {
    final AppType appType = this.appProperties.getType();
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();
    final String authSucessHandler = appSecurityProperties.getAuthSucessHandler();

    final AuthenticationSuccessHandler successHandler;
    if (isBlank(authSucessHandler)) {
      if (appType == TEMPLATE) {
        successHandler = templateSuccessHandler("/"); // TODO redirectUrl
      } else {
        successHandler = RestfulAuthenticationSuccessHandler.restfulSuccessHandler( //
          this.genericUserCookie, //
          this.exceptionHandler, //
          this.httpEntityMethodProcessor, //
          this.genericUserToken //
        );
      }
    } else {
      if ("restful".equalsIgnoreCase(authSucessHandler)) {
        successHandler = RestfulAuthenticationSuccessHandler.restfulSuccessHandler( //
          this.genericUserCookie, //
          this.exceptionHandler, //
          this.httpEntityMethodProcessor, //
          this.genericUserToken //
        );
      } else {
        successHandler = templateSuccessHandler(authSucessHandler);
      }
    }


    return successHandler;
  }

  /**
   * always enabled.
   *
   * @return authenticationEntryPoint
   */
  @Bean(name = FORM_AUTHENTICATION_ENTRYPOINT)
  public AuthenticationEntryPoint formAuthenticationEntryPoint() {
    final AppType appType = this.appProperties.getType();
    final AppSecurityProperties appSecurityProperties = this.appProperties.getSecurity();
    final String authEntryPoint = appSecurityProperties.getAuthEntryPoint();
    final String loginPage = appSecurityProperties.getLoginPage();

    final AuthenticationEntryPoint entryPoint;
    if (isBlank(authEntryPoint)) {
      if (appType == TEMPLATE) {
        final Boolean useDefaultLoginPage = appSecurityProperties.useDefaultLoginPage(this.environment);
        if (useDefaultLoginPage) {
          entryPoint = null;
        } else {
          // this will disable default loginPage
          entryPoint = templateEntryPoint( //
            loginPage, this.exceptionHandler.getExceptionResolver(), this.resolvedErrorCookie);
        }
      } else {
        entryPoint = restfulEntryPoint(this.exceptionHandler);
      }
    } else {
      if ("401".equals(authEntryPoint)) {
        final String sessionCookieName = this.serverProperties.getSession().getCookie().getName();
        final String headerValue = "Session realm=\"" + //
          (isNotBlank(sessionCookieName) ? sessionCookieName : "JSESSIONID") + // TODO is this realm ok?
          "\"";
        entryPoint = new Http401AuthenticationEntryPoint(headerValue);
      } else if ("403".equals(authEntryPoint)) {
        entryPoint = new Http403ForbiddenEntryPoint();
      } else if ("loginPage".equalsIgnoreCase(authEntryPoint)) {
        entryPoint = templateEntryPoint( //
          loginPage, this.exceptionHandler.getExceptionResolver(), this.resolvedErrorCookie);
      } else {
        entryPoint = restfulEntryPoint(this.exceptionHandler);
      }
    }

    return entryPoint;
  }

  /**
   * always enabled.
   *
   * @return passwordEncoder
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    //return NoOpPasswordEncoder.getInstance();
    return new ReentrantBCryptPasswordEncoder();
  }
}