Giter VIP home page Giter VIP logo

spring-webflux-security-jwt's Introduction

Authentication and Authorization using JWT with Spring WebFlux and Spring Security Reactive

Nice Docs to Read First

Before getting started I suggest you go through the next reference

Spring Webflux

Spring Security Reactive

Spring Security Architecture

Enable Spring WebFlux Security

First enable Webflux Security in your application with @EnableWebFluxSecurity

@SpringBootApplication
@EnableWebFluxSecurity
public class SecuredRestApplication {
....
}

Create an InMemory UserDetailsService

Define a custom UserDetailsService bean where an User with password and initial roles is added:

@Bean
    public MapReactiveUserDetailsService userDetailsRepository() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("user")
                .roles("USER", "ADMIN")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

In this example user information will be stored in memory using a Map but it can be replaced by different strategies.

Before getting a Json Web Token an user should use another authentication mechanism, for example HTTP Basic Authentication and provided the right credentials a JWT will be issued which can be used to perform future API calls by changing the Authetication method from Basic to Bearer.

Starting from Basic Authentication

Below there's a simple way to define Basic Authentication with Spring Security. Customization is needed in order to return a JWT on succesful authentication.

@Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .anyExchange().authenticated()
                .and()
            .httpBasic(); // Pure basic is not enough for us!
            
        return http.build();
    }

Inspect AuthenticationFilter, improvise, adapt overcome

With Spring Reactive, requests go through a chain of filters, each filter can aprove or discard requests according to different rules. Advantage is taken to perform request authentication. Different types of WebFilter are grouped by a WebFilterChain, in Spring Security there's AuthenticationWebFilter which outlines how authentication should be performed on requests matching a criteria.

AuthenticationWebFilter implements all the required behavior for Basic Authentication, take a look at it:

public class AuthenticationWebFilter implements WebFilter {

	private final ReactiveAuthenticationManager authenticationManager;

	private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler(); 
  // WE NEED A DIFFERENT SUCCESS HANDLER!!!!!!

	private Function<ServerWebExchange, Mono<Authentication>> authenticationConverter = new ServerHttpBasicAuthenticationConverter();

	private ServerAuthenticationFailureHandler authenticationFailureHandler = new ServerAuthenticationEntryPointFailureHandler(new HttpBasicServerAuthenticationEntryPoint());

	private ServerSecurityContextRepository securityContextRepository = NoOpServerSecurityContextRepository.getInstance();

	private ServerWebExchangeMatcher requiresAuthenticationMatcher = ServerWebExchangeMatchers.anyExchange();

....

The behavior that needs to be changed is what happens once an user has been authenticated using user/password credentials. The WebFilterChainServerAuthenticationSuccessHandler will pass the request through the filter chain. A custom implementation is needed in this step where a Json Web Token is generated and added to the response, then the exchange will follow its way.

Create custom SuccessHandler to make Basic Authentication return a Json Web Token

Create a custom ServerAuthenticationSuccessHandler, this handler is executed once the authentication with user/password has been successful, it receives the current exchange and Authentication object. A JWT is generated using the Exchange and Authentication object. In this way BasicAuthenticationSuccessHandler implements the desired behavior:

...
 @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
    // Create and attach a JWT before passing the exchange to the filter chain
        ServerWebExchange exchange = webFilterExchange.getExchange();
        exchange.getResponse()
                .getHeaders()
                .add(HttpHeaders.AUTHORIZATION, getHttpAuthHeaderValue(authentication));
        return webFilterExchange.getChain().filter(exchange);
    }
...

The response from the current exchange is updated with the HTTP Authorization header with a new JWT that contains data from the Authentication object.

Create a Basic Authentication filter that returns a JWT

Now create a new AuthenticationFilter with a custom handler:

...
UserDetailsRepositoryReactiveAuthenticationManager authManager;
        AuthenticationWebFilter basicAuthenticationFilter;
        ServerAuthenticationSuccessHandler successHandler;
        
        authManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsRepository());
        successHandler = new  BasicAuthenticationSuccessHandler();

        basicAuthenticationFilter = new AuthenticationWebFilter(authManager);
        basicAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);

...

Add this filter to ServerHttpSecurity

Add this to our ServerHttpSecurity:

...
http
                .authorizeExchange()
                    .pathMatchers("/login", "/")
                    .authenticated()
                .and()
                    .addFilterAt(basicAuthenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC)
...

The functionality that returns a JWT when authenticating using User and Password is now implemented.

Handle Requests with Bearer token Authorization Header

Now let's build the functionality that will take a request with the HTTP Authorization Header containing a Bearer token. The same way the AuthenticationWebFilter was customized before, customize another to create a new filter.

When using JWT all information needed to authenticate and authorize a user lives within a token. Perform the next steps:

Filter requests containing a Bearer token within its HTTP Authorization Header, verify that are well formed, confirm that it has a valid signature and then build an Authorization object with all information contained in the payload. If the JWT is invalid, there won't be Authorization resulting in an unauthorized response.

Because all information needed is contained in the JWT payload all invalid tokens will be rejected in the filtering step, but the contract defined by the AuthenticationWebFilter requires a non null AuthenticationManager. Create a dummy manager that will authenticate all exchanges. Why? Because all invalid JWT did not resulted in an authorization object and did not make it into this step.

Generate an Authentication object using only the information contained in the token

Create a converter ServerHttpBearerAuthenticationConverter that takes a request ServerWebExchange and returns an Authorization object created with the information extracted from the token:

...
 public Mono<Authentication> apply(ServerWebExchange serverWebExchange) {
        return Mono.justOrEmpty(serverWebExchange)
                .flatMap(AuthorizationHeaderPayload::extract)
                   .filter(matchBearerLength)
                .flatMap(isolateBearerValue)
                .flatMap(jwtVerifier::check)
                .flatMap(UsernamePasswordAuthenticationBearer::create).log();
    }
...

Create a dummy AuthenticationManager

Now implement a dummy AuthenticationManager called BearerTokenReactiveAuthenticationManager:

...
 public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.just(authentication);
    }
  
...

Add the new filter to ServerHttpSecurity

Finally chain this filter in the ServerHttpSecurity configuration object:

...
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

        http
                .authorizeExchange()
                    .pathMatchers("/login", "/")
                    .authenticated()
                .and()
                    .addFilterAt(basicAuthenticationFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
                       .authorizeExchange()
                    .pathMatchers("/api/**")
                    .authenticated()
                .and()
                    .addFilterAt(bearerAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION);

        return http.build();
    }
...

Create a REST Controller and configure access rules

...
 @GetMapping("/api/private")
    @PreAuthorize("hasRole('USER')")
    public Flux<FormattedMessage> privateMessage() {
        return messageService.getCustomMessage("User");
    }

...

Run the Application

With Maven

$ mvn spring-boot:run

With Gradle

$ ./gradlew bootRun

Test it

Login using HTTP Basic

$ curl -v  -u user:user localhost:8080/login

Inspect the response contents and find the authorization header. It should look like:

Authorization: Bearer eyJhbGciOiJIUzI1Ni.....

Use that in another request:

$ curl -v  -H "Authorization: Bearer eyJhbGciOiJIUzI1Ni....."  localhost:8080/api/admin

You should be able to consume the API

That's all

Hope you enjoy it.

spring-webflux-security-jwt's People

Contributors

raphaeldl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spring-webflux-security-jwt's Issues

How to use this application

Sorry I am new to this whole Spring Security scenario, and also to reactive programming. I am trying to run this application, and I am not sure how I can login to this application and generate the JWT Token for furthur requests to the resource server

Missing @EnableReactiveMethodSecurity

First off thank you for this great example.

I think you forgot to add the @EnableReactiveMethodSecurity annotation on your SecuredRestApplication. I was playing around a bit with your code and removing the ADMIN role from the user setup did not prevent me from accessing the /api/admin endpoint.

@Bean
public MapReactiveUserDetailsService userDetailsRepository() {
	UserDetails user = User.withDefaultPasswordEncoder()
	                       .username("user")
	                       .password("user")
	                       .roles("USER")
	                       .build();
	return new MapReactiveUserDetailsService(user);
}

Then I generated a new token Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOiJST0xFX1VTRVIiLCJpc3MiOiJyYXBoYS5pbyIsImV4cCI6MTU2NzY3OTY3OX0.C67PZ_YX2Zm1_YDMnVgqoxNXCEd4iKOhTM9EdiEA5WI (content can be checked via https://jwt.io/ and verified with the default secret of your app).

This will then still allow me to call the admin endpoint:

$ http -v :8080/api/admin "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOiJST0xFX1VTRVIiLCJpc3MiOiJyYXBoYS5pbyIsImV4cCI6MTU2NzY3OTY3OX0.C67PZ_YX2Zm1_YDMnVgqoxNXCEd4iKOhTM9EdiEA5WI"
GET /api/admin HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZXMiOiJST0xFX1VTRVIiLCJpc3MiOiJyYXBoYS5pbyIsImV4cCI6MTU2NzY3OTY3OX0.C67PZ_YX2Zm1_YDMnVgqoxNXCEd4iKOhTM9EdiEA5WI
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.8



HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
transfer-encoding: chunked

[
    {
        "message": "Hello Admin!",
        "name": "Admin"
    }
]

When adding the @EnableReactiveMethodSecurity annotation, I get the following, as expected:

HTTP/1.1 403 Forbidden
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
transfer-encoding: chunked

Denied

AuthorizationHeaderPayload should use getResponse()?

Should AuthorizationHeaderPayload be using getResponse()?

https://github.com/raphaelDL/spring-webflux-security-jwt/blob/master/src/main/java/io/rapha/spring/reactive/security/auth/jwt/AuthorizationHeaderPayload.java#L29

`public class AuthorizationHeaderPayload {

public static Mono<String> extract(ServerWebExchange serverWebExchange) {
    return Mono.justOrEmpty(serverWebExchange.getResponse()
            .getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION));
}

}`

Split responsibility of ServerHttpBearerAuthenticationConverter

I think ServerHttpBearerAuthenticationConverter should be used only for preparing the token for further processing:

Mono.justOrEmpty(serverWebExchange)
                .map(JWTAuthorizationPayload::extract)

while authenticating the token:

map(VerifySignedJWT::check)
                .map(UsernamePasswordAuthenticationFromJWTToken::create)

is the responsibility of JWTReactiveAuthenticationManager

By the way - instead of providing own implementation of JWTAuthorizationWebFilter just do:

@Component
public class JWTAuthenticationWebFilter extends AuthenticationWebFilter {

    public JWTAuthenticationWebFilter(final JWTAuthenticationManager authenticationManager,
            final ServerHttpBearerAuthenticationConverter converter,
            final UnauthorizedAuthenticationEntryPoint entryPoint) {
        super(authenticationManager);
        setAuthenticationConverter(converter);
        setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        setRequiresAuthenticationMatcher(new JWTHeadersExchangeMatcher()); //this is necessary to match headers. Requests without JWT header should not be taken into account by this filter
    }
    private static class JWTHeadersExchangeMatcher implements ServerWebExchangeMatcher {

        @Override
        public Mono<MatchResult> matches(final ServerWebExchange exchange) {
            return Mono.just(exchange)
                    .map(ServerWebExchange::getRequest)
                    .map(ServerHttpRequest::getHeaders)
                    .filter(h -> h.containsKey("some JWT header"))
                    .flatMap($ -> MatchResult.match())
                    .switchIfEmpty(MatchResult.notMatch());
        }
    }
}

And don't forget to set:

        http
            .exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedAuthenticationEntryPoint ())
            .and()

where

@Component
public class UnauthorizedAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

	@Override
	public Mono<Void> commence(final ServerWebExchange exchange, final AuthenticationException e) {
		return Mono.fromRunnable(() -> exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED));
	}
}

otherwise SpringSecurity will still display BasicAuth on requests without headers

Doc not exact :) cost me 1 hour my life

Use that in another request:

$ curl -v -H "Authorization: Authorization: Bearer eyJhbGciOiJIUzI1Ni....." localhost:8080/api/admin

You should be able to consume the API


have to be:
$ curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1Ni....." localhost:8080/api/admin

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.