Spring Security OAuth2 - Need clarification and help to configure Implicit flow

1.2k views Asked by At

I am struggling to configure Spring Security OAuth2 to support implicit flow (I had no problems with password or authorization code).

These are the different endpoints:

Authorization server

http://localhost:8082/oauth/authorize
http://localhost:8082/oauth/token
...

Resource server

http://localhost:8081/users (protected resource)

Client

http://localhost:8080/api/users invokes http://localhost:8081/users initiating the OAuth2 dance.

What I see is:

  1. http://localhost:8080/api/users gets redirected to the authorization server with this in the URL: http://localhost:8082/oauth/authorize?client_id=themostuntrustedclientid&response_type=token&redirect_uri=http://localhost:8080/api/accessTokenExtractor

  2. I am prompted with the OAuth approval screen, where I grant all the scopes. Then the browser is redirected to the redirect_uri: http://localhost:8080/api/accessTokenExtractor with a fragment containing the access_token: http://localhost:8080/api/accessTokenExtractor#access_token=3e614eca-4abe-49a3-bbba-1b8eea05c147&token_type=bearer&expires_in=55&scope=read%20write

QUESTIONS:

a. HOW CAN I RESUME AUTOMATICALLY THE EXECUTION OF THE ORIGINAL REQUEST?

The spec defines this behaviour with the access_token as a fragment in the URL: since the fragments aren't sent directly to the servers, we have to use a web page script to extract it and send it to the client (my spring-mvc application). This implies setting a redirect_uri pointing at the script, instead of to the original request:

http://localhost:8080/api/accessTokenExtractor#access_token=3e614eca-4abe-49a3-bbba-1b8eea05c147&token_type=bearer&expires_in=55&scope=read%20write

The accessTokenExtractor web page sends the token to the client. The problem is I don't have the original call (http://localhost:8080/api/users) anymore...

b. Below you can see the client invocation:

    restTemplate.getOAuth2ClientContext().getAccessTokenRequest()
        .setAll(['client_id': 'themostuntrustedclientid',
                 'response_type': 'token', 
                 'redirect_uri': 'http://localhost:8080/api/accessTokenExtractor'])

    HttpHeaders headers = new HttpHeaders()
    ResponseEntity<List<String>> response = restTemplate.exchange('http://localhost:8081/users', HttpMethod.GET, null, new ParameterizedTypeReference<List<String>>(){}, [])
    response.getBody()

if I don't set manually the parameters client_id, response_type and redirect_uri (necessary for the UserRedirectRequiredException) the authorization server complains, it needs them. ARE WE EXPECTED TO SET THEM MANUALLY?

The strange thing is that they are available in ImplicitAccessorProvider.obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request):

ImplicitResourceDetails resource = (ImplicitResourceDetails) details;
    try {
        ...

resource contains all of them, however they are not copied to request.

If we compare with AuthorizationCodeAccessTokenProvider here the private method getRedirectForAuthorization() does it automatically...WHY THE DIFFERENCE?

CONFIGURATION:

Authorization Server config:

@EnableAuthorizationServer
@SpringBootApplication
class Oauth2AuthorizationServerApplication {

    static void main(String[] args) {
        SpringApplication.run Oauth2AuthorizationServerApplication, args
    }
}

@Configuration
class OAuth2Config extends AuthorizationServerConfigurerAdapter{

    @Autowired
    private AuthenticationManager authenticationManager

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager([])
        manager.createUser(new User("jose","mypassword", [new SimpleGrantedAuthority("ROLE_USER")]))
        manager.createUser(new User("themostuntrustedclientid","themostuntrustedclientsecret", [new SimpleGrantedAuthority("ROLE_USER")]))
        return manager
    }

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()

        //curl trustedclient:trustedclientsecret@localhost:8082/oauth/token -d grant_type=password -d username=user -d password=cec31d99-e5ee-4f1d-b9a3-8d16d0c6eeb5 -d scope=read
        .withClient("themostuntrustedclientid")
            .secret("themostuntrustedclientsecret")
            .authorizedGrantTypes("implicit")
            .authorities("ROLE_USER")
            .scopes("read", "write")
            .accessTokenValiditySeconds(60)

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(this.authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //security.checkTokenAccess('hasRole("ROLE_RESOURCE_PROVIDER")')
        security.checkTokenAccess('isAuthenticated()')
    }
}

resource server config and protected endpoint:

@EnableResourceServer
@SpringBootApplication
class Oauth2ResourceServerApplication {

    static void main(String[] args) {
        SpringApplication.run Oauth2ResourceServerApplication, args
    }
}

@Configuration
class OAuth2Config extends ResourceServerConfigurerAdapter{

    @Value('${security.oauth2.resource.token-info-uri}')
    private String checkTokenEndpointUrl

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            // Since we want the protected resources to be accessible in the UI as well we need
            // session creation to be allowed (it's disabled by default in 2.0.6)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .and()
            .requestMatchers().antMatchers("/users/**")
        .and()
            .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/users").access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.PUT, "/users/**").access("#oauth2.hasScope('write')")
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices()
        remoteTokenServices.setCheckTokenEndpointUrl(checkTokenEndpointUrl)
        remoteTokenServices.setClientId("usersResourceProvider")
        remoteTokenServices.setClientSecret("usersResourceProviderSecret")
        resources.tokenServices(remoteTokenServices)
    }
}

@RestController
class UsersRestController {

    private Set<String> users = ["jose", "ana"]

    @GetMapping("/users")
    def getUser(){
        return users
    }

    @PutMapping("/users/{user}")
    void postUser(@PathVariable String user){
        users.add(user)
    }

}

And this is the client config:

@EnableOAuth2Client
@SpringBootApplication
class SpringBootOauth2ClientApplication {

    static void main(String[] args) {
        SpringApplication.run SpringBootOauth2ClientApplication, args
    }
}

@Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth.eraseCredentials(false)
            .inMemoryAuthentication().withUser("jose").password("mypassword").roles('USER')
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        .authorizeRequests()
        .anyRequest().hasRole('USER')
        .and()
        .formLogin()
    }

}

@Configuration
class OAuth2Config {

    @Value('${oauth.resource:http://localhost:8082}')
    private String baseUrl

    @Value('${oauth.authorize:http://localhost:8082/oauth/authorize}')
    private String authorizeUrl

    @Value('${oauth.token:http://localhost:8082/oauth/token}')
    private String tokenUrl

    @Autowired
    private OAuth2ClientContext oauth2Context

    @Bean
    OAuth2ProtectedResourceDetails resource() {
        ImplicitResourceDetails resource = new ImplicitResourceDetails()
        resource.setAuthenticationScheme(AuthenticationScheme.header)
        resource.setAccessTokenUri(authorizeUrl)
        resource.setUserAuthorizationUri(authorizeUrl);
        resource.setClientId("themostuntrustedclientid")
        resource.setClientSecret("themostuntrustedclientsecret")
        resource.setScope(['read', 'write'])
        resource
    }

    @Bean
    OAuth2RestTemplate restTemplate() {
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resource(), oauth2Context)
        //restTemplate.setAuthenticator(new ApiConnectOAuth2RequestAuthenticator())
        restTemplate
    }
}

My client has the following controller that invokes a protected aouth2 endpoint from the resource server:

@RestController
class ClientRestController {

    @Autowired
    private OAuth2RestTemplate restTemplate

    def exceptionHandler(InsufficientScopeException ex){
        ex
    }

    @GetMapping("/home")
    def getHome(HttpSession session){
        session.getId()
    }

    @GetMapping("/users")
    def getUsers(HttpSession session){
        println 'Session id: '+ session.getId()

        //TODO Move to after authentication
        Authentication auth = SecurityContextHolder.getContext().getAuthentication()
        restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAll(['client_id': 'themostuntrustedclientid', 'response_type': 'token', 'redirect_uri': 'http://localhost:8080/api/users'])

        HttpHeaders headers = new HttpHeaders()
        ResponseEntity<List<String>> response = restTemplate.exchange('http://localhost:8081/users', HttpMethod.GET, null, new ParameterizedTypeReference<List<String>>(){}, [])


        response.getBody()
    }
}
0

There are 0 answers