Java Jersey Auth Filter and JDBI Injection
I started with a dummy jersey project and later added jdbi to that. You can see both posts below http://msdalp.github.io/2018/03/28/Java-Jersey-2.26-Tutorial/
http://msdalp.github.io/2018/04/07/Java-Jersey-2.26-JDBI/
I will continue from the last part which you can pull from https://github.com/msdalp/jersey-jdbi-sample on master
branch. All work done on this post will go to auth_filter-dao
branch.
Now I want to add these in order:
- Add a proper user class with password and write jdbi implementation for that.
- Add a login method with token.
- Different roles for users so later we can utilize Jersey’s
@RolesAllowed()
for role based service calls. - Add a filter to check tokens to see if user is authorized or not.
- Implement user role details
- Write a Dao annotation so we can inject Jdbi interfaces easily.
For the simplest case I need a User model with unique username, a bcrypt hashed password, and a role selected from ‘admin’, ‘mod’, or ‘user’ with default value ‘user’.
Create an auth_user
table with sql provided above. In general I don’t like using user
as the table name because it is possible to mixing up with mysql system tables or other services. id’s are auto generated and username
is unique. password
is defined as binary(60) since storing the result of bcrypt hashed password but not the text itself. Lastly an enum for user roles as admin,mod,and default user itself.
Representation of that model in java can be defined as above and make sure getter and setter methods are implemented, otherwise automatic JSON conversion will fail (depends on the library). Jackson uses @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
for write only fields which will be only added while reading from JSON to object but not the other way. JDBI interface for this class is given below and it is pretty straightforward.
We also want to have a token table so we can store tokens assigned to users after login. token
itself will be varchar(32) since we will be using BigInteger(130, secureRandom).toString(32)
for generating tokens. expire_at
will be given as plus one day by default and user_id
is the token owner.
Simple token Dao model with retrieving all tokens, a single token, or inserting a new one.@GetGeneratedKeys
returns the inserted id of token which will be returned back to user on service call.
These will be enough for creating the User resource class with methods add, update, and delete. But first we need a Bcrypt class for hashing user passwords. There are few implementations or you can have a class for that which are quite easy to find on Github. I am going to add JBcrypt to my dependencies and use it from there.
Simple REST logic for the User resource.
GET users = HTTP 200 get all users
GET users/{userId} = HTTP 200 get a specific user
POST users = HTTP 201 creates a new user and accept user body as json
PUT users/{userId} = HTTP 201 update the user specified on user id and and accept user body as json
DELETE users/{userId} = HTTP 204 delete a specific user
Check the methods below and read the details through the comments.
Use Postman or any similar tool to test these services. Create a user with POST request using the user json below and use GET to check the user you have created.
It should return a result similar to this and also you can try updating and deleting user service calls.
After creating user we have all tools we need to have a proper login structure. Create an AuthResource
class and have a POST
method for login call.
Simply check if user input is valid and if not throw error. Later check if user exists and the password is valid. A POST
method with consuming user model as json or form-url-encoded
formatted username and password fields are both feasible. Both works quite well so pick one that you are comfortable with. I will be using JSON consuming one. Again you can go through the comments to see the explanations.
Token is obviously useless right now without a proper filter to check if user has a valid token. Jersey provides ContainerRequestFilter
and by implementing this interface you can have a filter method. It enables you to check or validate headers and other stuff before processing whole request. For a simple token check read the Authorization
header and get the token value. If there is no token or the token we got is invalid and abort the request with HTTP 401
not authorized. Also we will allow this auth
path so people can login without a token.
You can inject resources with @Context
annotation. There are some resources already available which are provided by Jersey like HttpServletRequest
, HttpServletResponse
or UriInfo
. Also you can define your custom resources to inject database source or SecurityContext
. By utilizing these ServletRequest
and ContainerRequest
we will get Authorization
header and the top part of the request path. If you just check token and return error when it is not valid then there will be no way for users to login properly. Therefore you need to exclude the auth
resource from the token control (make it public access). Also I am defining errors as just classes so I can show it quickly but a proper way is having a GenericExceptionMapper
and catch errors from there for logging properly and returning generic errors.
Now call users
service again without a token and it will return 401
error to you saying Auth token is not valid
. After adding the tokens we got from earlier calls it will work again properly.
Next add specific roles to user so normal users can not add or delete users. It will be possible thanks to SecurityContext
that Jersey provides and we will use the role enum we defined earlier.
Start with implementing Principal
interface as AuthUser implements Principal
. You will implement one method which returns the name of the user. Also make sure Role
enum is public so it is accessible in SecurityContext
.
Now we can create a SecurityUser
which implements SecurityContext
. It will ask you to add isSecure
, authenticationScheme
, isUserInRole
, and getPrincipal
. We implemented Principal
on AuthUser
earlier so we can return that user in here as principal.
We added user into this object and it will be our principal. Authentication scheme is “token” or any other name you prefer. Set isSecure as true and compare the role given to us with the user’s role and return true if they are equal.
This is just a class though. How do we put this into our Context
? As expected it will require us to register RolesAllowed
as register (RolesAllowedDynamicFeature.class);
in our RestApp
class. Jersey requires registering features before using it unless it is enabled by default. After this, in the filter method if it is actually a valid token then get the user and set it as SecurityContext
. I updated filter method a bit so we can just return if it is an auth
request.
With this filter method when it is an auth
request we just continue. If token header is empty or it is not valid then directly throw no auth error. At last it is guaranteed that this is a valid token so get the user info and set it as the current security context. Whenever you call @Context SecurityContext securityContext;
in any resource you will be able to access to the current user.
Update the UserResource
and add @RolesAllowed({"admin","mod"})
on top of it to make path users/*
to be only accessible by mods and admins. When you try to access it with user role, it will throw 403 Forbidden
.
Also you can allow a sub path to user role by just putting @RolesAllowed({"user"})
on top of the method. Let’s say we want to give access of retrieving users to user role. Adding this will only affect this specific method and now our default user can access to /users
call directly.
At last I wanted to have a JDBI annotation so we can just inject it by using @Dao UserDao dao
on methods or constructors rather than bunch of DbiManager.ondemand etc.
However Jersey 2.26 broke some api calls and apparently there is no AbstractValueFactoryProvider
class anymore. Also no clue what is the replacement and can’t ask on Github at this stage because project is archived and will be transferred to Eclipse Jakarta.
You can check all codes on https://github.com/msdalp/jersey-jdbi-sample/tree/auth_filter-dao I think I will have one or two more posts about Jersey and it will cover:
* Using Redis injection for token validations and cache
* Implement GenericExceptionMapper for catching exceptions and errors properly
* And maybe lastly implement web sockets on jersey.
However I am not really keen about this and I might just move on to the Spring Boot and have some examples on it.