50

I have a Spring REST application which at first was secured with Basic authentication.

Then I added a login controller that creates a JWT JSON Web Token which is used in subsequent requests.

Could I move the following code out of the login controller and into the security filter? Then I would not need the login controller any longer.

tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());

Or could I remove the Basic authentication?

Is it a good design to mix Basic authentication with a JWT?

Although it all works fine, I'm a bit in the dark here as to best design this security.

SilverlightFox
  • 32,436
  • 11
  • 76
  • 145
Stephane
  • 11,836
  • 25
  • 112
  • 175
  • 1
    How is the token sent to the server in subsequent requests? (HTTP Header? Cookie?). Also, are you using TLS (SSL)? – Les Hazlewood Mar 09 '15 at 16:05
  • Hi Les, nice to see you popping up again ! Yes, the token is sent as a X-Auth-Token header. I'm also using TLS. Is TLS mandatory when using a JWT ? – Stephane Mar 09 '15 at 17:37
  • 1
    Hi Stephane! :) If the JWT represents a verified identity, yes, I'd consider TLS mandatory, otherwise its (much) easier for MITM attacks. – Les Hazlewood Mar 09 '15 at 22:30
  • Last background question before I attempt to answer: is your REST client a JavaScript (JQuery, Angular, etc) or mobile client? – Les Hazlewood Mar 09 '15 at 22:33
  • Good point. I reckon there's no need for basic authentication in a jwt setup.. – Stephane Mar 09 '15 at 22:33
  • I used basic authentication first to get going quick. And added jwt auth. I should now remove the basic auth. I suppose there's no need for it any longer. – Stephane Mar 09 '15 at 22:35
  • Can someone provide any example for above use case? I want to use BasicAuthenticationFilter for login page and my custom filter which is JWT filter for all other page. – kedar Feb 16 '17 at 02:46
  • @kedar Why would you want to authenticate a user before letting him go to your login page ? – Stephane Feb 16 '17 at 08:04
  • I am developing a login api which we take the http basic authentication and will send back a JWT on successful login and for all other api I will use this JWT – kedar Feb 16 '17 at 19:09

2 Answers2

113

Assuming 100% TLS for all communication - both during and at all times after login - authenticating with username/password via basic authentication and receiving a JWT in exchange is a valid use case. This is almost exactly how one of OAuth 2's flows ('password grant') works.

The idea is that the end user is authenticated via one endpoint, e.g. /login/token using whatever mechanism you want, and the response should contain the JWT that is to be sent back on all subsequent requests. The JWT should be a JWS (i.e. a cryptographically signed JWT) with a proper JWT expiration (exp) field: this ensures that the client cannot manipulate the JWT or make it live longer than it should.

You don't need an X-Auth-Token header either: the HTTP Authentication Bearer scheme was created for this exact use case: basically any bit of information that trails the Bearer scheme name is 'bearer' information that should be validated. You just set the Authorization header:

Authorization: Bearer <JWT value here>

But, that being said, if your REST client is 'untrusted' (e.g. JavaScript-enabled browser), I wouldn't even do that: any value in the HTTP response that is accessible via JavaScript - basically any header value or response body value - could be sniffed and intercepted via MITM XSS attacks.

It's better to store the JWT value in a secure-only, http-only cookie (cookie config: setSecure(true), setHttpOnly(true)). This guarantees that the browser will:

  1. only ever transmit the cookie over a TLS connection and,
  2. never make the cookie value available to JavaScript code.

This approach is almost everything you need to do for best-practices security. The last thing is to ensure that you have CSRF protection on every HTTP request to ensure that external domains initiating requests to your site cannot function.

The easiest way to do this is to set a secure only (but NOT http only) cookie with a random value, e.g. a UUID.

Then, on every request into your server, ensure that your own JavaScript code reads the cookie value and sets this in a custom header, e.g. X-CSRF-Token and verify that value on every request in the server. External domain clients cannot set custom headers for requests to your domain unless the external client gets authorization via an HTTP Options request, so any attempt at a CSRF attack (e.g. in an IFrame, whatever) will fail for them.

This is the best of breed security available for untrusted JavaScript clients on the web today that we know of. Stormpath wrote an article on these techniques as well if you're curious. HTH!

Les Hazlewood
  • 18,480
  • 13
  • 68
  • 76
  • 1
    Beautiful answer Les and thank you. I recall I added the login controller later in the app development, when I learned about and implemented the JWT token authentication in the app. Indeed I couldn't figure out how to create the token from within the Basic auth Spring Security filter. Reading your solution, I see that it is how I could and should do it, and remove the login controller altogether as it becomes unnecessary... – Stephane Mar 11 '15 at 14:03
  • Hi Les, I was about to replace the X-Auth-Token header by the standard Authorization header using the Bearer prefix. That's when I stumbled upon an answer stating that I should not use a standard header and instead use a custom one. His point is that this standard header should be left to Basic authentication. See http://stackoverflow.com/questions/12086041/basic-authentication-with-a-guid-token-for-rest-api-instead-of-username-password Puzzled... – Stephane Mar 13 '15 at 13:09
  • 1
    @StephaneEybert I added an answer to that thread. The `Authorization` header supports many _schemes_. You can use whatever scheme you want. `Basic` represents one algorithm. `Bearer` is just whatever text follows it (no algorithm). Other schemes (like `Digest`) use a different algorithm. You could even invent your own scheme. The point is the header is the same, but scheme names and their trailing text values reflect the exact behavior. I expand on this [here](http://stackoverflow.com/a/14046557/407170) – Les Hazlewood Mar 15 '15 at 00:16
  • @StephaneEybert in my answer above, I recommend that you *should not* use a custom header for _authentication_ reasons - just use the `Authorization` header (and the `Bearer` scheme in your case). You *would* need to use a custom header for the CSRF Token approach since that isn't an authentication mechanism. – Les Hazlewood Mar 15 '15 at 00:18
  • @StephaneEybert also, maybe it wasn't clear, but in your use case, I suggested that you not use an Authorization header *at all* - a secure, http-only cookie is even better in your case. Read [this blog article](https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/) for why (this was linked in my answer above too). – Les Hazlewood Mar 15 '15 at 00:21
  • All right ! Awesome knowledge you share here ! I shall go for the Authorization header then. And later see if I can go for the cookie solution you have recommended. – Stephane Mar 17 '15 at 06:06
  • @les-hazlewood I don't understand why we need the cookie with the random value to _ensure that external domains initiating requests to your site cannot function_. This doesn't come free with Same-origin policy? – gabrielgiussi Oct 15 '15 at 22:54
  • Under Spring Boot 2 the configuration is explained at `https://stackoverflow.com/a/51772848/958373` – Stephane Aug 09 '18 at 17:33
  • Using a temporary JWT, how would you handle expirations in the client side? Example: users on a page doing work during which JWT expires. They attempt to commit their work, resulting in 401, and they find themselves in the login page rather annoyed. – Half_Duplex May 09 '20 at 20:21
  • 1
    @Half_Duplex My understanding is that the common way to handle this client side is to call a `refresh` endpoint in advance of the JWT's expiration. Perhaps by using a service worker or with setInterval. – albertjorlando Mar 07 '22 at 17:37
1

Here is some code to back up the accepted answer on how to do this in Spring....simply extend UsernamePasswordAuthenticationFilter and add it to Spring Security...this works well with HTTP Basic Authentication + Spring Security

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

        this.authenticationManager = authenticationManager;

    }

    @Override

    public Authentication attemptAuthentication(HttpServletRequest req,

                                                HttpServletResponse res) throws AuthenticationException {

        try {

            ApplicationUser creds = new ObjectMapper()

                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(

                    new UsernamePasswordAuthenticationToken(

                            creds.getUsername(),

                            creds.getPassword(),

                            new ArrayList<>())

            );

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    @Override

    protected void successfulAuthentication(HttpServletRequest req,

                                            HttpServletResponse res,

                                            FilterChain chain,

                                            Authentication auth) throws IOException, ServletException {

        String token = Jwts.builder()

                .setSubject(((User) auth.getPrincipal()).getUsername())

                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))

                .signWith(SignatureAlgorithm.HS512, SECRET)

                .compact();

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);

    }

}

using JWT lib.:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

spring boot config class

package com.vanitysoft.payit.security.web.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    import com.vanitysoft.payit.util.SecurityConstants;

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
           private UserDetailsService userDetailsService;

            @Autowired
            private  BCryptPasswordEncoder bCryptPasswordEncoder;

         @Override
           protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService)
                      .passwordEncoder(bCryptPasswordEncoder);
           }

         @Override
            protected void configure(HttpSecurity http) throws Exception {
             http.cors().and().csrf().disable()
                    .authorizeRequests()                             
                        .antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll()
                        .antMatchers("/user/**").authenticated()
                        .and()
                        .httpBasic()
                        .and()
                        .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                        .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                        .logout()
                        .permitAll();

            }
    }
Jeryl Cook
  • 989
  • 17
  • 40
  • Would you have the code that injects this filter into the Spring Security configuration ? – Stephane Aug 04 '18 at 08:43
  • Is there a way to express that this filter is to be used only for the login request ? – Stephane Aug 04 '18 at 09:10
  • How do you ensure the authentication manager you inject into your filter has been created before injecting the filter into the security configuration ? – Stephane Aug 04 '18 at 10:06
  • 1
    I am using Spring Boot the wiring of the filter is handled automatically by annotations..ill update my answer with the code i used in the Config.... check out the SpringBoot and Spring Security tutorials. – Jeryl Cook Aug 05 '18 at 23:20
  • Are you on Spring Boot 2 as well ? Does your `JWTAuthorizationFilter` filter also extend the same `UsernamePasswordAuthenticationFilter` class ? Why are having two filters and not doing the authentication and authorization in the same filter ? Why do you use this `@EnableGlobalMethodSecurity(prePostEnabled = true)` ? You don't have another filter to authenticate from a JWT token ? – Stephane Aug 06 '18 at 14:13
  • And yes I have read so many tutorials trying to have my old application work under Spring Boot 2 but a few weeks have passed and I'm still in the trenches https://stackoverflow.com/questions/51686921/spring-security-filter-not-firing-up/51689661 – Stephane Aug 06 '18 at 14:35
  • @Stephane Spring Boot 2. – Jeryl Cook Oct 11 '18 at 17:55