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:
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
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()
}
}