1. Introduction
Nowadays, we have more and more concerns about performance, and at the same time, we want to know how systems can communicate fast and reliably. Many times we want to send information and keep it confidential and safe as much as possible. Sensitive data sometimes must also move through the web publicly and trigger actions at the other end of the wire. Mostly, we want to generate actions that will cause data mutations. In these cases, we are not only looking at protecting our data. We want to make sure that the actions triggered by sending our data are trusted. We can protect our data in several ways. Most commonly, we send the data via a TLS
(Transport Layer Security) secure connection. That will ensure that our data gets Encrypted through the wire. We use Certificates to create trusting relationships between two parties and achieve this.
In this article, I want to discuss the JWT
standard and further see how can we integrate JWT
into a common Enterprise
application. In this case, we will have a look at KumuluzEE
.
Let’s have a look at some basic concepts. JWT
or JSON Web Token, or better yet, JavaScript Object Notation Web Token, is a standard defined in RFC7519. This standard has been, like all RFC
(Request For Comments) standards, defined, written, and published by the IETF
(Internet Engineering Task Force). It can be defined in multiple ways. Generally, we can say that JWT
is a compact, safe form of transmitting claims between two parties. One way of simplifying what a claim is, is basically to describe it as a name/value pair that contains information. We need this information to guarantee a few important aspects of our internet communication. We need to make sure that the information we receive is validated and trusted in the first instance. Then we need to validate it. This is basically it.
In order to implement this standard, we can use several frameworks that can help us implement a Java enterprise application. Spring Boot is being used widely. Many times it’s also being wrapped under another name in propriety software from certain organizations like banks and other financial organizations. For our example, I decided to do something different. Instead of Spring Boot, we are going to have a look at an example with KumuluzEE
. The point is to identify exactly what JWT
is and what it looks like. Java Enterprise Applications are basically applications that can be deployed in an application server or just run on their own via the use of an embedded server. As an example, Spring Boot applications run on an embedded Tomcat server. In this article, our focus will be set on KumuluzEE
. Just like Spring Boot it also contains an embedded server. Except that in this case it is called Jetty. This is used in combination with Weld in order to provide CDI(Context Dependency Injection). All Java EE
and Jakarta EE
technology standards are compatible with this framework
.
2. Case Example
In order to exemplify how JWT
works in its basic form, I had to think of a way to present it. Classic examples where security is a concern are banks. However, making a whole bank application to show how JWT
works would be a waste of time and maybe too many concepts would be involved. Instead, what I made is a very simple banking system. Our main concern is to show how data flows through the wire and how users get access to certain areas of our application. I’m also not going to discuss TLS or how we can send encrypted information through the wire. We will keep our focus on JWT
in its purest form.
Our case is a banking system used by a group defending nature and the environment. This is just a fun way to show how JWT
works. The main character of this League of Nature is Lucy, who is becoming a common character in all my articles.
3. Architecture
Before we start, let’s just sketch our running application. It’s a very simple application, but it’s still a good thing to draw it:
The reason why this is so simple is that since JWT
gets checked on every request and every request gets verified against the public key, then we know that as long as we send the correct token on every request we will be able to get through. JWT
can be integrated with OAuth2, Okta SSO, or any other authorization mechanism. In this case, what we are doing is establishing authentication and authorization. In our application, we are going to use JWT
and with it, authenticate our message using a signature. We won’t log into the application though. Instead, we will authorize users to use our application after successful authentication. At this point, it’s easy to see that JWT
at its core is actually a very small part of a full application. Nonetheless, some functionality needs to be added. These are the Resources we need:
- Balance system
- Credit system
Let’s just say that our basic system will only register money and credit requests. Essentially it will just accumulate values. Let’s also assume that some people will be able to get credit and others won’t. Some people will be able to store money and other people will be able to get credit.
4. Choosing technologies
As mentioned in the introduction, we will use KumuluzEE
as our enterprise application framework, and we will implement an ultra-basic application in a way that we can look at basic JWT
terminology and concepts.
Make sure to have the correct Java version. At this stage, we will need minimal Java 17 SDK installed. We will need maven, git, a Java-compatible IDE like IntelliJ, and a shell of some sort.
5. Setup
In order to begin our application, we have a few KumuluzEE
dependencies. This is mainly because KumuluzEE
, just like Spring Boot needs a couple of dependencies. Let’s have a look at the POM file briefly:
<dependencies>
<dependency>
<groupId>com.kumuluz.ee.openapi</groupId>
<artifactId>kumuluzee-openapi-mp</artifactId>
</dependency>
<dependency>
<groupId>com.kumuluz.ee.openapi</groupId>
<artifactId>kumuluzee-openapi-mp-ui</artifactId>
</dependency>
<dependency>
<groupId>com.kumuluz.ee</groupId>
<artifactId>kumuluzee-microProfile-3.3</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-assertions-core-jvm</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Let’s discuss a few dependencies briefly. As you read this, please follow our pom.xml
file from top to bottom. This is important in order to understand the following explanation.
We need a package of dependencies to make our application work. , Fortunately, KumuluzEE
, provides us with Microprofile libraries which contain basic standard bundles to start this application. This is all contained in the KumuluzEE
-Microprofile library. In order to be able to configure our app with all the JWT
parameters we need, we have to add a MicroProfile library to it. At the same time, we need a JSON processing library. This will be what Johnson Core does. We need of course the core of KumuluzEE
to work. Jetty is the underlying server that runs the KumuluzEE
framework. This is why we need it in our dependencies. Considering we need CDI
, we also need a library that supports it. In order to enable our REST endpoints, we need the rest library of KumuluzEE
. In order to get our API, we then need a Geronimo library. This will ensure that we have an implementation of JSR-374
available. We also need to interpret our JWT
and its JSON-formatted
contents.
Lombok is not really needed per se. It just makes everything beautiful and shiny! Logback is also important to have so that we can better interpret logs and understand our results.
Let’s now take a look at our resources
folder.
To start off let’s first understand what we expect to find in this folder. We need to configure our application with something related to JWT
, Logback and finally, we need to say something about the beans we are going to create.
Let’s look at the simplest file there. The beans.xml can be found in META-INF:
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
xmlns:weld="http://jboss.org/schema/weld/beans"
bean-discovery-mode="all">
<weld:scan>
<weld:exclude name="org.jesperancinha.fintech.model.Accounts"/>
</weld:scan>
</beans>
This is just a typical and as you may be thinking now, a bit of an old file. At this point, the idea is just to get KumuluzEE
running. We do have an exclude action. This tells Weld not to consider class Accounts in its scanning for beans action. This is important because with the implementation we are using, Weld
will basically consider every class with an empty constructor as a bean. We will see later on why we don’t want Accounts to be considered a bean. For the moment let’s keep in mind that we are making requests under the Request scope. This is logical because every request can have a different user.
Let’s now see how "logback
" is implemented. It is also found in META-INF
:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
This is just a very straightforward configuration for our logs
.
Finally, maybe the most important file of our application. This is the config-template. At this point, it is important to note that some of the files I’ve created in this project are part of a template structure. I will explain more about that later. This template file is supposed to be turned into a config.yml file which will be read by MicroProfile. This file is located at the root of resources:
kumuluzee:
name: your-financeje-banking
version: 1.0.0
jwt-auth:
public-key: {{ publicKey }}
issuer: {{ issuer }}
healthy: true
We will see later on what exactly all of these properties actually mean. All of them are self-explanatory. The publicKey and issuer are all parameters which will be replaced. We will explore that later on. Our bash scripts will make sure they get replaced.
We are almost ready to go coding, but first, let’s have a look at our JWT
token structure.
6. Hands-on code
Let’s make our very small application. This section will explain how can we get our application to work with JWT
. What we want to see is if we can specify users to access some of our REST
methods and not others.
One of the ways to start looking at this code is to first have a look at our plain JWT
token. Here is our admin example:
{
"iss": "joaofilipesabinoesperancinha",
"jti": "01MASTERFINANCE",
"sub": "admin",
"aud": "nature",
"upn": "admin",
"groups": [
"user",
"admin",
"client",
"credit"
],
"user_id": 1,
"access": "TOP",
"name": "Admin"
}
Each one of these names in our JSON
is referred to as claims. In our example, we see a few Reserved claims:
- "
iss
" — This is the issuer of the token. We can arbitrarily choose a value for this. The value of this parameter must match the issuer variable to be replaced in the config.yml we have seen before. - "
jti
" — This is a unique identifier of the token. We can for example use this claim to prevent a token from being used twice or more times. - "
sub
" — This is the subject of the token. It can be the user or anything we like. It’s important to keep in mind that this can also be used as an identifier, key, naming, or anything we want. - "
upn
" — User principal name. This is used to identify the principal the user is using. - "
groups
" — This is an array of the groups the current user belongs to. Essentially this will determine what a request with this token can do. In our token, we then see a few Custom claims. We can use this just as well as the Reserved claims - "
user_id
" — We will use this to set the user id. - "
access
" — We will determine the access level of the user. - "
name
" — The name of the user.
7. Hands-on code
Let’s make a recap of what we know so far. We know we will communicate with tokens with a structure we have determined. Further, we have set up the configuration of our application, the logback configuration and finally, we set up a custom configuration for the enterprise bean lookup.
Let’s look at the package model. Here we will find 3 classes. These classes basically just represent an aggregation of accounts and the representation between client
and account
. This way we can start by looking at kotlin file Model.kt where Client
is located at:
data class Client constructor(
@JsonProperty
var name: String ?= null
)
This first model class is the representation of our client. Our client
for our case only has a name. This is the username represented by the "jwt
" attribute name.
Further, we have Account
:
data class Account(
@JsonProperty
val accountNumber: String?,
@JsonProperty
val client: Client? = null,
@JsonProperty
var currentValue: BigDecimal = BigDecimal.ZERO,
@JsonProperty
var creditValue: BigDecimal = BigDecimal.ZERO
) {
fun addCurrentValue(value: Long) = Account(
accountNumber, client, currentValue
.add(BigDecimal.valueOf(value)), creditValue
)
fun addCreditValue(value: Long): Account = Account(
accountNumber, client, currentValue, currentValue
.add(BigDecimal.valueOf(value))
)
}
In this class, we basically set up an accountNumber, a client, a currentValue and finally a creditValue. Notice that we are defaulting all values to 0. We are also using BigDecimal, purely because we are dealing with money. Money must be exact and cannot suffer system round-ups or round-downs. This means in other words and as an example that a number such as 0.0000000000000000000000000000000000000000000000000001
euros must remain that number all the time. Also, we want to add values to our account. This is where the method addCurrentValue comes to exist. For the same reasons, we will also top up our credit with the addCreditValue
.
Finally, in the last piece of our data setup we come across class Accounts
:
open class Accounts constructor(
open val accountMap: MutableMap<String, Account> = mutableMapOf()
)
This is essentially just an aggregator of all our accounts. We will use its map content to mimic the behavior of a database.
Now let’s look at the controller package. This is where we create our application running with our data model. First, let’s have a look at class BankApplication
:
@LoginConfig(authMethod = "MP-JWT")
@ApplicationPath("/")
@DeclareRoles("admin", "creditor", "client", "user")
class BankApplication : Application()
With this, we are saying 3 important things. With the LoginConfig annotation, we define it to use and understand JWT
tokens according to MicroProfile. The ApplicationPath defines the application root. This is where the URL of the application will begin. In our example, it will be HTTP://localhost:8080. Finally, the DeclareRoles defines the roles that are going to be used and accepted by our application. Roles and Groups are interchangeable terms in this situation.
In order for injection to work efficiently, we create an annotation specific to identify the account map:
annotation class AccountsProduct
Next, we create a cache object factory AccountsFactory:
class AccountsFactory : Serializable {
@Produces
@AccountsProduct
@ApplicationScoped
fun accounts(): Accounts = Accounts(mutableMapOf())
companion object {
@Throws(JsonProcessingException::class)
fun createResponse(
currentAccount: Account,
name: JsonString,
accounts: Accounts,
log: Logger,
objectMapper: ObjectMapper,
principal: Principal?,
jsonWebToken: JsonWebToken?
): Response {
val jsonObject = Json.createObjectBuilder()
.add("balance", currentAccount.currentValue)
.add("client", name)
.build()
accounts.accountMap[name.string] = currentAccount
log.info("Principal: {}", objectMapper.writeValueAsString(principal))
log.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken))
return Response.ok(jsonObject)
.build()
}
}
}
This factory is the reason why we disabled lookup specifically for Accounts
. Instead of allowing the lookup process to create a bean, we create the aggregator instance ourselves. Using the Produces annotation, allows us to create the bean. Using our custom annotation, AccountsProduct, we make the use of this bean more specific. Finally, by using ApplicationScoped
, we define its scope as being the Application
scope. In other words, the account aggregation bean will behave as a singleton object across the application.
The "createResponse
" is just a generic method to create JSON responses.
What we now need is two "Resources". This is basically the same as "Controllers
" in Spring. It is a different name, but it has exactly the same usage.
Let’s look at the AccountsResource
class:
@Path("accounts")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON)
open class AccountResource {
@Inject
@AccountsProduct
open var accounts: Accounts? = null
@Inject
open var principal: Principal? = null
@Inject
open var jsonWebToken: JsonWebToken? = null
@Inject
@Claim("access")
open var access: JsonString? = null
@Claim("iat")
@Inject
open var iat: JsonNumber? = null
@Inject
@Claim("name")
open var name: JsonString? = null
@Inject
@Claim("user_id")
open var userId: JsonNumber? = null
@POST
@RolesAllowed("admin", "client", "credit")
@Throws(JsonProcessingException::class)
open fun createAccount(): Response = createResponse(
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account(
client = Client(name = requireNotNull(name).string),
accountNumber = UUID.randomUUID().toString()
)
)
@POST
@RolesAllowed("admin", "user")
@Path("user")
@Throws(JsonProcessingException::class)
open fun createUser(): Response {
return createResponse(
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account(
client = Client(name = requireNotNull(name).string),
accountNumber = UUID.randomUUID().toString()
)
)
}
@GET
@RolesAllowed("admin", "client")
@Throws(JsonProcessingException::class)
open fun getAccount(): Response? {
return createResponse(
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError()
.build()
)
}
@PUT
@RolesAllowed("admin", "client")
@Consumes(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun cashIn(transactionBody: TransactionBody): Response? {
val userAccount =
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError()
.build()
val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0)
requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount
return createResponse(currentAccount)
}
@GET
@Path("all")
@Produces(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun getAll(): Response? {
val allAccounts = ArrayList(
requireNotNull(accounts).accountMap
.values
)
logger.info("Principal: {}", objectMapper.writeValueAsString(principal))
logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken))
return Response.ok(allAccounts)
.build()
}
@GET
@Path("summary")
@Throws(JsonProcessingException::class)
open fun getSummary(): Response? {
val totalCredit = requireNotNull(accounts).accountMap
.values
.map(Account::currentValue)
.stream()
.reduce { result, u -> result.add(u) }
.orElse(BigDecimal.ZERO)
val jsonObject = Json.createObjectBuilder()
.add("totalCurrent", totalCredit)
.add("client", "Mother Nature Dream Team")
.build()
logger.info("Summary")
logger.info("Principal: {}", objectMapper.writeValueAsString(principal))
logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken))
return Response.ok(jsonObject)
.build()
}
@GET
@RolesAllowed("admin", "client")
@Path("jwt")
open fun getJWT(): Response? {
val jsonObject = Json.createObjectBuilder()
.add("jwt", requireNotNull(jsonWebToken).rawToken)
.add("userId", requireNotNull(userId).doubleValue())
.add("access", requireNotNull(access).string)
.add("iat", requireNotNull(iat).doubleValue())
.build()
return Response.ok(jsonObject)
.build()
}
@Throws(JsonProcessingException::class)
private fun createResponse(currentAccount: Account): Response =
AccountsFactory.createResponse(
currentAccount,
requireNotNull(name),
requireNotNull(accounts),
logger,
objectMapper,
principal,
jsonWebToken
)
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
val logger: Logger = LoggerFactory.getLogger(AccountResource::class.java)
}
}
Take a moment to look at this class more in detail. The Path
annotation defines how to reach this resource from the root. Remember that we are using "/" as the root. In this case, "accounts" is our root access point for this resource. All of our recourses, in our case only two are running with scope RequestResource. With annotation Produces determines that all responses to all requests regardless of their type will take the form of JSON formatted messages.
To inject our aggregator
we just use the combination of the Inject annotation and AccountsProduct
annotation:
@Inject
@AccountsProduct
open var accounts: Accounts? = null
This matches what we defined in the factory.
Further, we are also injecting two important elements of security. A principal
and the jsonWebToken
:
@Inject
open var principal: Principal? = null
@Inject
open var jsonWebToken: JsonWebToken? = null
Both JsonWebToken
and Principal
will be the same, and we will see that in our logs.
In our resources, we can always inject claims from a request with a certain token:
@Inject
@Claim("name")
open var name: JsonString? = null
@Inject
@Claim("user_id")
open var userId: JsonNumber? = null
This is accomplished with the combination of the Inject
and Claim
annotations. The name placed under the Claim
annotation defines which claim we want to inject. We have to be careful with the type we define our parameters with. In our example,r we only need JsonString
and JsonNumber
types.
First, let’s look at how we are creating accounts and users:
@POST
@RolesAllowed("admin", "client", "credit")
@Throws(JsonProcessingException::class)
open fun createAccount(): Response = createResponse(
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account(
client = Client(name = requireNotNull(name).string),
accountNumber = UUID.randomUUID().toString()
)
)
@POST
@RolesAllowed("admin", "user")
@Path("user")
@Throws(JsonProcessingException::class)
open fun createUser(): Response {
return createResponse(
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: Account(
client = Client(name = requireNotNull(name).string),
accountNumber = UUID.randomUUID().toString()
)
)
}
Creating accounts and users
The purpose here is to be able to separate methods and give them different permissions. In our example, they both just create an account, but it’s important to notice that only users with roles user can use the createUser method. In the same way, only users with roles of client and credit can access the method createAccount.
Let’s now look in detail at the PUT request method of this resource:
@PUT
@RolesAllowed("admin", "client")
@Consumes(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun cashIn(transactionBody: TransactionBody): Response? {
val userAccount =
requireNotNull(accounts).accountMap[requireNotNull(name).string] ?: return Response.serverError()
.build()
val currentAccount = userAccount.addCurrentValue(transactionBody.saldo?: 0)
requireNotNull(accounts).accountMap[requireNotNull(name).string] = currentAccount
return createResponse(currentAccount)
}
Cashing In
We know that annotation PUT
indicates that this method is only accessible with requests of type PUT
. Annotation Path then tells Jetty that the path to this method is a value. This is also known as a PathParam
. Finally, we can define this method be only allowed to be used by users with roles admin or client. The input value is then passed on to our Long value variable by the usage of the PathParam.
If we don’t define any roles, then any user with the right token will be able to access these methods.
The CreditResource
is implemented in the same way:
@Path("credit")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON)
open class CreditResource {
@Inject
@AccountsProduct
open var accounts: Accounts? = null
@Inject
open var principal: Principal? = null
@Inject
open var jsonWebToken: JsonWebToken? = null
@Inject
@Claim("access")
open var access: JsonString? = null
@Inject
@Claim("iat")
open var iat: JsonNumber? = null
@Inject
@Claim("name")
open var name: JsonString? = null
@Inject
@Claim("user_id")
open var userId: JsonNumber? = null
@GET
@RolesAllowed("admin", "credit")
@Throws(JsonProcessingException::class)
open fun getAccount(): Response = requireNotNull(accounts).let { accounts ->
createResponse(
accounts.accountMap[requireNotNull(name).string] ?: return Response.serverError().build()
)
}
@PUT
@RolesAllowed("admin", "credit")
@Consumes(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun cashIn(transactionBody: TransactionBody) = requireNotNull(accounts).let { accounts ->
requireNotNull(name).let { name ->
accounts.accountMap[name.string] = (accounts.accountMap[name.string] ?: return Response.serverError()
.build()).addCreditValue(transactionBody.saldo?: 0L)
createResponse(
(accounts.accountMap[name.string] ?: return Response.serverError()
.build()).addCreditValue(transactionBody.saldo?: 0L)
)
}
}
@GET
@Path("all")
@Produces(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun getAll(): Response? {
val allAccounts = ArrayList(
requireNotNull(accounts).accountMap
.values
)
logger.info("Principal: {}", objectMapper.writeValueAsString(principal))
logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken))
return Response.ok(allAccounts)
.build()
}
@GET
@Path("summary")
@Produces(MediaType.APPLICATION_JSON)
@Throws(
JsonProcessingException::class
)
open fun getSummary(): Response? {
val totalCredit = requireNotNull(accounts).accountMap
.values
.map(Account::creditValue)
.stream()
.reduce { total, v -> total.add(v) }
.orElse(BigDecimal.ZERO)
val jsonObject = Json.createObjectBuilder()
.add("totalCredit", totalCredit)
.add("client", "Mother Nature Dream Team")
.build()
logger.info("Summary")
logger.info("Principal: {}", objectMapper.writeValueAsString(principal))
logger.info("JSonWebToken: {}", objectMapper.writeValueAsString(jsonWebToken))
return Response.ok(jsonObject)
.build()
}
@GET
@RolesAllowed("admin", "client")
@Path("jwt")
open fun getJWT(): Response? {
val jsonObject = Json.createObjectBuilder()
.add("jwt", requireNotNull(jsonWebToken).rawToken)
.add("userId", requireNotNull(userId).doubleValue())
.add("access", requireNotNull(access).string)
.add("iat", requireNotNull(iat).doubleValue())
.build()
return Response.ok(jsonObject)
.build()
}
@Throws(JsonProcessingException::class)
private fun createResponse(currentAccount: Account): Response {
return AccountsFactory.createResponse(
currentAccount,
requireNotNull(name),
requireNotNull(accounts),
logger,
objectMapper,
principal,
jsonWebToken
)
}
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
val logger: Logger = LoggerFactory.getLogger(CreditResource::class.java)
}
}
The only difference is that instead of using roles admin
and client
we are now using admin
and credit
roles. Also, notice that accounts for users will never be created in this resource
. That’s only possible via the account’s resource
.
Now that we know how the code is implemented let’s first recap which methods have we made available in our REST
service.
8. Application Usage
Let’s check the list of the services being used:
Type,URL,Payload,Result,Roles Allowed
POST,http://localhost:8080/accounts,n/a,Created account,admin/client/credit
POST,http://localhost:8080/accounts/user,n/a,Created user,admin/user
GET,http://localhost:8080/accounts,n/a,Matching account,admin/client
PUT,http://localhost:8080/accounts,{saldo: Long}, Current Balance,admin/client
GET,http://localhost:8080/accounts/all,n/a,All current accounts,All
GET,http://localhost:8080/accounts/summary,n/a,Sum of all balances,All
GET,http://localhost:8080/credit,n/a,Matching account,admin/client
PUT,http://localhost:8080/credit,{saldo: Long}, Current Credit,admin/client
GET,http://localhost:8080/credit/all,n/a,All credits,All
GET,http://localhost:8080/credit/summary,n/a,Sum credits,All
All Endpoints
9. Generating test environment
I have created a bash
file in the root folder. This file is called "setupCertificates.sh". Let’s have a look into it to have an idea of what it does:
#!/bin/bash
mkdir -p your-finance-files
cd your-finance-files || exit
openssl genrsa -out baseKey.pem
openssl pkcs8 -topk8 -inform PEM -in baseKey.pem -out privateKey.pem -nocrypt
openssl rsa -in baseKey.pem -pubout -outform PEM -out publicKey.pem
echo -e '\033[1;32mFirst test\033[0m'
java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \
-p ../jwt-plain-tokens/jwt-token-admin.json \
-key ../your-finance-files/privateKey.pem >> token.jwt
CERT_PUBLIC_KEY=$(cat ../your-finance-files/publicKey.pem)
CERT_ISSUER="joaofilipesabinoesperancinha"
echo -e "\e[96mGenerated public key: \e[0m $CERT_PUBLIC_KEY"
echo -e "\e[96mIssued by: \e[0m $CERT_ISSUER"
echo -e "\e[96mYour token is: \e[0m $(cat token.jwt)"
cp ../your-financeje-banking/src/main/resources/config-template ../your-financeje-banking/src/main/resources/config_copy.yml
CERT_CLEAN0=${CERT_PUBLIC_KEY//"/"/"\/"}
CERT_CLEAN1=${CERT_CLEAN0//$'\r\n'/}
CERT_CLEAN2=${CERT_CLEAN1//$'\n'/}
CERT_CLEAN3=$(echo "$CERT_CLEAN2" | awk '{gsub("-----BEGIN PUBLIC KEY-----",""); print}')
CERT_CLEAN4=$(echo "$CERT_CLEAN3" | awk '{gsub("-----END PUBLIC KEY-----",""); print}')
CERT_CLEAN=${CERT_CLEAN4//$' '/}
echo -e "\e[96mCertificate cleanup: \e[0m ${CERT_CLEAN/$'\n'/}"
sed "s/{{ publicKey }}/$CERT_CLEAN/g" ../your-financeje-banking/src/main/resources/config_copy.yml > ../your-financeje-banking/src/main/resources/config_cert.yml
sed "s/{{ issuer }}/$CERT_ISSUER/g" ../your-financeje-banking/src/main/resources/config_cert.yml > ../your-financeje-banking/src/main/resources/config.yml
rm ../your-financeje-banking/src/main/resources/config_cert.yml
rm ../your-financeje-banking/src/main/resources/config_copy.yml
echo -e "\e[93mSecurity elements completely generated!\e[0m"
echo -e "\e[93mGenerating tokens...\e[0m"
TOKEN_FOLDER=jwt-tokens
mkdir -p ${TOKEN_FOLDER}
#
CREATE_ACCOUNT_FILE=createAccount.sh
CREATE_USER_FILE=createUser.sh
SEND_MONEY_FILE=sendMoney.sh
ASK_CREDIT_FILE=askCredit.sh
TOKEN_NAME_VALUE=tokenNameValue.csv
echo "#!/usr/bin/env bash" > ${CREATE_ACCOUNT_FILE}
chmod +x ${CREATE_ACCOUNT_FILE}
echo "#!/usr/bin/env bash" > ${CREATE_USER_FILE}
chmod +x ${CREATE_USER_FILE}
echo "#!/usr/bin/env bash" > ${SEND_MONEY_FILE}
chmod +x ${SEND_MONEY_FILE}
echo "#!/usr/bin/env bash" > ${ASK_CREDIT_FILE}
chmod +x ${ASK_CREDIT_FILE}
for item in ../jwt-plain-tokens/jwt-token*.json; do
if [[ -f "$item" ]]; then
filename=${item##*/}
per_token=${filename/jwt-token-/}
token_name=${per_token/.json/}
cp "${item}" jwt-token.json
java -jar ../your-finance-jwt-generator/target/your-finance-jwt-generator.jar \
-p jwt-token.json \
-key ../your-finance-files/privateKey.pem > token.jwt
cp token.jwt ${TOKEN_FOLDER}/token-"${token_name}".jwt
token=$(cat token.jwt)
echo "# Create account: ""${token_name}" >> ${CREATE_ACCOUNT_FILE}
echo "echo -e \"\e[93mCreating account \e[96m${token_name}\e[0m\"" >> ${CREATE_ACCOUNT_FILE}
echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X POST >> ${CREATE_ACCOUNT_FILE}
echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_ACCOUNT_FILE}
echo "# Create user: ""${token_name}" >> ${CREATE_USER_FILE}
echo "echo -e \"\e[93mCreating user \e[96m${token_name}\e[0m\"" >> ${CREATE_USER_FILE}
echo curl -i -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts/user -X POST >> ${CREATE_USER_FILE}
echo "echo -e \"\e[93m\n---\e[0m\"" >> ${CREATE_USER_FILE}
echo "# Send money to: "${token_name} >> ${SEND_MONEY_FILE}
echo "echo -e \"\e[93mSending money to \e[96m${token_name}\e[0m\"" >> ${SEND_MONEY_FILE}
echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/accounts -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'" >> ${SEND_MONEY_FILE}
echo "echo -e \"\e[93m\n---\e[0m\"" >> ${SEND_MONEY_FILE}
echo "# Asking money credit to: "${token_name} >> ${ASK_CREDIT_FILE}
echo "echo -e \"\e[93mAsking credit from \e[96m${token_name}\e[0m\"" >> ${ASK_CREDIT_FILE}
echo curl -i -H"'Content-Type: application/json'" -H"'Authorization: Bearer ""${token}""'" http://localhost:8080/credit -X PUT -d "'{ \"saldo\": "$((1 + RANDOM % 500))"}'">> ${ASK_CREDIT_FILE}
echo "echo -e \"\e[93m\n---\e[0m\"" >> ${ASK_CREDIT_FILE}
echo "${token_name},${token}" >> ${TOKEN_NAME_VALUE}
fi
done
Environment generation
Please follow the file as I explain what it does. This is important so that we understand exactly what it is doing. We first create private and public keys in a PEM
format. We then use the private key with our runnable "your-finance-jwt-generator.jar" . This is our runnable jar which allows for the quick creation of tokens. The issuer cannot be changed later on. Finally, it creates a token. We will see how to read this token later on. This token contains 3 extra Header claims. These are "kid", "typ", and "alg". It follows the following format:
{
"kid": "jwt.key",
"typ": "JWT",
"alg": "RS256"
}
The header of the JWT
Let’s look at these claims more closely:
- "kid" — Works as a hint claim. It indicates which sort of algorithm we are using.
- "typ" — It is used to declare
IANA
media types. There are three optionsJWT
(JSON Web token),JWE
(JSON Web Encryption), andJWA
(JSON Web Algorithms). These types aren’t relevant to our experiment. We will only see that our token isn’t really well encrypted and that it’s really easy to decrypt it. We will also see that although we can decrypt tokens, we cannot that easily tamper the to perform other actions. - "alg" — This is how we define the signature type we want to use. Signing can be considered as a cryptographic operation that will ensure that the original token has not been changed and is trusted. In our case, we are using RS256 otherwise known as RSA Signature with SHA-256.
With our public key, we can finally use it to change our template. The new config.yml file should look something like this:
kumuluzee:
name: your-financeje-banking
version: 1.0.0
jwt-auth:
public-key: FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKEN
issuer: joaofilipesabinoesperancinha
healthy: true
config.yml
The second step is to create four files. For every single plain token in the directory "jwt-plain-tokens
", we will create four commands. The first command is to create users that can effectively do things with their accounts. These are users with profiles "admin
", "client
", and "credit
".
Let’s run the file "createAccount.sh
", in order to create them. The second command will create the rest of the users which don’t possess any rights yet. This is the file "createUser.sh". Let’s run it. Now we’ll see that all users are finally created. Let’s now look into details about transactions and look at the remaining two commands. One to "cashin" and another to ask for more credit. The first generated file is the "sendMoney.sh" bash script. Here we can find all requests to "cashin
". In this file you’ll find a curl request to send random money quantities to users, per user. Let’s look at the admin case:
#!/usr/bin/env bash
# Send money to: admin
echo -e "\e[93mSending money to \e[96madmin\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer= FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 125}'
echo -e "\e[93m\n---\e[0m"
# Send money to: cindy
echo -e "\e[93mSending money to \e[96mcindy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 360}'
echo -e "\e[93m\n---\e[0m"
# Send money to: faustina
echo -e "\e[93mSending money to \e[96mfaustina\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 50}'
echo -e "\e[93m\n---\e[0m"
# Send money to: jack
echo -e "\e[93mSending money to \e[96mjack\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 205}'
echo -e "\e[93m\n---\e[0m"
# Send money to: jitska
echo -e "\e[93mSending money to \e[96mjitska\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 332}'
echo -e "\e[93m\n---\e[0m"
# Send money to: judy
echo -e "\e[93mSending money to \e[96mjudy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 295}'
echo -e "\e[93m\n---\e[0m"
# Send money to: lucy
echo -e "\e[93mSending money to \e[96mlucy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 160}'
echo -e "\e[93m\n---\e[0m"
# Send money to: malory
echo -e "\e[93mSending money to \e[96mmalory\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 413}'
echo -e "\e[93m\n---\e[0m"
# Send money to: mara
echo -e "\e[93mSending money to \e[96mmara\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 464}'
echo -e "\e[93m\n---\e[0m"
# Send money to: namita
echo -e "\e[93mSending money to \e[96mnamita\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 51}'
echo -e "\e[93m\n---\e[0m"
# Send money to: pietro
echo -e "\e[93mSending money to \e[96mpietro\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 491}'
echo -e "\e[93m\n---\e[0m"
# Send money to: rachelle
echo -e "\e[93mSending money to \e[96mrachelle\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 474}'
echo -e "\e[93m\n---\e[0m"
# Send money to: sandra
echo -e "\e[93mSending money to \e[96msandra\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 417}'
echo -e "\e[93m\n---\e[0m"
# Send money to: shikka
echo -e "\e[93mSending money to \e[96mshikka\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/accounts -X PUT -d '{ "saldo": 64}'
echo -e "\e[93m\n---\e[0m"
sendMoney.sh extract
The same users have also their credit requests assigned to them:
#!/usr/bin/env bash
# Asking money credit to: admin
echo -e "\e[93mAsking credit from \e[96madmin\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 137}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: cindy
echo -e "\e[93mAsking credit from \e[96mcindy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 117}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: faustina
echo -e "\e[93mAsking credit from \e[96mfaustina\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 217}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: jack
echo -e "\e[93mAsking credit from \e[96mjack\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 291}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: jitska
echo -e "\e[93mAsking credit from \e[96mjitska\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 184}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: judy
echo -e "\e[93mAsking credit from \e[96mjudy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 388}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: lucy
echo -e "\e[93mAsking credit from \e[96mlucy\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 219}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: malory
echo -e "\e[93mAsking credit from \e[96mmalory\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 66}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: mara
echo -e "\e[93mAsking credit from \e[96mmara\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 441}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: namita
echo -e "\e[93mAsking credit from \e[96mnamita\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 358}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: pietro
echo -e "\e[93mAsking credit from \e[96mpietro\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 432}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: rachelle
echo -e "\e[93mAsking credit from \e[96mrachelle\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 485}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: sandra
echo -e "\e[93mAsking credit from \e[96msandra\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 500}'
echo -e "\e[93m\n---\e[0m"
# Asking money credit to: shikka
echo -e "\e[93mAsking credit from \e[96mshikka\e[0m"
curl -i -H'Content-Type: application/json' -H'Authorization: Bearer FAKE.FAKE.FAKE' http://localhost:8080/credit -X PUT -d '{ "saldo": 89}'
echo -e "\e[93m\n---\e[0m"
askCredit.sh extract
All our characters
are part of the League of Nature
. Essentially just some group of people to be part of this banking system. In this context, they are defending the environment. It’s not really relevant for the article what this group of people does or where in the story they fit in, but for context, they participate in actions to defend the environment and slow down the effects of climate change. Some of our characters
can do everything, others cannot do anything and others can only "cashin" or just "ask for credit". Also notice that I am obfuscating sensitive information. These tokens normally should not be shared or be visible in on particular URL. They are yes always available via the browser developer console but anyway is to protect
some requests being made. This is a concept known as "security-per-obscurity" and
although it does not technically prevent the user to become aware of the token being used, it does work as a deterrent.
In both methods, when we make a deposit or when we ask for credit, notice that for each request, we are sending a random number between 1 to 500.
We are now almost ready to start our application, but first, let’s take a dive into a bit more theory.
10. How is a JWT
token made
Now that we have generated our tokens, let’s look into one of them. I am going to show you an obfuscated token, and we are going to use that to understand this.
Here is our token:
FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKE
.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETO
.FAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKENFAKETOKEN
What’s important here to notice is that our token is divided into three parts:
- Header — This is a Base64 encoded JSON configuration header, just as we discussed above.
- Payload — This is a Base64 encoded JSON payload. This is where we defined our Reserved and Custom claims. We can also define here Private and Public claims. Both of them fall under Custom claims. As a quick note, we can do what we want with both of these claims. However public claims are referred to as the ones defined in the IANA JSON Web Token Registry. It is important that we name our tokens in a certain way in order to avoid collisions with the registry. Public claims can also be defined as standard optional. Private claims follow no standard and it’s up to us to define them.
-
Signature — This is where we can be a bit creative. The signature is a cyphered combination of the
Header
and thePayload
. We decide the algorithm we want to use and this bit of the token will basically determine if the message we are sending is to be trusted. It is unique to that combination and our server will use the "public-key" we created to determine if we have a match. If you remember from the above we are usingRS256
in our example.
Before we continue, please note that both the Header
and the Payload
can be decyphered
in our example. We just "can’t" tamper with the payload or the header and still make it trusted. The protection against the potential effects of a malicious token can only be protected by the algorithm we choose. So choose wisely.
If you are working in an organization where top secret information is a concern, such as a bank, please DON’T do what we are about to do. This is only a way for us to check online the content of the tokens we’ve generated locally.
First, let’s go to https://jwt.io/ and fill out our JWT
token. Use the token you’ve just generated:
Using https://jwt.io/ to check the contents of our token
Let’s examine what we have here. This is our administrator token. That person is "Admin" in our example. We can see that our parameters are all available. In our list we see "sub", "aud", "upn", "access", "user_id", "iss", "name", "groups" and finally the "jti". We also have some extra claims. Let’s look at them:
"auth_time" — This is when the authentication has happened. Our token as been authenticated on Sunday, 17 July 2022 16:15:47 GMT+02:00 DST
"iat" — This is when the token has been created. In our case, this happens simultaneously as the auth_time.
"exp" — This is the expiry date of the token. It expires on Sunday, 17 July 2022 16:32:27 GMT+02:00 DST. We didn’t specify any expiry date in our token. This means that JWT
uses its default value of ~15 minutes.
Let’s now perform some tests.
11. Running the application
The code is ready to be used on GitHub. If we check the code out and open it with Intellij we need to be aware that we can’t run this application like a Spring Boot application. There is no "psvm" to make it run. Instead, we can just run the generated jar directly and make sure that we make an "mvn build" just before. Here is how I am using it at the moment:
[]https://github.com/jesperancinha/your-finance-je "Environment setup to run the application")
Let’s now run the "setupCertificates.sh
" script again. I don’t know how much time you took to get here but it’s very likely that the 15 minutes are already gone at this point. Just in case, just run them again.
Let’s start our app!
We can start it like this:
mvn clean install
java -jar your-financeje-banking/target/your-financeje-banking.jar
Or we can just run it through our ready-to-go running configuration. Check the repo and the Makefile beforehand if you want to understand everything it does:
make dcup-full-action
This script will run 2 services. One on port 8080
and the other on port 8081
. On port 8080
we will run a version of this software running our own code to generate JWT
tokens. On port 8081, we will run a version using the jwtknizr
generator created by Adam Bien
. We will focus this article, however on the service running on port 8080
. If you want, you can also run cypress
with:
make cypress-open
This will open
the cypress
console, and you’ll be able to run the tests with the browser of your choice. However, browser options are still limited at this stage. Most of the requests will actually be command line requests provided by cypress
.
For now, let’s not go into "cypress
". Please go to your browser and head on to this location:
http://localhost:8080/accounts/all
We should be getting a result like this:
As we can see, "Malory
", "Jack Fallout
", and "Jitska
" don’t have any credit or money. This is because they have only been given the user group. Notice also that Shikka
has been given no credit. "Shikka
", is our only client who doesn’t have the group credit.
If we look at the logs, we can see that successful operations take this format:
Sending money to admin
HTTP/1.1 200 OK
Date: Sun, 17 Jul 2022 15:01:13 GMT
X-Powered-By: KumuluzEE/4.1.0
Content-Type: application/json
Content-Length: 32
Server: Jetty(10.0.9)
{"balance":212,"client":"Admin"}
A 200 lets us know that the operation went successfully.
In the case of "Malory", "Jack Fallout", and "Jitska", both operations fail and then we will get this kind of message:
Sending money to jitska
HTTP/1.1 403 Forbidden
X-Powered-By: KumuluzEE/4.1.0
Content-Length: 0
Server: Jetty(10.0.9)
A 403 lets us know that our JWT
token has been validated and it’s trusted. However, the user is forbidden to perform that operation. In other words, they have no access to the designated method.
Let’s tamper with our tokens a bit. If we change some of the tokens of the sendMoney.sh file. We should be getting this:
Sending money to admin
HTTP/1.1 401 Unauthorized
X-Powered-By: KumuluzEE/4.1.0
WWW-Authenticate: Bearer realm="MP-JWT"
Content-Length: 0
Server: Jetty(10.0.9)
This 401
means that our token was not validated. It means that the public key that the server uses to check if our token is to be trusted, has found no match. If the public key cannot evaluate and validate the signature of the JWT token, it will then reject it.
As a recap, the Header and the "Payload" are not encrypted. They are just base 64 "encoded". This means that "Decoding" allows us always to have a peek inside what the payload actually is. If we are looking to safeguard our payload from eavesdropping, we shouldn’t use the "Payload" of the token for anything else but select identification parameters. The problem lies really when someone gets their hands on the JWT
token, for example, when the TLS tunnel has been compromised and someone is able to read the content of the exchanged messages. When that happens, there is still another safeguard. And this is the signature. The only one able to validate a message going in is the server that contains the public key. This public key, although public, only allows validating the incoming message by running against the signature and the "Header + Payload".
12. Conclusion
We have reached the end of our session. Thank you for following this.
We can see how JWT
tokens are compact and very much less verbose than their XML counterpart, the SAML
tokens. We have seen how easy it is to create and use tokens to get certain authorizations needed for certain methods and how we get there via a signed token.
I find however very important to get an idea of how JWT
works. Hopefully, with this, I have given you a good introduction to how JWT
tokens work.
To get a better idea of how all of this works, I advise you to play around with the implemented cypress
tests. This is a great way to see how requests are being made and what we are testing and what is expected. Then you’ll also get a better idea of why some users get to perform certain operations and others don’t.
I have placed all the source code of this application on GitHub
I hope that you have enjoyed this article as much as I enjoyed writing it.
Thank you for reading!
13. References
- Eclipse MicroProfile JWT Authentication API
- MicroProfile JSON Web Token (JWT)
- JSON Web Token Claims
- Kumuluz Blog
- kumuluzEE
- REST API Security
- RESTful API Security
- Tutorial: Create and Verify JWTs in Java
- oktadeveloper/okta-java-jwt-example
- Spring Boot CLI
- Java EE Security API (JSR 375/Soteria) with JWT tokens
- payara/Payara-Examples
- Securing JAX-RS Endpoints with JWT
- Secure your application with Eclipse MicroProfile JWT Auth
- JWT Dispenser
- Configurable Token Expiration 4.0.0
- Microservices with KumuluzEE MicroProfile 1.2
- JWT authentication: When and how to use it
- The Ultimate Guide to handling JWTs on frontend clients (GraphQL)
- IANA JSON Web Token (JWT)
- CyberChef
- Using KumuluzEE Security
- A plain English introduction to JSON web tokens (JWT): what it is and what it isn’t
- Internet Engineering Task Force RFC7519
Top comments (0)