Today we will be building an Authentication Service. Starts with a simple REST service.
v1
Initial Design
- register user
- login
Requirement
- GraalVM - https://www.graalvm.org/downloads/
- Maven - https://maven.apache.org/download.cgi
Remember to set JAVA_HOME to the GraalVM location.
My environment
$ mvn -v
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: C:\development\apache-maven-3.6.3
Java version: 11.0.10, vendor: GraalVM Community, runtime: C:\Program Files\GraalVM\graalvm-ce-java11-21.0.0.2
Default locale: en_AU, platform encoding: Cp1252
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
Instructions
1. scaffold a new project
mvn io.quarkus:quarkus-maven-plugin:1.13.1.Final:create \
-DprojectGroupId=blog.technodeck \
-DprojectArtifactId=auth-service \
-Dverison=0.0.1-SNAPSHOT \
-Dextensions="resteasy-jackson,jdbc-h2,hibernate-orm-panache"
2. Configure application.properties
# configure your datasource
quarkus.datasource.db-kind = h2
quarkus.datasource.username = sa
quarkus.datasource.password = sa
quarkus.datasource.jdbc.url = jdbc:h2:mem:authdatabase
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create
3. Create User entity.
package blog.technodeck.auth.entity;
import javax.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
public class User extends PanacheEntity {
public String username;
public String password;
}
4. Create the AuthResource class with a POST endpoint.
package blog.technodeck.auth.resource;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import blog.technodeck.auth.entity.User;
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {
Logger logger = LoggerFactory.getLogger(getClass());
@Transactional
@POST
@Path("/register")
public User register(User user) {
logger.info("Registering user: {}", user.username);
user.persist();
return user;
}
}
5. Start the application
mvn compile quarkus:dev
You should see some output like:
2021-04-11 00:57:06,428 INFO [io.quarkus] (Quarkus Main Thread) auth-service stopped in 0.011s
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-04-11 00:57:07,720 INFO [io.quarkus] (Quarkus Main Thread) auth-service 0.0.1-SNAPSHOT on JVM (powered by Quarkus 1.13.1.Final) started in 1.287s. Listening on: http://localhost:8080
2021-04-11 00:57:07,723 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-04-11 00:57:07,724 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-h2, mutiny, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation]
2021-04-11 00:57:07,724 INFO [io.qua.dep.dev.RuntimeUpdatesProcessor] (vert.x-worker-thread-15) Hot replace total time: 1.495s
6. Time to get serious, call a Postman
Hit the POST request and you will get a created User returned with an ID
7. The basic functionality is working. Now we need to change the config a little bit to continue.
We now want to keep the data after reboot the application, so we put the DB file in the home folder.
# quarkus.datasource.jdbc.url = jdbc:h2:mem:authdatabase
quarkus.datasource.jdbc.url = jdbc:h2:~/auth-database
And only update the schema if needed.
#quarkus.hibernate-orm.database.generation = drop-and-create
quarkus.hibernate-orm.database.generation = update
8. Add "/list" and "/login" endpoints.
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public User login(
@FormParam("username") String username,
@FormParam("password") String password) {
logger.info("{} : {}", username, password);
return User.find("username = ?1 and password = ?2", username, password)
.singleResult();
}
@GET
@Path("/list")
public List<User> list() {
return User.findAll().list();
}
- Register POST (sane as step 6)
- List GET
- Login POST
10. Add AccessToken
Create AccessToken entity class.
package blog.technodeck.auth.entity;
import java.time.ZonedDateTime;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
public class AccessToken extends PanacheEntity {
public Long userId;
public String token;
public ZonedDateTime expires;
}
Update "/login" endpoint to be @Transactional and return AccessToken.
@Transactional
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public AccessToken login(
@FormParam("username") String username,
@FormParam("password") String password) {
logger.info("{} : {}", username, password);
User user = User.find("username = ?1 and password = ?2", username, password)
.singleResult();
AccessToken accessToken = new AccessToken();
accessToken.userId = user.id;
accessToken.token = UUID.randomUUID().toString();
accessToken.expires = ZonedDateTime.now().plusMinutes(30);
accessToken.persist();
return accessToken;
}
11. Do login again and now you will get a token JSON returned.
12. Try login with an incorrect username or password. You will get an default error page.
13. Wrapping our code with a try-catch clause and returns a friendly message.
try {
User user = User.find("username = ?1 and password = ?2", username, password)
.singleResult();
AccessToken accessToken = new AccessToken();
accessToken.userId = user.id;
accessToken.token = UUID.randomUUID().toString();
accessToken.expires = ZonedDateTime.now().plusMinutes(30);
accessToken.persist();
return accessToken;
} catch(Exception e) {
Response response = Response.status(Response.Status.NOT_FOUND)
.entity(e.getMessage())
.build();
throw new NotFoundException(response);
}
14. Finally we add a "/verify" endpoint for future use.
@GET
@Path("/verify/{id}/{token}")
public void verify(@PathParam("id") Long id, @PathParam("token") String token) {
AccessToken accessToken =
AccessToken.find(
"userId = ?1 and token = ?2", Sort.by("id").descending(), id, token
).firstResult();
if(accessToken == null) {
logger.info("Invalid token.");
throw new ForbiddenException("Invalid token");
}
if(accessToken.expires.isBefore(ZonedDateTime.now())) {
logger.info("Token expired.");
throw new ForbiddenException("Token Expired");
}
}
A valid token will return HTTP 204 empty content.
The invalid token will get an HTTP 403 error.
Conclusion
I have demonstrated how to use quarkus to build a simple authentication service. Using quarkus, the code is so minimal and clean. It is just amazing.
Source Code: https://github.com/devilkazuya99/auth-service
References:
No comments:
Post a Comment