0

When sending the following request:

curl -i -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/auth/authorize?client_id=ng-zero&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fcallback&response_type=code&scope=read_profile" -X POST -d "{ \"username\" : \"xxxx@yahoo.se\", \"password\" : \"xxxxxx\" }"

I get the error message: InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed

I'm trying to implement an OAuth2 provider using Spring Boot 2.0.4.RELEASE with spring-security-oauth2-autoconfigure 2.0.6.RELEASE with the authorization code OAuth2 grant type.

I understand the /oauth/authorize endpoint is a secured one and the user must be authenticated prior to sending a request to it.

Should my authorization server have a login controller or a login filter ?

As a side note, I wonder if I should use the authorization code grant type when my front-end is an Angular one.. maybe the password grant type is more appropriate ?

But how to carry the resulting "yes the user is successfully logged in" state to the authorization server in that above request ?

I explicitly open the /auth/login endpoint, I explicitly secure the /auth/token endpoint, and I'm not saying anything about the /auth/authorize endpoint.

Note that I remapped the two endpoints:

/oauth/authorize -> /auth/authorize
/oauth/token     -> /auth/token

I have the following security configuration for the authorization server:

@EnableWebSecurity
@ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "xxx.xxxxxxxxx.user.rest.service", "xxx.xxxxxxxxx.user.rest.filter" })
public class SecurityAuthorizationServerConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder());
    }

    @Autowired
    private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Override
    public void configure(WebSecurity webSecurity) throws Exception {
        webSecurity.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
        .csrf()
        .disable()
        .formLogin().disable()
        .httpBasic().disable()
        .logout().disable();

        http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http
        .authorizeRequests()
        .antMatchers(getUnsecuredPaths().toArray(new String[]{})).permitAll()
        .antMatchers(RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN).authenticated()
    }

    private List<String> getUnsecuredPaths() {
        List<String> unsecuredPaths = Arrays.asList(
            RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.LOGIN
        );
        return unsecuredPaths;
    }

}

And my authorization server is configured as:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    static final String CLIENT_ID = "ng-zero";
    static final String CLIENT_SECRET = "secret";
    static final String GRANT_TYPE_PASSWORD = "password";
    static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
    static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtProperties jwtProperties;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private TokenAuthenticationService tokenAuthenticationService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
        .withClient(CLIENT_ID)
        .secret(CLIENT_SECRET)
        .redirectUris("http://localhost:4200/callback")
        .authorizedGrantTypes(GRANT_TYPE_PASSWORD, GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN)
        .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
        .resourceIds("user-rest")
        .scopes("read_profile", "write_profile", "read_firstname")
        .accessTokenValiditySeconds(jwtProperties.getAccessTokenExpirationTime())
        .refreshTokenValiditySeconds(jwtProperties.getRefreshTokenExpirationTime());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
        .tokenKeyAccess("permitAll()")
        .checkTokenAccess("isAuthenticated()")
        .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
        .authenticationManager(authenticationManager)
        .tokenServices(tokenServices())
        .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
        .tokenEnhancer(jwtAccessTokenConverter())
        .accessTokenConverter(jwtAccessTokenConverter())
        .userDetailsService(userDetailsService);

        endpoints
        .pathMapping("/oauth/authorize", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.AUTHORIZE)
        .pathMapping("/oauth/token", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN);
    }

    class CustomTokenEnhancer extends JwtAccessTokenConverter {

        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            User user = (User) authentication.getPrincipal();
            Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
            info.put("email", user.getEmail());
            info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
            info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
            info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
            DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
            customAccessToken.setAdditionalInformation(info);
            customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
            return super.enhance(customAccessToken, authentication);
        }

    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new CustomTokenEnhancer();
        jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
        return jwtAccessTokenConverter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        return defaultTokenServices;
    }

}

UPDATE: I see at https://stackoverflow.com/questions/39424176/spring-security-oauth2-insufficientauthenticationexception that the /oauth/authorize endpoint should be secured by Spring Security with a redirect to a login page so as to authenticate the user before granting access to the /oauth/authorize endpoint. But what if the authorization server is stateless ? Can we have this authorization server as a API server ? How to "log in" the user then ? That is why I opened the /oauth/authorize endpoint in the first place. But having it opened, shows this User must be authenticated with Spring Security before authorization can be completed. message.

Stephane
  • 11,836
  • 25
  • 112
  • 175
  • I have not run your code but I think you should add AUTHORIZATION header to your request with the secret you provided in your configuration. – Nima Ajdari Nov 05 '18 at 05:53

1 Answers1

0

The issue was that I needed to secure the /oauth/authorize endpoint. So I did, and using the access token from the user login, I could then use that access token to send a request to the /oauth/authorize endpoint and in return receive the html form asking about the confirmation of the scope.

Stephane
  • 11,836
  • 25
  • 112
  • 175