Spring security OAuth2 accept JSON

前端 未结 6 1837
深忆病人
深忆病人 2020-12-09 00:56

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 PO         


        
相关标签:
6条回答
  • 2020-12-09 01:14

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

    Here is the repo with the integration test

    0 讨论(0)
  • 2020-12-09 01:15

    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"
    }
    
    0 讨论(0)
  • 2020-12-09 01:20

    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.

    0 讨论(0)
  • 2020-12-09 01:23

    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
        }
    
        ...
    
    }
    
    0 讨论(0)
  • 2020-12-09 01:23

    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:

    1. Create a custom spring security filter.
    2. In your custom filter, create a class to subclass HttpRequestWrapper. The class allow you to wrap the original request and get parameters from JSON.
    3. In your subclass of HttpRequestWrapper, parse your JSON in request body to get username, password and grant_type, and put them with the original request parameter into a new HashMap. Then, override method of getParameterValues, getParameter, getParameterNames and getParameterMap to return values from that new HashMap
    4. Pass your wrapped request into the filter chain.
    5. Configure your custom filter in your Spring Security Config.

    Hope this can help

    0 讨论(0)
  • 2020-12-09 01:33

    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);
          }
    
    }
    
    0 讨论(0)
提交回复
热议问题