8

I'm trying to change JHipster so it uses a JSON object for authentication instead of form parameters. I've managed to make this work for its JWT authentication mechanism. Now I'd like to do it for other authentication options.

Is there an easy way to change Spring Security's default security configuration to allow this? Here's what JHipster uses now:

.and()
    .rememberMe()
    .rememberMeServices(rememberMeServices)
    .rememberMeParameter("remember-me")
    .key(env.getProperty("jhipster.security.rememberme.key"))
.and()
    .formLogin()
    .loginProcessingUrl("/api/authentication")
    .successHandler(ajaxAuthenticationSuccessHandler)
    .failureHandler(ajaxAuthenticationFailureHandler)
    .usernameParameter("j_username")
    .passwordParameter("j_password")
    .permitAll()

I'd like to send the following as JSON instead of form parameters:

{username: "admin", password: "admin", rememberMe: true}
Angelo Fuchs
  • 9,825
  • 1
  • 35
  • 72
Matt Raible
  • 8,187
  • 9
  • 61
  • 120

2 Answers2

1

I just needed something very similar, so I wrote it.

This uses Spring Security 4.2, WebSecurityConfigurationAdapter. There instead of using ...formLogin()... I wrote an own Configurer that uses JSON when available and defaults to Form if not (because I need both functionalities).

I copied all things that needed to be present (but I didn't care about) from org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer and org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter where the source code and documentation greatly helped me.

It is well possible that you will need to copy over other functions as well, but it should do in principle.

The Filter that actually parses the JSON is in the end. The code sample is one class so can be copied over directly.

/** WebSecurityConfig that allows authentication with a JSON Post request */
@Configuration
@EnableWebSecurity(debug = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // resources go here
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // here you will need to configure paths, authentication provider, etc.

        // initially this was http.formLogin().loginPage...

        http.apply(new JSONLoginConfigurer<HttpSecurity>()
                  .loginPage("/authenticate")
                  .successHandler(new SimpleUrlAuthenticationSuccessHandler("/dashboard"))
                  .permitAll());
    }

    /** This is the a configurer that forces the JSONAuthenticationFilter.
     * based on org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
     */
    private class JSONLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
              AbstractAuthenticationFilterConfigurer<H, JSONLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {

        public JSONLoginConfigurer() {
            super(new JSONAuthenticationFilter(), null);
        }

        @Override
        public JSONLoginConfigurer<H> loginPage(String loginPage) {
            return super.loginPage(loginPage);
        }

        @Override
        protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
            return new AntPathRequestMatcher(loginProcessingUrl, "POST");
        }

    }

    /** This is the filter that actually handles the json
     */
    private class JSONAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

        protected String obtainPassword(JsonObject obj) {
            return obj.getString(getPasswordParameter());
        }

        protected String obtainUsername(JsonObject obj) {
            return obj.getString(getUsernameParameter());
        }

        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 
                  throws AuthenticationException {
            if (!"application/json".equals(request.getContentType())) {
                // be aware that objtainPassword and Username in UsernamePasswordAuthenticationFilter
                // have a different method signature
                return super.attemptAuthentication(request, response);
            }

            try (BufferedReader reader = request.getReader()) {

                //json transformation using javax.json.Json
                JsonObject obj = Json.createReader(reader).readObject();
                String username = obtainUsername(obj);
                String password = obtainPassword(obj);

                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                          username, password);

                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException ex) {
                throw new AuthenticationServiceException("Parsing Request failed", ex);
            }
        }
    }
}
Angelo Fuchs
  • 9,825
  • 1
  • 35
  • 72
  • I'm getting an exception on startup from the loginPage method, it's too long to post as comment – Andy Feb 11 '19 at 18:24
  • Caused by: java.lang.IllegalStateException: securityBuilder cannot be null at org.springframework.security.config.annotation.SecurityConfigurerAdapter.getBuilder(SecurityConfigurerAdapter.java:68) – Andy Feb 11 '19 at 18:37
  • @Andy I'm currently not at my dev station. If you need support either send me an email or post a question – Angelo Fuchs Feb 12 '19 at 09:45
  • https://gist.github.com/debedb/4b47ed9f355d9124cc1743a89421ee8f – Gregory Golberg Dec 12 '21 at 09:36
  • @GregoryGolberg I'm not working on this any more and cannot verify that your change works; So I wont change the post. I think its no good to hide your optimization behind an outside link. Would you want to post it as a separate answer? – Angelo Fuchs Dec 14 '21 at 15:29
0

I have done such a kind of thing. The solution it's not difficult but I did the trick creating a custom security filter mainly based in UserNamePasswordAuthenticationFilter.

Actually, you should override attemptAuthentication method. Just overriding obtainPassword and obtainUsername may not be enaugh, since you want to read the request body and you must do it at once for both parameters (if you don't create a kind of multi-read HttpServletRequest wrapper)

Solution must be like this:

    public class JsonUserNameAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
    //[...]
    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }

    UsernamePasswordAuthenticationToken authRequest =
            this.getUserNamePasswordAuthenticationToken(request);

            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);

            return this.getAuthenticationManager().authenticate(authRequest);
        }
        //[...]

protected UserNamePasswordAuthenticationToken(HttpServletRequest request){
    // here read the request body and retrieve the params to create a UserNamePasswordAuthenticationToken. You may use jackson of whatever you like most
}
//[...]
}

Then you must configure it. I allways use xml based configuration for this kind of complex configs,

    <beans:bean id="jsonUserNamePasswordAuthenticationFilter" 
                class="xxx.yyy.JsonUserNamePasswordAuthenticationFilter">
            <beans:property name="authenticationFailureHandler>
                <beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                    <!-- set the failure url to a controller request mapping returning failure response body.
                    it must be NOT secured -->
                </beans:bean>
            </beans:property>
            <beans:property name="authenticationManager" ref="mainAuthenticationManager" />
            <beans:property name="authenticationSuccessHandler" >
                <beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                    <!-- set the success url to a controller request mapping returning success response body.
                    it must be secured -->
                </beans:bean>
            </beans:property>
        </beans:bean>

        <security:authentication-manager id="mainAuthenticationManager">                
            <security:authentication-provider ref="yourProvider" />
        </security:authentication-manager>

<security:http pattern="/login-error" security="none"/>
    <security:http pattern="/logout" security="none"/>

<security:http pattern="/secured-pattern/**" auto-config='false' use-expressions="false"
        authentication-manager-ref="mainAuthenticationManager" 
        create-session="never" entry-point-ref="serviceAccessDeniedHandler">
        <security:intercept-url pattern="/secured-pattern/**" access="ROLE_REQUIRED" />
        <security:custom-filter ref="jsonUserNamePasswordAuthenticationFilter" 
            position="FORM_LOGIN_FILTER" />     
        <security:access-denied-handler ref="serviceAccessDeniedHandler"/>
        <security:csrf disabled="true"/>
    </security:http>

You may create some extra objects as a access-denied-handler, but that's the easiest part of the thing

jlumietu
  • 6,234
  • 3
  • 22
  • 31
  • 1
    Can You look at this post: https://stackoverflow.com/questions/35687148/how-to-make-spring-security-accept-json-instead-of-form-parameters#35699200 – masterdany88 Mar 01 '16 at 13:31
  • 1
    XML Config is completely broken. Please refer to [this question instead](http://stackoverflow.com/questions/19500332/spring-security-and-json-authentication). – Younes May 09 '17 at 19:11
  • @Younes are you sure? It's been a while since this reply but it was taken from a tested and working example. What exactly is broken? Of course, there are some elements not configured, just where the xml comment tags are placed, but it was definitely working when I wrote my answer – jlumietu May 09 '17 at 19:17
  • @jlumietu yes, very sure. `` at line 10 doesn't have its closing tag and `` at line 1 doesn't have its closing tag. To get it right, you have to replace `` at line 15 by `<\beans:property>`. – Younes May 09 '17 at 19:35
  • 1
    @Younes you have filled the gaps within the comments with your settings? Wait, I found a typo... – jlumietu May 09 '17 at 19:37
  • @Younes you are rigth, and there was another mistake. In the `authenticationSuccessHandler` there was also a ref attribute referring another bean `ref="customSuccessHandler"` whilst at the same time a bean was being provided inside the body of the tag. Now it should work – jlumietu May 09 '17 at 19:48