SpringBoot 2, OAUTH 2 and client_credentials in POST parameters
Rédigé par gorki Aucun commentaireProblem :
Edited : due to a bug, see also (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)
I try to implements client credentials OAUTH flow for a server which is resource and authentication server. (Goody summary here : https://www.slideshare.net/halyph/oauth2-and-spring-security)
There is plenty of posts on how to make an oauth 2 authentication server, (like authentication code flow less with client_credentials mode...
And in these examples :
- they often use inMemory() or jdbc() client details service (built-in easy to do for example : Java in use, Understanding spring-security-oauth2 @EnableAuthorizationServer)
- or use WebSecurity (example on medium)
- basic authentication (example here : Spring Boot + Oauth2 client credentials)
But if I easily found how to perform this kind of request : POST + BasicAuth(client_id/client_secret), I wasn't able to do :
POST + optional BasicAuth + client_id/client_secret in post parameters
Solution :
I activate DEBUG on spring security and see that filter chains do not have UsernamePasswordAuthenticationFilter, I plan to add the filter in websecurity as in some examples but it was not working.
After a lot of unsuccessful tries, I finally understand that I have to add a kind of UsernamePasswordAuthenticationFilter on OAUTH2 filter chain and not on the others (yes, there is multiple security filter chain....)
Going back to origins, I check AuthorizationServerConfigurerAdapter
override method and ... eurêka !
As simple as add a
ClientCredentialsTokenEndpointFilter
with
security.allowFormAuthenticationForClients
()
method... So simple, so hard to find.
But my AuthorizationServer
is now :
Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)
And a simple (for now) client detail service :
package com.example;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import java.util.Arrays;
public class MyClientDetailsService implements ClientDetailsService {
private static final String CLIENT_CREDENTIALS = "client_credentials";
private static final String REFRESH_TOKEN = "refresh_token";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final int VALID_FOREVER = -1;
private static final String CLIENT_ID = "my-client";
// encoding method prefix is required for DelegatingPasswordEncoder which is default since Spring Security 5.0.0.RC1
// you can use one of bcrypt/noop/pbkdf2/scrypt/sha256
// you can change default behaviour by providing a bean with the encoder you want
// more: https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-encoding
static final String CLIENT_SECRET = "{noop}my-secret";
@Override
public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
if (s.equals(CLIENT_ID)) {
BaseClientDetails client = new BaseClientDetails();
client.setClientId(s);
client.setClientSecret(CLIENT_SECRET);
client.setAuthorizedGrantTypes(Arrays.asList(CLIENT_CREDENTIALS, REFRESH_TOKEN));
client.setScope(Arrays.asList(SCOPE_READ, SCOPE_WRITE, TRUST));
client.setAccessTokenValiditySeconds(VALID_FOREVER);
client.setRefreshTokenValiditySeconds(VALID_FOREVER);
return client;
}
return null;
}
}
And my resource server :
Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)
The CORS filter from (https://www.hoab.fr/springboot-2-oauth-2-options-and-cors) :
package com.example;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {
public SimpleCorsFilter() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
And a protected resource :
package com.example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/metrics")
@RestController
public class Metrics {
@GetMapping
String getToken() {
return "OK";
}
}