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.