Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
Then I started on a path of discovery and following message in a spring security documentation kind of crushed my hopes.
Spring Security’s Authorization Server support was never a good fit. An Authorization Server requires a library to build a product. Spring Security, being a framework, is not in the business of building libraries or products. For example, we don’t have a JWT library, but instead we make Nimbus easy to use. And we don’t maintain our own SAML IdP, CAS or LDAP products.So, as they say, the writing was on the wall. I was using Authorization Server provided by Spring and they have stopped supporting that in spring security 5, the spring cloud needed dependency on WebFlux which was supported with spring security 5. All in all I got totally confused with the dependencies and concluded that I will have to build a Authorization Server from scratch. This post is about my effort towards that and some learnings on the way.
First step, I cleaned up all the code that had anything to do with Filters, Authentication, Converters in the old server. WebFlux just doesn't play nicely with any of that. Then I read a primer on reactive programming, I think if you read this blog and look at my code, you will realize that from reactive programming point of view, I am still not very fluent. In any case, I learnt enough to make this work.
The next challenge was database. Any of the relational databases are not reactive and I was stuck with MySQL. For the time being I have left it at that. In future, I will replace it with a backend that is reactive. For now, the interface to persistence layer, the repositories etc are all old style JpaRepository.
Let's get started now. The first step is to look at the primary filter chain. This is equivalent to the WebSecurityConfigurerAdapter in any old style non-WebFlux system.
Let's look at all important method securityWebFilterChain, We first define a AuthenticationWebFilter. This class is provided by Spring and we just use it. But this class requires either an implementation of ReactiveAuthenticationManager or an implementation of ReactiveAuthenticationManagerResolver. Since we might need more than authentication server because we are building a authorization server that can handle JWT tokens, we decided to implement a ReactiveAuthenticationManagerResolver that can handle two AuthenticationManager based on specified criteria.
As is clear from the resolve method of the class VarahamihirAuthenticationManagerResolver, we have two different ReactiveAuthenticationManager instances, one that will handle Client tokens and the other one that will handle User tokens. To differentiate between the types of tokens, the class UserDetailsRepositoryReactiveAuthenticationManager that is provided by Spring is instantiated with either a VarahamihirReactiveClientUserDetailService which implements ReactiveUserDetailsService for clients or VarahamihirReactiveUserDetailService which implements the same service for normal users. This point, line 58 in VarahamihirWebServerSecurityConfig will make sure that right table from the database is looked up for the identification of client or user. Let's take a look at two ReactiveUserDetailsService classes.
Now we can carefully examine VarahamihirWebServerSecurityConfig which defines the Filter chain for authentication purposes.
- Lines 56 to 59 define the AuthenticationWebFilter which is added in the chain at line line 68.
- Lines 61 to 63 added some filters that I used to extract HTTP parameters into RequestContext so that other methods can have access to them if required. Lots of multi-tenancy code is built around using these variables. For example a path of the URL contains tenant discriminator and it used to identify the tenant. If you want to know the details of it, it is described in detail in my other blog at this location.
- Lines 65 and 66 define the URLs that need to be exempted from authentication mechanism. For example we want new user registration to be outside the authentication flow.
- Lines 67 to 71 define actual authenticated endpoint handling.
- Line 72 adds a filter that will do processing related to JWT tokens.
We also define another AuthenticationManager to take care authentication using JWT tokens. Here is the class. Here is a very critical things to note. Once you are confirming authentication success, you should only use one constructor in UsernamePasswordAuthenticationToken class that takes a set of GrantedAuthority as an argument. Otherwise the authentication will always fail. You can not use setAuthentication(true) as an option.
This AuthenticationManager is used from the WebFilter that processing all JWT tokens. Here is the filter code.
The filter is making sure authentication is handled and security context is setup properly. There is a hook provided for any processing that one might want to do on authentication success. We have set it to the method onAuthSuccess. We use VarahamihirJWTAuthConverters to convert HTTP headers into a valid authentication object. Look at apply method below.
We use Nimbus JOSE+JWT for handling of JWT token itself. The functionality of this library is wrapped around our utility class VarahamihirJWTUtil.
These are the most important bits and your authorization server is ready. The complete code is available on my repository here. The key modules to look for are identity-server and varahamihir-common. This has a working authorization server tagged as v1.0.
$ curl --request POST \ --url http://localhost:8081/default/registration/user \ --header 'authorization: Basic c3VwZXJzZWNyZXRjbGllbnQ6c3VwZXJzZWNyZXRjbGllbnQxMjM=' \ --header 'cache-control: no-cache' \ --header 'content-type: application/json' \ --header 'postman-token: 5790c497-8264-f5b3-025f-d9963c5b41a1' \ --header 'x-tenant: default' \ --data '{\n "username" : "admin2",\n "password" : "admin2123",\n "fullname" : "Adminitrator for a tenant"\n}'
{ "id": "9af7a442-bc25-4d2c-b5d1-341dcf6fe2da", "tenantId": "a5c8cbe0-bce5-440b-b18e-ffc96d253092", "fullname": "Adminitrator for a tenant", "username": "admin2", "mask": 0 }
$ curl --request POST --url http://localhost:8081/default/oauth/token --header authorization: Basic c3VwZXJzZWNyZXRjbGllbnQ6c3VwZXJzZWNyZXRjbGllbnQxMjM= --header cache-control: no-cache --header content-type: application/x-www-form-urlencoded --header postman-token: 6d5facf2-3143-62ea-09d5-86ee08a76310 --header x-tenant: default --data username=admin2&password=admin2123&audience=self&grant_type=password
{ "auth_token": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..hhAq6K4M2dH4StukQvbH-A.pLREINAeNzvrDuFC03kewv47nVU2_2xqLypkdm_aO2FBWCPDnQNVCva65K1PcMGXpHGZ2hurbex4ELcM4WB9GX93rKn1W9bP1PmWCqPORX43kEPUgV6E9P6GZ0Y0mrvhQcss8M9qOF1gBlTOAD7F8hFEPEE0HQypVGXv1prt-11bSylVJO55-sOHGW-MITaYy8t0eWL8WDdP9msVdrcpjjqEVAaM6snJGPc7xp3l4NM.XUedILH4QfRqRKGliAtgtQ", "expiry": 500, "refreshToken": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..zuO7tVknKsgnM5R37gobRg.TDciaBT-StLDTLjBIeR-rLQgFCG7UOUxHREJ5uK8DgVgCvsNQ4xufTEYxcrUxskF5Gr717-HvIrNsXtTnPQdTpktBfdszNUv8-WFsnG1EV63-t8ZJdo7fWQ_O3DNQY78FvGvvqJoQPs2Besz1P9uDNc4NIb4ivm9Nz51aYMSZYOA-8HzDGlS-0fzQfg5ElUEAjyp0p4KVhpUFifNmZkESGcKfU6tYCbhxGVH1NmD8Ec.ESgaKVnFngNzpYAGDyfq1g", "refreshTokenExpiry": 1000, "scope": "read,write", "tokenType": "Bearer" }
No comments:
Post a Comment