Bearer Authentication can be random tokens. They are secure and remove the need of jsession id. But they will be more useful if they can carry information along with them. A JWT token has 3 parts to it.
Header and Payload both are JSON. They need to be Base64 encoded. The dot separates each part.
String signature = hmacSha256(base64(header) + "." + base64(payload), secret);
String jwtToken = base64(header) + "." + base64(payload) + "." + signature;
Here is an example
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0IiwiYXVkIjpbImFkbWluIl0sImlzcyI6Im1hc29uLm1ldGFtdWcubmV0IiwiZXhwIjoxNTc0NTEyNzY1LCJpYXQiOjE1NjY3MzY3NjUsImp0aSI6ImY3YmZlMzNmLTdiZjctNGViNC04ZTU5LTk5MTc5OWI1ZWI4YSJ9.EVcCaSqrSNVs3cWdLt-qkoqUk7rPHEOsDHS8yejwxMw
The standard algorithm for signing is HMAC + SHA256
also called has HS256
. Here we will declare the header as a constant string. This header will be used during verification
for checking the algorithm used.
private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
We need to encode the header and payload. For encoding, we use Base64
and URL encoding. Java 8 provides a method for Base64 URL encoding.
URL encoding makes the JWT safe to be sent as a part of the url.
import java.util.Base64;
//...
private static String encode(byte[] bytes) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
So encoding the header will always give us constant string eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
The simplest information you can store in your payload is
private JSONObject payload = new JSONObject();
//...
payload.put("sub", sub);
payload.put("aud", aud);
payload.put("exp", expires);
Here is an example
{
"sub": "13324",
"aud": "admin",
"exp": 1512203223
}
We must encode the payload since it will be in JSON format. After encoding it will become a compact string as follows. During verification, this string is decoded using the same Base64 URL decoding mechanism to retrieve the JSON payload.
eyJzdWIiOiIxMjM0IiwiYXVkIjpbImFkbWluIl0sImlzcyI6Im1hc29uLm1ldGFtdWcubmV0IiwiZXhwIjoxNTc0NTEyNzY1LCJpYXQiOjE1NjY3MzY3NjUsImp0aSI6ImY3YmZlMzNmLTdiZjctNGViNC04ZTU5LTk5MTc5OWI1ZWI4YSJ9
The expiration time exp
is set into the JWT token as a timestamp. The server sets this time to match it against
the current timestamp during verification. Expiration time is kept short to prevent against token highjacking. Expiration time is a hard-coded expiration time into the token. Once it is set inside the token it cannot be changed. Unless the token contains an issued time field, the server has no choice but to discard the token after the expiration time has reached.
Another alternative to exp
is Issued At iat
. iat
is more flexible and server can decide the expiration time according to its policy.
Header and payload are concatenated with a dot and signed with HMAC + SHA256 algorithm using a secret key. You need to maintain a configurable secret key somewhere.
private String hmacSha256(String data, String secret) {
try {
byte[] hash = secret.getBytes(StandardCharsets.UTF_8);
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(hash, "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return encode(signedBytes);
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
Logger.getLogger(JWebToken.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
return null;
}
}
//...
signature = hmacSha256(encodedHeader + "." + encode(payload));
Read how and why cryptographic hash functions are used to sign messages.
Token verification does not require any database call.
Token-Based authentication requires a database to create and verify tokens. JWT creation may require access to the database for user details, but verification is all about checking if the server has signed the token and its still valid (looking at the expiry time).
Since 99% of the request will comprise of resource access and verification (Rest 1% may be unauthenticated resources access). This makes JWT a more efficient token authentication mechanism.
Verification requires the following steps:
You can find the steps to decode in java here.
Now that we trust the token, we can use the User Id and the role mentioned in the token and provide access to the requested resource. The payload shouldn't contain sensitive information like payment information.
The below example usage shows how a Data access object is used to create the JSONObject. Once the token is generated it is passed on to the client by the auth server. Where the token is sent back again to the server, the server verifies the token.
//Create the token from user details.
JSONObject payload = dao.getBearerDetails(user, pass);
String bearerToken = JWebToken(payload).toString();
//@TODO Send this token over the network
//recieve the bearer token
JWebToken incomingToken = new JWebToken(bearerToken);
if (incomingToken.isValid()) {
System.out.println(incomingToken.getSubject());
}
The program provides a constructor for both generating the token and verifying the generated token. It uses more information like issued at (iat), issuer(iat), etc.
It is hosted on github
The following code can be used to generate JWT token
private static final int EXPIRY_DAYS = 90;
JSONObject jwtPayload = new JSONObject();
jwtPayload.put("status", 0);
JSONArray audArray = new JSONArray();
audArray.put("admin");
jwtPayload.put("sub", "John");
jwtPayload.put("aud", audArray);
LocalDateTime ldt = LocalDateTime.now().plusDays(EXPIRY_DAYS);
jwtPayload.put("exp", ldt.toEpochSecond(ZoneOffset.UTC)); //this needs to be configured
String token = new JWebToken(jwtPayload).toString();
JWT token recieved in the String format can be used to verify and extract audience and subject information as follows.
//verify and use
JWebToken incomingToken = new JWebToken(bearerToken);
if (!incomingToken.isValid()) {
String audience = incomingToken.getAudience();
String subject = incomingToken.getSubject();
}