I am starting with Spring OAuth2. I would like to send the username and password to /oauth/token endpoint in POST body in application/json format.
curl -X POST -H "Authorization: Basic YWNtZTphY21lc2VjcmV0" -H "Content-Type: application/json" -d '{
"username": "user",
"password": "password",
"grant_type": "password"
}' "http://localhost:9999/api/oauth/token"
Is that possible?
Could you please give me an advice?
Solution (not sure if correct, but it seam that it is working):
Resource Server Configuration:
@Configuration
public class ServerEndpointsConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
JsonToUrlEncodedAuthenticationFilter jsonFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(jsonFilter, ChannelProcessingFilter.class)
.csrf().and().httpBasic().disable()
.authorizeRequests()
.antMatchers("/test").permitAll()
.antMatchers("/secured").authenticated();
}
}
Filter:
@Component
@Order(value = Integer.MIN_VALUE)
public class JsonToUrlEncodedAuthenticationFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (Objects.equals(request.getContentType(), "application/json") && Objects.equals(((RequestFacade) request).getServletPath(), "/oauth/token")) {
InputStream is = request.getInputStream();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[16384];
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
byte[] json = buffer.toByteArray();
HashMap<String, String> result = new ObjectMapper().readValue(json, HashMap.class);
HashMap<String, String[]> r = new HashMap<>();
for (String key : result.keySet()) {
String[] val = new String[1];
val[0] = result.get(key);
r.put(key, val);
}
String[] val = new String[1];
val[0] = ((RequestFacade) request).getMethod();
r.put("_method", val);
HttpServletRequest s = new MyServletRequestWrapper(((HttpServletRequest) request), r);
chain.doFilter(s, response);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
Request Wrapper:
public class MyServletRequestWrapper extends HttpServletRequestWrapper {
private final HashMap<String, String[]> params;
public MyServletRequestWrapper(HttpServletRequest request, HashMap<String, String[]> params) {
super(request);
this.params = params;
}
@Override
public String getParameter(String name) {
if (this.params.containsKey(name)) {
return this.params.get(name)[0];
}
return "";
}
@Override
public Map<String, String[]> getParameterMap() {
return this.params;
}
@Override
public Enumeration<String> getParameterNames() {
return new Enumerator<>(params.keySet());
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
Authorization Server Configuration (disable Basic Auth for /oauth/token endpoint:
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
...
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients(); // Disable /oauth/token Http Basic Auth
}
...
}
From the OAuth 2 specification,
The client makes a request to the token endpoint by sending the
following parameters using the "application/x-www-form-urlencoded"
Access token request should use application/x-www-form-urlencoded
.
In Spring security, the Resource Owner Password Credentials Grant Flow is handled by ResourceOwnerPasswordTokenGranter#getOAuth2Authentication
in Spring Security:
protected OAuth2Authentication getOAuth2Authentication(AuthorizationRequest clientToken) {
Map parameters = clientToken.getAuthorizationParameters();
String username = (String)parameters.get("username");
String password = (String)parameters.get("password");
UsernamePasswordAuthenticationToken userAuth = new UsernamePasswordAuthenticationToken(username, password);
You can send username
and password
to request parameter.
If you really need to use JSON, there is a workaround. As you can see, username
and password
is retrieved from request parameter. Therefore, it will work if you pass them from JSON body into the request parameter.
The idea is like follows:
- Create a custom spring security filter.
- In your custom filter, create a class to subclass
HttpRequestWrapper
. The class allow you to wrap the original request and get parameters from JSON. - In your subclass of
HttpRequestWrapper
, parse your JSON in request body to getusername
,password
andgrant_type
, and put them with the original request parameter into a newHashMap
. Then, override method ofgetParameterValues
,getParameter
,getParameterNames
andgetParameterMap
to return values from that newHashMap
- Pass your wrapped request into the filter chain.
- Configure your custom filter in your Spring Security Config.
Hope this can help
Also you can modify @jakub-kopřiva solution to support http basic auth for oauth.
Resource Server Configuration:
@Configuration
public class ServerEndpointsConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
JsonToUrlEncodedAuthenticationFilter jsonFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(jsonFilter, BasicAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers("/test").permitAll()
.antMatchers("/secured").authenticated();
}
}
Filter with internal RequestWrapper
@Component
public class JsonToUrlEncodedAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (Objects.equals(request.getServletPath(), "/oauth/token") && Objects.equals(request.getContentType(), "application/json")) {
byte[] json = ByteStreams.toByteArray(request.getInputStream());
Map<String, String> jsonMap = new ObjectMapper().readValue(json, Map.class);;
Map<String, String[]> parameters =
jsonMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new String[]{e.getValue()})
);
HttpServletRequest requestWrapper = new RequestWrapper(request, parameters);
filterChain.doFilter(requestWrapper, response);
} else {
filterChain.doFilter(request, response);
}
}
private class RequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> params;
RequestWrapper(HttpServletRequest request, Map<String, String[]> params) {
super(request);
this.params = params;
}
@Override
public String getParameter(String name) {
if (this.params.containsKey(name)) {
return this.params.get(name)[0];
}
return "";
}
@Override
public Map<String, String[]> getParameterMap() {
return this.params;
}
@Override
public Enumeration<String> getParameterNames() {
return new Enumerator<>(params.keySet());
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
}
And also you need to allow x-www-form-urlencoded authentication
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
...
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
}
...
}
With this approach you can still use basic auth for oauth token and request token with json like this:
Header:
Authorization: Basic bG9yaXpvbfgzaWNwYQ==
Body:
{
"grant_type": "password",
"username": "admin",
"password": "1234"
}
With Spring Security 5 I only had to add .allowFormAuthenticationForClients() + the JsontoUrlEncodedAuthenticationFilter noted in the other answer to get it to accept json in addition to x-form post data. There was no need to register the resource server or anything.
You can modify @jakub-kopřiva solution to implement only authorization server with below code.
@Configuration
@Order(Integer.MIN_VALUE)
public class AuthorizationServerSecurityConfiguration
extends org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration {
@Autowired
JsonToUrlEncodedAuthenticationFilter jsonFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.addFilterBefore(jsonFilter, ChannelProcessingFilter.class);
super.configure(httpSecurity);
}
}
Hello based on @Jakub Kopřiva answer I have made improvements in order to create working integration tests.
Just so you know, Catalina RequestFacade throws an error in Junit and MockHttpServletRequest, used by mockmvc, does not contain a field "request" as I expect in the filter (therefore throwning NoSuchFieldException when using getDeclaredField()):
Field f = request.getClass().getDeclaredField("request");
This is why I used "Rest Assured". However at this point I ran into another issue which is that for whatever reason the content-type from 'application/json' is overwritten into 'application/json; charset=utf8' even though I use MediaType.APPLICATION_JSON_VALUE
. However, the condition looks for something like 'application/json;charset=UTF-8' which lies behind MediaType.APPLICATION_JSON_UTF8_VALUE
, and in conclusion this will always be false.
Therefore I behaved as I used to do when I coded in PHP and I have normalized the strings (all characters are lowercase, no spaces).
After this the integration test finally passes.
---- JsonToUrlEncodedAuthenticationFilter.java
package com.example.springdemo.configs;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.catalina.connector.Request;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.web.savedrequest.Enumerator;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
@Component
@Order(value = Integer.MIN_VALUE)
public class JsonToUrlEncodedAuthenticationFilter implements Filter {
private final ObjectMapper mapper;
public JsonToUrlEncodedAuthenticationFilter(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
@SneakyThrows
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Field f = request.getClass().getDeclaredField("request");
f.setAccessible(true);
Request realRequest = (Request) f.get(request);
//Request content type without spaces (inner spaces matter)
//trim deletes spaces only at the beginning and at the end of the string
String contentType = realRequest.getContentType().toLowerCase().chars()
.mapToObj(c -> String.valueOf((char) c))
.filter(x->!x.equals(" "))
.collect(Collectors.joining());
if ((contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE.toLowerCase())||
contentType.equals(MediaType.APPLICATION_JSON_VALUE.toLowerCase()))
&& Objects.equals((realRequest).getServletPath(), "/oauth/token")) {
InputStream is = realRequest.getInputStream();
try (BufferedReader br = new BufferedReader(new InputStreamReader(is), 16384)) {
String json = br.lines()
.collect(Collectors.joining(System.lineSeparator()));
HashMap<String, String> result = mapper.readValue(json, HashMap.class);
HashMap<String, String[]> r = new HashMap<>();
for (String key : result.keySet()) {
String[] val = new String[1];
val[0] = result.get(key);
r.put(key, val);
}
String[] val = new String[1];
val[0] = (realRequest).getMethod();
r.put("_method", val);
HttpServletRequest s = new MyServletRequestWrapper(((HttpServletRequest) request), r);
chain.doFilter(s, response);
}
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
class MyServletRequestWrapper extends HttpServletRequestWrapper {
private final HashMap<String, String[]> params;
MyServletRequestWrapper(HttpServletRequest request, HashMap<String, String[]> params) {
super(request);
this.params = params;
}
@Override
public String getParameter(String name) {
if (this.params.containsKey(name)) {
return this.params.get(name)[0];
}
return "";
}
@Override
public Map<String, String[]> getParameterMap() {
return this.params;
}
@Override
public Enumeration<String> getParameterNames() {
return new Enumerator<>(params.keySet());
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
来源:https://stackoverflow.com/questions/38165131/spring-security-oauth2-accept-json