spring-boot OAuth2 client configuration - spring-boot

I try to implement OAuth2 client using authorization-code grant flow by spring-boot.
But it does not work.
"http://external_server/oauth/authorize" was called, but no GET arguments added.
Does anyone know what is wrong in below configuration?
Auth provider is implemented by doorkeeper and it's already working.
so URL constants in WebSecurityConfiguration are correct.
#Configuration
#EnableWebMvcSecurity
#EnableOAuth2Client
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String AUTH_ENDPOINT = "http://external_server";
private static final String LOGIN_URL = AUTH_ENDPOINT + "/users/sign_in";
private static final String LOGOUT_URL = AUTH_ENDPOINT + "/sign_out";
private static final String AUTH_URL = AUTH_ENDPOINT + "/oauth/authorize";
private static final String ACCESS_TOKEN_URL = AUTH_ENDPOINT + "/oauth/token";
#Autowired OAuth2ClientContext oAuth2ClientContext;
/**
* for specific api
*/
#Bean public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* for accessing protected resource
*/
#Bean public OAuth2RestTemplate oAuth2RestTemplate() {
return new OAuth2RestTemplate(resource(), oAuth2ClientContext);
}
#Bean protected OAuth2ProtectedResourceDetails resource() {
AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
resource.setClientId("_xxx_");
resource.setClientSecret("_yyy_");
resource.setUserAuthorizationUri(AUTH_URL);
resource.setAccessTokenUri(ACCESS_TOKEN_URL);
return resource;
}
#Override public void configure(WebSecurity web) throws Exception {
web.debug(true).ignoring().antMatchers("/webjars/**", "/css/**");
}
#Override protected void configure(HttpSecurity http) throws Exception {
//#formatter:off
http.csrf().disable().authorizeRequests()
.antMatchers("/", "/callback")
.permitAll()
.anyRequest()
.authenticated();
http.formLogin()
.loginPage(AUTH_URL)
.loginProcessingUrl(LOGIN_URL);
http.httpBasic()
.disable();
//#formatter:on
}
}

By default only POST Method is enabled. You may need to include GET Method on AuthorizationConfig.
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
Will be like this:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
....
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints){
endpoints.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
}
On source code of Spring Oauth we have:
private Set<HttpMethod> allowedTokenEndpointRequestMethods() {
// HTTP POST should be the only allowed endpoint request method by default.
if (allowedTokenEndpointRequestMethods.isEmpty()) {
allowedTokenEndpointRequestMethods.add(HttpMethod.POST);
}
return allowedTokenEndpointRequestMethods;
}

Related

After Spring Boot 2 upgade authorization server returns "At least one redirect_uri must be registered with the client."

I upgraded our authorization server from Spring Boot 1.5.13.RELEASE to 2.1.3.RELEASE, and now I can authenticate, but I can no longer access the site. Here is the resulting URL and error after the POST to /login.
https://auth-service-test-examle.cfapps.io/oauth/authorize?client_id=proxy-service&redirect_uri=http://test.example.com/login&response_type=code&state=QihbF4
OAuth Error
error="invalid_request", error_description="At least one redirect_uri must be registered with the client."
To troubleshoot, I started a fresh project based on the Spring Security 5.1.4.RELEASE sample "oauth2authorizationserver." I layered on the features used in our Spring Boot 1.5.13 authorization server making sure the unit tests passed (except one test class). If I #Ignore the failing tests and deploy the code I get the problem described above.
The problem is reproducible in the AuthenticationTests.loginSucceeds() JUnit test that passed before the upgrade. It expects a 302, but now it gets a 403 because it goes to the root of the authentication server. I published the entire example on GitHub
spring-security-5-upgrade_sso-auth-server
Clone the project and run the unit tests and you will see the failures.
Here are some of the key settings that can be found in the project on GitHub.
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
private final String privateKey;
private final String publicKey;
private final AuthClientDetailsService authClientDetailsService;
private final AuthenticationManager authenticationManager;
private final AuthUserDetailsService authUserDetailsService;
#Autowired
public AuthServerConfig(
#Value("${keyPair.privateKey}") final String privateKey,
#Value("${keyPair.publicKey}") final String publicKey,
final AuthClientDetailsService authClientDetailsService,
final AuthUserDetailsService authUserDetailsService,
final AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.privateKey = privateKey;
this.publicKey = publicKey;
this.authClientDetailsService = authClientDetailsService;
this.authUserDetailsService = authUserDetailsService;
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(authClientDetailsService);
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
.userDetailsService(authUserDetailsService)
.tokenStore(tokenStore());
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(privateKey);
converter.setVerifierKey(publicKey);
return converter;
}
}
public class GlobalAuthenticationConfig extends GlobalAuthenticationConfigurerAdapter {
private final AuthUserDetailsService authUserDetailsService;
#Autowired
public GlobalAuthenticationConfig(final AuthUserDetailsService authUserDetailsService) {
this.authUserDetailsService = authUserDetailsService;
}
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(authUserDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
#Configuration
#Order(-20)
protected class LoginConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.requestMatchers().antMatchers(LOGIN, "/oauth/authorize", "/oauth/confirm_access")
.and()
.logout().permitAll()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().loginPage(LOGIN).permitAll();
// #formatter:on
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManager);
}
}
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final AuthUserDetailsService authUserDetailsService;
#Autowired
public WebSecurityConfig(AuthUserDetailsService authUserDetailsService) {
this.authUserDetailsService = authUserDetailsService;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(authUserDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
What else needs to be done in Spring Boot 2.1.3.RELEASE to redirect the user back to the original webpage?
It's important that OAuth 2.0 clients register a redirect_uri with Authorization Servers as an Open Redirector mitigation. As such, Spring Boot 2.1.x has this as its default behavior, which is why you're seeing the error.
You can do one of two things:
Add redirect_uris, one for each client
Ideally, you'd update your clients to each have a registered redirect_uri, which would likely be retrieved in an implementation of ClientDetailsService:
public class MyClientDetailsService implements ClientDetailsService {
private final MyRespository myRepository;
public ClientDetails loadClientByClientId(String clientId) {
return new MyClientDetails(this.myRepository.getMyDomainObject(clientId));
}
private static class MyClientDetails extends MyDomainObject implements ClientDetails {
private final MyDomainObject mine;
public MyClientDetails(MyDomainObject delegate) {
this.delegate = delegate;
}
// implement ClientDetails methods, delegating to your domain object
public Set<String> getRegisteredRedirectUri() {
return this.delegate.getRedirectUris();
}
}
}
This setup with the private subclass - while not necessary - is nice because it doesn't tie the domain object directly to Spring Security.
Add a custom RedirectResolver
Or, you can customize the RedirectResolver, though this wouldn't secure against Open Redirects, which was the original reason for the change.
public MyRedirectResolver implements RedirectResolver {
private final RedirectResolver delegate = new DefaultRedirectResolver();
public String resolveRedirect(String redirectUri, ClientDetails clientDetails) {
try {
return this.delegate.resolveRedirect(redirectUri, clientDetails);
} catch ( InvalidRequestException ire ) {
// do custom resolution
}
}
}

springboot oauth how to validate access_token

Hello everyone hope you doing well,
i have problem using open authentication in spring boot, when accessing page rest with postman is not even using param access token it still show the result, this my code please help???
Authorization Server Config class:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends
AuthorizationServerConfigurerAdapter{
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private TokenStore tokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler);
endpoints.authenticationManager(authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// TODO Auto-generated method stub
clients.inMemory()
.withClient("admin").secret("123")
.scopes("read","write")
.authorizedGrantTypes("password","refresh_token")
.accessTokenValiditySeconds(5*60)
.refreshTokenValiditySeconds(10*60);
}
}
Resource Server Config
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
#Override
public void configure(HttpSecurity http)throws Exception{
http
.anonymous().disable()
.authorizeRequests().antMatchers("/api/**") /** this
.authenticated()
.and()
.exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
}
Security Config
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private SecurityUtility hash;
#Autowired
private ClientDetailsService clientDetailsService;
private static final String[] PUBLIC_MATCHERS = { "/", "/css/**", "/image/**", "/js/**", "/newUser",
"/forgetPassword", "/login", "/logout", "/fonts/**", "/signUp", "/register", "/sendEmail", "/logout", "/tes","/oauth2/**","/api/**",
"/admin/tes","/SpringSecurityOAuth2Example/**",
"/admin/tes2" };
private static final String[] ADMIN_MATCHERS = { "/admin", "/admin/**" };
private static final String[] OAUTH2_PAGE = { "/oauth/**", "/api/**" };
private final String USERS_QUERY = "select username, password, is_enabled from user where username=?";
private final String ROLES_QUERY = "select u.username, u.is_enabled, r.name as authority from user u "
+ "inner join user_role ur on (u.id = ur.user_id) " + "inner join role r on (ur.role_id = r.roleid) "
+ "where username=?";
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(PUBLIC_MATCHERS).permitAll().anyRequest().authenticated().and().formLogin()
.loginPage("/login").loginProcessingUrl("/app-login").usernameParameter("app_username")
.passwordParameter("app_password").defaultSuccessUrl("/myAccount").permitAll()
.and().logout().logoutSuccessUrl("/login")
.permitAll();
http.authorizeRequests().antMatchers(ADMIN_MATCHERS).hasRole("ADMIN");
// http.csrf().disable();
http.csrf().ignoringAntMatchers(OAUTH2_PAGE);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// temporary
// auth.inMemoryAuthentication().withUser("admin").password("admin").roles("test");
auth.jdbcAuthentication().usersByUsernameQuery(USERS_QUERY).authoritiesByUsernameQuery(ROLES_QUERY)
.dataSource(dataSource).passwordEncoder(hash.passwordEncoder());
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
}
Auth Controller
#RestController
#EnableResourceServer
public class AuthController {
#GetMapping("/api/demo1")
public String apiTes() {
System.out.println("sysout mas");
return "return result";
}
}
solved guys, it because i was using springboot 1.5.10 so i have to add
security.oauth2.resource.filter-order=3
to spring application.properties

How to catch and handle InvalidGrantException (User is disabled)?

I noticed that my ResponseEntityExceptionHandler does not work with exceptions thrown by Spring Security in my Spring Boot application.
However, I need a way to catch InvalidGrantException which seems to get thrown when a user account is still disabled.
The use case is simple: If a user is currently disabled, I want to throw an appropriate error to my client s.t. it can display a message accordingly.
Right now the response by Spring Security is
{
error: "invalid_grant",
error_description: "User is disabled"
}
I saw this question but for some reason my AuthFailureHandler is not getting invoked:
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// Never reached ..
System.out.println("Hello World!");
}
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(customAuthEntryPoint());;
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
Any idea what I am missing?
Configuration Code
Here is my OAuth2Configuration which also contains a ResourceServerConfiguraerAdapter which is supposed to handle the exception
#Configuration
#EnableAuthorizationServer
#EnableResourceServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
private final TokenStore tokenStore;
private final JwtAccessTokenConverter accessTokenConverter;
private final AuthenticationManager authenticationManager;
#Autowired
public OAuth2Configuration(TokenStore tokenStore, JwtAccessTokenConverter accessTokenConverter, AuthenticationManager authenticationManager) {
this.tokenStore = tokenStore;
this.accessTokenConverter = accessTokenConverter;
this.authenticationManager = authenticationManager;
}
#Value("${security.jwt.client-id}")
private String clientId;
#Value("${security.jwt.client-secret}")
private String clientSecret;
#Value("${security.jwt.scope-read}")
private String scopeRead;
#Value("${security.jwt.scope-write}")
private String scopeWrite;
#Value("${security.jwt.resource-ids}")
private String resourceIds;
private final static String WEBHOOK_ENDPOINTS = "/r/api/*/webhooks/**";
private final static String PUBLIC_ENDPOINTS = "/r/api/*/public/**";
#Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds)
.accessTokenValiditySeconds(60*60*24*7 * 2) // Access tokens last two weeks
.refreshTokenValiditySeconds(60*60*24*7 * 12) // Refresh tokens last 12 weeks
//.accessTokenValiditySeconds(5)
//.refreshTokenValiditySeconds(10)
.authorizedGrantTypes("password", "refresh_token"); //, "client_credentials");
}
/**
* Since there are currently multiply clients, we map the OAuth2 endpoints under /api
* to avoid conflicts with #{#link ForwardController}
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
// Create path mappings to avoid conflicts with forwarding controller
.pathMapping("/oauth/authorize", "/api/v1/oauth/authorize")
.pathMapping("/oauth/check_token", "/api/v1/oauth/check_token")
.pathMapping("/oauth/confirm_access", "/api/v1/oauth/confirm_access")
.pathMapping("/oauth/error", "/api/v1/oauth/error")
.pathMapping("/oauth/token", "/api/v1/oauth/token")
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.reuseRefreshTokens(false)
.authenticationManager(authenticationManager);
}
/**
* Forwarding controller.
*
* This controller manages forwarding in particular for the static web clients. Since there are multiple
* clients, this controller will map <i>any</i> GET request to the root /* to one of the clients.
*
* If no match was found, the default redirect goes to /web/index.html
*
*/
#Controller
public class ForwardController {
#RequestMapping(value = "/sitemap.xml", method = RequestMethod.GET)
public String redirectSitemapXml(HttpServletRequest request) {
return "forward:/a/web/assets/sitemap.xml";
}
#RequestMapping(value = "/robots.txt", method = RequestMethod.GET)
public String redirectRobotTxt(HttpServletRequest request) {
return "forward:/a/web/assets/robots.txt";
}
#RequestMapping(value = "/*", method = RequestMethod.GET)
public String redirectRoot(HttpServletRequest request) {
return "forward:/a/web/index.html";
}
#RequestMapping(value = "/a/**/{path:[^.]*}", method = RequestMethod.GET)
public String redirectClients(HttpServletRequest request) {
String requestURI = request.getRequestURI();
if (requestURI.startsWith("/a/admin/")) {
return "forward:/a/admin/index.html";
}
if (requestURI.startsWith("/a/swagger/")) {
return "forward:/a/swagger/swagger-ui.html#/";
}
return "forward:/a/web/index.html";
}
}
#Configuration
#EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.requiresChannel()
/* Require HTTPS evereywhere*/
.antMatchers("/**")
.requiresSecure()
.and()
.exceptionHandling()
.and()
/* Permit all requests towards the public api as well as webhook endpoints. */
.authorizeRequests()
.antMatchers(PUBLIC_ENDPOINTS, WEBHOOK_ENDPOINTS)
.permitAll()
/* Required for ForwardController */
.antMatchers(HttpMethod.GET, "/*")
.permitAll()
.antMatchers("/r/api/**")
.authenticated();
// #formatter:on
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(resourceIds)
.authenticationEntryPoint(customAuthEntryPoint());
}
#Component
public class AuthFailureHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
System.out.println("Hello");
// FIXME We need to return HTTP 401 (s.t. the client knows what's going on) but for some reason it's not working as intended!
throw authException;
}
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
}
In addition, here is the WebSecurityConfigurerAdapter although I do not think this plays a role here:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${security.signing-key}")
private String signingKey;
private final UserDetailsService userDetailsService;
private final DataSource dataSource;
#Autowired
public WebSecurityConfig(#Qualifier("appUserDetailsService") UserDetailsService userDetailsService, DataSource dataSource) {
this.userDetailsService = userDetailsService;
this.dataSource = dataSource;
}
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Override
public void configure(WebSecurity web) throws Exception {
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
/**
* Using {#link JwtTokenStore} for JWT access tokens.
* #return
*/
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* Provide {#link DefaultTokenServices} using the {#link JwtTokenStore}.
* #return
*/
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
/**
* We provide the AuthenticationManagerBuilder using our {#link UserDetailsService} and the {#link BCryptPasswordEncoder}.
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.authenticationProvider(daoAuthenticationProvider());
}
/**
* Using {#link BCryptPasswordEncoder} for user-password encryption.
* #return
*/
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Provide {#link DaoAuthenticationProvider} for password encoding and set the {#link UserDetailsService}.
* #return
*/
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(this.userDetailsService);
return daoAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requiresChannel()
.requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
}
}
Create a custom class that extends ResponseEntityExceptionHandler, annotate it with #ControllerAdvice and handle OAuth2Exception (base class of InvalidGrantException).
#ExceptionHandler({OAuth2Exception.class})
public ResponseEntity<Object> handleOAuth2Exception(OAuth2Exception exception, WebRequest request) {
LOGGER.debug("OAuth failed on request processing", exception);
return this.handleExceptionInternal(exception, ErrorOutputDto.create(exception.getOAuth2ErrorCode(), exception.getMessage()), new HttpHeaders(), HttpStatus.valueOf(exception.getHttpErrorCode()), request);
}
Use HandlerExceptionResolverComposite to composite all exception resolvers in the system into one exception resolver. This overrides the corresponding bean defined in
WebMvcConfigurationSupport class. Add list of exceptionResolvers, like DefaultErrorAttributes and ExceptionHandlerExceptionResolver (this enables AOP based exceptions handling by involving classes with #ControllerAdvice annotation such as custom class that we created that extends ResponseEntityExceptionHandler.
<bean id="handlerExceptionResolver" class="org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
<property name="exceptionResolvers">
<list>
<bean class="org.springframework.boot.web.servlet.error.DefaultErrorAttributes"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
<property name="messageConverters">
<list>
<ref bean="jackson2HttpMessageConverter" />
</list>
</property>
</bean>
</list>
</property>
Have you tried if defining a #ControllerAdvice specifying your InvalidGrantException works?
#ControllerAdvice
#ResponseBody
public class GlobalExceptionHandler {
#ExceptionHandler(InvalidGrantException.class)
public ResponseEntity<CustomErrorMessageTO> handleInvalidGrant(
InvalidGrantException invalidGrantException) {
CustomErrorMessageTO customErrorMessageTO = new CustomErrorMessageTO("Not granted or whatsoever");
return new ResponseEntity<>(customErrorMessageTO, HttpStatus.UNAUTHORIZED);
}
}
class CustomErrorMessageTO {
private String message;
public CustomErrorMessageTO(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

Authenticate and get to Reddit resource

I wanted to call https://oauth.reddit.com/api/v1/me endpoint, so I created follwing REST controller:
#RestController
#RequestMapping("/reddit")
public class RedditController {
#Autowired
private OAuth2RestTemplate redditRestTemplate;
#Value("${secured.service.url:https://oauth.reddit.com/api/v1/me}")
private String endpoint;
#RequestMapping(value = "/message", method = RequestMethod.GET)
public String getMessageFromSecuredService(){
ResponseEntity<String> entity = redditRestTemplate.getForEntity(endpoint, String.class);
return entity.getBody();
}
}
To configure authentication I created following configuration:
#Configuration
#EnableOAuth2Client
#EnableWebSecurity
public class KeycloakClientCredentialsConfig extends WebSecurityConfigurerAdapter {
//...
#Override
public void configure(final WebSecurity web) throws Exception {
web.ignoring().antMatchers("/**");
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
}
#Bean
public OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("reddit");
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("identity", "edit", "flair", "history", "modconfig", "modflair", "modlog", "modposts", "modwiki", "mysubreddits", "privatemessages", "read", "report", "save", "submit", "subscribe", "vote", "wikiedit", "wikiread"));
details.setPreEstablishedRedirectUri("http://localhost:8080");
details.setUseCurrentUri(false);
return details;
}
#Bean
public OAuth2RestTemplate createRestTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails(), clientContext);
}
}
However each time I am not getting JSON result, but HTML page so it seems that authentication didn't work.
Do you know if my configuration is not set correctly?
Maybe my REST template should be built on configuration for invoking refresh token endpoint instead of authorize endpoint?

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

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

Resources