Securing APIs is an essential part of any serious web applications. Quarkus offers many useful extensions to secure REST APIs in multiple ways. Supported mechanisms are basic auth, JWT, etc. In this article, we go through how to secure REST APIs in Quarkus using Basic Auth.
In the last three articles we’ve built a User REST service, integrated with MySQL, and added Swagger and Swagger UI. The last piece of the puzzle is to secure those APIs. And this post is all about that.
The aim is to restructure and secure the following APIs,
GET /v1/users
– accessible to users withADMIN
orUSER
roleGET /v1/users/:id
– accessible to users withADMIN
orUSER
rolePOST /v1/users
– open (not secured)PUT /v1/users/:id
– accessible to users withADMIN
role onlyDELETE /v1/users/:id
– accessible to users withADMIN
role only
As you can see, we don’t change POST /v1/users
endpoint. Because we want new users to signUp
.
The authentication mechanism for this example is Basic Auth. So a user has to enter his username
and password
to interact with any APIs (except sign up). That means the existing user
entity ought to be adjusted accordingly (more on that later).
Besides that, we will define two roles USER
and ADMIN
respectively. This allows us to limit user modification and deletion to the ADMIN
role only.
Implementation
Now that we are clear with the problem space, let’s write some code 🙂
Adding Quarkus Security JPA dependency
As mentioned before Quarkus supports multiple authentication mechanisms. There is a dedicated library for each as follows,
Extension | Description |
quarkus-elytron-security-properties-file | Provides support for simple properties files that can be used for testing security. This supports both embedding user info in application.properties and standalone properties files. |
quarkus-security-jpa | Provides support for authenticating via JPA. |
quarkus-elytron-security-jdbc | Provides support for authenticating via JDBC. |
quarkus-elytron-security-oauth2 | Provides support for OAuth2 flows using Elytron. This extension will likely be deprecated soon and replaced by a reactive Vert.x version. |
quarkus-smallrye-jwt | A MicroProfile JWT implementation that provides support for authenticating using Json Web Tokens. This also allows you to inject the token and claims into the application as per the MP JWT spec. |
quarkus-oidc | Provides support for authenticating via an OpenID Connect provider such as Keycloak. |
quarkus-keycloak-authorization | Provides support for a policy enforcer using Keycloak Authorization Services. |
To implement Basic Auth with database support (JPA since we use Hibernate), we have to add quarkus-security-jpa
dependency to the project.
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
</dependency>
</dependencies>
Changing the User entity
We need to introduce three additional fields to the User
entity. These fields are: Username
, Password
, and Role
. For that, let’s change the User
entity.
@Entity
@Table(name = "users")
public class User {
// removed for brevity
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
@JsonbTransient
private String password;
@Column(name = "role", nullable = false)
private String role;
// removed for brevity
public void setPassword(String password) {
this.password = BcryptUtil.bcryptHash(password);
}
// removed for brevity
}
Since we don’t want to store plain raw passwords in the database, we call the hash function in the setPassword
method.
Note that the password
field is annotated with @JsonbTransient
. That’s to prevent returning password hash in the response body. Quarkus uses Jsonb
library, and the annotations differ from Jackson
. Using @JsonIgnore
doesn’t work.
Adding security annotation to User entity
We need to inform Quarkus about the authentication details. So it knows which table and which fields to query on authentication. That sounds complicated, but it’s not. All we have to do is to add some annotations.
@UserDefinition
@Entity
@Table(name = "users")
public class User {
// removed for brevity
@Username
@Column(name = "username", nullable = false)
private String username;
@Password
@Column(name = "password", nullable = false)
@JsonbTransient
private String password;
@Roles
@Column(name = "role", nullable = false)
private String role;
// removed for brevity
public void setPassword(String password) {
this.password = BcryptUtil.bcryptHash(password);
}
// removed for brevity
}
@UserDefinition
indicatesUser.java
is the user entity for authentication@Username
defines this field will be used as a username by Quarkus@Password
defines this field will be used as a password by Quarkus
Updating UserDTO
There is a static class in UserController
called UserDto
. It’s used for user creation and edition. This DTO should be updated accordingly to support username
, password
, and role
fields.
@Schema(name="UserDTO", description="User representation to create")
public static class UserDto {
@NotBlank
@Schema(title = "Username", required = true)
private String username;
@NotBlank
@Schema(title = "Password", required = true)
private String password;
@Schema(title = "User role, either ADMIN or USER. USER is default")
private String role;
@NotBlank
@Schema(title="User given name", required = true)
private String firstName;
@NotBlank
@Schema(title="User surname", required = true)
private String lastName;
@Min(value = 1, message = "The value must be more than 0")
@Max(value = 200, message = "The value must be less than 200")
@Schema(title="User age between 1 to 200", required = true)
private int age;
// removed for brevity
public User toUser() {
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setRole(StringUtils.isBlank(role) ? "USER" : StringUtils.upperCase(role));
user.setFirstName(firstName);
user.setLastName(lastName);
user.setAge(age);
return user;
}
}
Securing the UserController
The next step is to annotate the UserController
and secure them. Following what mentioned before we can annotate each endpoint accordingly as follows,
@RequestScoped
@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
@GET
@RolesAllowed({"USER", "ADMIN"})
public List<User> getUsers() {
return userService.getAllUsers();
}
@GET
@RolesAllowed({"USER", "ADMIN"})
@Path("/{id}")
public User getUser(@PathParam("id") int id) throws UserNotFoundException {
return userService.getUserById(id);
}
@POST
@PermitAll
public User createUser(@Valid UserDto userDto) {
return userService.saveUser(userDto.toUser());
}
@PUT
@RolesAllowed("ADMIN")
@Path("/{id}")
public User updateUser(@PathParam("id") int id, @Valid UserDto userDto) throws UserNotFoundException {
return userService.updateUser(id, userDto.toUser());
}
@DELETE
@RolesAllowed("ADMIN")
@Path("/{id}")
public Response deleteUser(@PathParam("id") int id) throws UserNotFoundException {
userService.deleteUser(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
}
Add authentication to Swagger UI
By now everything should work, except Swagger UI. That needs to be modified to support the authentication AKA login. For that, we have to add the final annotation on top of UserController
as follows,
@SecurityScheme(securitySchemeName = "Basic Auth", type = SecuritySchemeType.HTTP, scheme = "basic")
public class UserController {
// removed for brevity
}
The annotation shows the security name and schema.
Now if you run the project and open localhost:8080/swagger-ui
, you should see the REST APIs are secured. And to interact with them you need to have username and password.
You can find some predefined user in this file,
https://github.com/kasramp/quarkus-rest-example/blob/master/docker/db_tables/user.sql
And as usual, the complete implementation is available on GitHub at this link,
https://github.com/kasramp/quarkus-rest-example
That’s all for this post, I hope you have learned how to secure REST APIs in Quarkus using Basic Auth.
Inline/featured images credits
- Foreground picture (Security guard) by RyanMcGuire on Pixabay
- Quarkus logo by Quarkus