Oauth2 is an industry-standard protocol for authorization.
As per Oauth2 specification(RFC-6749) β
The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.
The following diagram illustrates the working of an Oauth2 authentication request with an Authorization Code Grant flow.
There are four parties involved β
- Client is a third party Application that wants access to the protected resource from a resource server.
- Authentication Server is the server which helps to authenticate a Resource Owner.
- Resource Owner is the user that owns the protected resource.
- Resource Server is the server which serves the protected resources owned by Resource Owner.
- First of all, the client sends an authorization request to Resource Owner so that on behalf of the Resource Owner, it can access the protected resource(s).
- If the Authorization-code-grant is used, the Authorization code is returned to the client. It means the Resource owner has given access to the client for protected resources.
- The client sends this Authorization code to the Authentication Server, which in return provides an Authentication token β typically a JWT token.
- Once the client has the authentication token, It use it to access the protected resources from a resource server. The token expires after a set timeout.
In this post, we will focus on the 4th step i.e. How a Resource Server validates a JWT token provided by any third party client.
First let us understand, what is JWT and what APIβs are provided by spring security to implement Jwt Authentication.
What is JWT?
ππΌ Checkout the complete introduction at jwt.io π
Spring Security API for JWT Authentication
The below diagram provides a thorough overview of Spring security API Specs for JWT Authentication.
When a client submits a request along with bearer token. It is passed through the security filter chain. The
BearerTokenAuthenticationFilter
creates aBearerTokenAuthenticationToken
of the typeAuthentication
.Next, The
AuthenticationManagerResolver
resolves theAuthenticationManager
which in turn selects the specificAuthenticationProvider
.The
BearerTokenAuthenticationToken
is passed toAuthenticationProvider
byProviderManager
( the default Implementation ofAuthenticationManager
)For JWT authentication,
JwtAuthenticationProvider
is selected. It decodes, verifies and validates theJwt
usingJwtDecoder
.If the authentication succeeds, the Authentication is set on the
SecurityContextHolder
.If the Authentication fails,
SecurityContextHolder
is cleared.
Finally, Let move ahead with implementing the JWT Authentication.
JWT Authentication in Spring Security
In order to implement it, we would require the following components β
- Authentication server - we will use Keycloak. It supports Oauth2.0.
- Resource Server - We will create one using a spring-boot application.
- Client - We can use Postman API client as the client.
- User - we will setup one user in Keycloak server.
Authentication server via Keycloak
- Follow this link to quickly setup a Keycloak server via Docker.
- Follow this tutorial to setup Keycloak Authorization code grant.
While you are at it, here are few things, you would require once the Keycloak server is setup.
- issuer uri β http://authserver.com/auth/realms/{realm}
- jwks-uri β http://authserver.com/auth/realms/{realm}/protocol/openid-connect/certs
-
Metadata endpoint βTo use the
issuer-uri
property, the Authorization server should support either one of the below URLs as the metadata endpoint :
Make sure to replace authserver.com with valid domain. Also make sure to provide the value value for realm
Once you have the Keycloak
server ready β Letβs go ahead and create a resource server.
Resource Server
The resource server will be the simplest one and will contain only one secure rest API.
Dependencies:
-
spring-security-oauth2-resource-server
**β Most of the resource server support is collected here. -
spring-security-oauth2-jose
β provides support for decoding and verifying JWT.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
API Endpoint
GET /api/v1/users
@RestController
@RequestMapping("/api/v1")
public class UserController {
@GetMapping("/users")
public List<User> getUsers(){
return Arrays.asList(
new User("john doe", 100),
new User("jane doe",300)
);
}
}
Since we have Spring security in the class path, every route will be private.
Setup JWT issuer URL
This is minimal setup required to implement the JWT authentication. The issuer-url
provided is used by Resource Server to discover public keys of authorization server and validate the token. It is also the same URL present in iss
claim.
Spring:
security:
oauth2:
resourceserver:
jwt:
issuer-url: http://localhost:8080/auth/realms/{realm}
When the Resource server is starts up , it will automatically configure itself to validate JWT encoded Bearer Token. It achieves this querying the Authorization Server metadata endpoint for jwks_url
property. This provides access to supported algorithm and its valid public keys.
The Only drawback to this setup is that it would fail if the Authentication server is not already up. To void startup failure, we would need to add the jwk-set-uri
Spring:
security:
oauth2:
resourceserver:
jwt:
issuer-url: http://localhost:8080/auth/realms/dev
jwk-set-uri: http://localhost:8080/auth/realms/{realm}/protocol/openid-connect/certs
Now, the Resource Server will not ping the authorization server at startup. However, issuer-uri
is still kept to validate the JWT iss
claim on incoming token.
Spring Security provides extension points to override or customize the default behavior of the implementation. We will look into customizing some of the default features.
β¦But before that, Letβs test the default implementation.
Time to Test the Implementation π
If you recall, the resource server contains one endpoint with path /api/v1/users
. If we call it without providing an authentication token, it will return 401 - Unauthorized
status. That is due absence of authorization token.
Letβs see how we can can use authorization code grant to fetch a token from the Keycloak
server and use it to access the API
provided by the resource server.
Step - 1: Request OAuth Authorization Code
At this point, we would need a client to request the Authorization code. However to make it easier to test, we can run the following URL in the browser. It should redirect you to the login page and you will have to provide the credentials of the user.
http://authserver.com/auth/realms/{realm}/protocol/openid-connect/auth
?client_id=your-client-id&response_type=code&state=app-state
After the successful completion, it will redirect you to the redirect-url
with the values similar to below.
http://{redirect-url}/?state=appstate
&session_state=d7c5d4de-c883-494a-a2a2-e5108062830c
&code=f5935f66-88e0-4085-80aa-000b2a6b2b51.d7c5d4de-c883-494a-a2a2-e5108062830c.bf89a5ff-5703-42d6-9534-ca59f667f81f
We would require code
to fetch the actual token.
Step - 2: Fetch the Authentication Token
curl -L -X POST "http://localhost:8080/auth/realms/dev/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Cookie: JSESSIONID=E8D36F0DBCBF7E33130B9125F8795CAC.9b51ecd0cc5c; JSESSIONID=8E34666DECDB395B1754FD08C5B385F2" \
--data-urlencode "client_id=client-id-value" \
--data-urlencode "client_secret=client-secret-uuid" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=92236b97-c48f-4827-ae80-80a46e39a0f2.d7c5d4de-c883-494a-a2a2-e5108062830c.bf89a5ff-5703-42d6-9534-ca59f667f81f" \
--data-urlencode "redirect-uri=http://localhost:8085"
-
client-id
,client-secret
can be fetched from the client credentials inKeycloak
server. -
grant_type = authorization_code
describes the grant type used. -
code
fetched in the Step-1. -
redirect-url
should be same as configured for client inKeycloak
server.
The response returned would look similar to the below example:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0S1ZQNGVhRVdjdno4ZjRndXlPd05XTE9GWFEzYWo0b1I0eWx0dkFSZldFIn0.eyJleHAiOjE2MzI5MzMzOTksImlhdCI6MTYzMjkzMzA5OSwiYXV0aF90aW1lIjoxNjMyOTMzMDgyLCJqdGkiOiIzZTk1NGRkMi0zMjhhLTQ3NzItYWQ2NS0xOWQ3NGM2MGZjZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGV2IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImMzNjFkMGVjLWM1MWItNGRmNy05MTA1LTVhOWUyZGViMmRjOCIsInR5cCI6IkJlYXJlciIsImF6cCI6InJlc291cmNlc2VydmVyIiwic2Vzc2lvbl9zdGF0ZSI6ImMyYWRiODIyLWQwY2ItNDY3MC1iYmNjLWU3NWJlOTkyYzg5OSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1kZXYiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVzb3VyY2VzZXJ2ZXIiOnsicm9sZXMiOlsiVVNFUiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJ1c2VyX25hbWUiOiJ1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciJ9.A66uqbRwsUL36GSGozZ7FC3x-M4SCYYLaABMdps-XneseP1saIjsTbHO2QrYq2HbD9jl6nKTYxJHjMdbsRJyY3VtM2mf1D8W24-u8y8qmGf1YNbtFfSTZyrUmwiACEv17onAT8wKgR0C4sdbVFETpRY12f2qQb0mM4ZkT9QQ5DYPBu6dnwyBVXLYJzn8kfmp7JB0OR6LsBTTtyh03t_xiRwb1nSALbUmwq7iUk9lTFEUuUZ182p05q3TKxy9b_kxrCh91EYoYWUdBEhRM4yHjrvN99T-MFpRVaCadyn2YibFbCeZHpsqUmgi-ghR3I70U70HGsL22FEAE4N9X5y_pg",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzZGU1NjBjZi1kMDZlLTRiZmItODY2Yi1mNzJhYjk0YjA0NGMifQ.eyJleHAiOjE2MzI5MzQ4OTksImlhdCI6MTYzMjkzMzA5OSwianRpIjoiODQ5OWNmY2QtZjY1Yy00YzdhLThhNDctMzdhNjg4ZGZjMjU0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL2RldiIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9kZXYiLCJzdWIiOiJjMzYxZDBlYy1jNTFiLTRkZjctOTEwNS01YTllMmRlYjJkYzgiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoicmVzb3VyY2VzZXJ2ZXIiLCJzZXNzaW9uX3N0YXRlIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5Iiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5In0.747XKhyNqZCDEzSfLV4K96sgAW0daN1C1ROUr5L_s_E",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "c2adb822-d0cb-4670-bbcc-e75be992c899",
"scope": "email profile"
}
Step - 3: Run the API with
We will use the access_token
value from previous response as the bearer token to run the private API β (api/v1/users
) provided by resource server.
curl -L -X GET "http://localhost:8090/api/v1/users" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0S1ZQNGVhRVdjdno4ZjRndXlPd05XTE9GWFEzYWo0b1I0eWx0dkFSZldFIn0.eyJleHAiOjE2MzI5MzMzOTksImlhdCI6MTYzMjkzMzA5OSwiYXV0aF90aW1lIjoxNjMyOTMzMDgyLCJqdGkiOiIzZTk1NGRkMi0zMjhhLTQ3NzItYWQ2NS0xOWQ3NGM2MGZjZGEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGV2IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImMzNjFkMGVjLWM1MWItNGRmNy05MTA1LTVhOWUyZGViMmRjOCIsInR5cCI6IkJlYXJlciIsImF6cCI6InJlc291cmNlc2VydmVyIiwic2Vzc2lvbl9zdGF0ZSI6ImMyYWRiODIyLWQwY2ItNDY3MC1iYmNjLWU3NWJlOTkyYzg5OSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1kZXYiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVzb3VyY2VzZXJ2ZXIiOnsicm9sZXMiOlsiVVNFUiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiYzJhZGI4MjItZDBjYi00NjcwLWJiY2MtZTc1YmU5OTJjODk5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJ1c2VyX25hbWUiOiJ1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlciJ9.A66uqbRwsUL36GSGozZ7FC3x-M4SCYYLaABMdps-XneseP1saIjsTbHO2QrYq2HbD9jl6nKTYxJHjMdbsRJyY3VtM2mf1D8W24-u8y8qmGf1YNbtFfSTZyrUmwiACEv17onAT8wKgR0C4sdbVFETpRY12f2qQb0mM4ZkT9QQ5DYPBu6dnwyBVXLYJzn8kfmp7JB0OR6LsBTTtyh03t_xiRwb1nSALbUmwq7iUk9lTFEUuUZ182p05q3TKxy9b_kxrCh91EYoYWUdBEhRM4yHjrvN99T-MFpRVaCadyn2YibFbCeZHpsqUmgi-ghR3I70U70HGsL22FEAE4N9X5y_pg" \
-H "Cookie: JSESSIONID=8E34666DECDB395B1754FD08C5B385F2"
Response:
[
{
"name": "John Doe",
"age": 100
},
{
"name": "Jane Doe",
"age": 300
}
]
Next steps in this implementation are to customize the default such as
- Added a different timeout value to avoid any issues in distributed systems.
- Configure GrantedAuthority mapping
- .. and much more.
I have written a comprehensive tutorial about it.
You can check it out here
Top comments (0)