Skip to content

Commit

Permalink
Merge pull request #5 from raeperd/feature/#3-jwt-service
Browse files Browse the repository at this point in the history
#3 Implement JWT
  • Loading branch information
raeperd committed Apr 23, 2021
2 parents 20b91ea + 27fe647 commit 4888ad7
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 1 deletion.
2 changes: 1 addition & 1 deletion spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,4 @@ No additional parameters required

`GET /api/tags`

No authentication required, returns a [List of Tags](#list-of-tags)
# No authentication required, returns a [List of Tags](#list-of-tags)
4 changes: 4 additions & 0 deletions src/main/java/io/github/raeperd/realworld/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ private User(String username, String email, String password) {
protected User() {
}

public long getId() {
return id;
}

public String getEmail() {
return email;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.raeperd.realworld.domain.jwt;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

class Base64URL {

private Base64URL() {
}

public static String encodeFromString(String rawString) {
return encodeFromBytes(rawString.getBytes(StandardCharsets.UTF_8));
}

public static String encodeFromBytes(byte[] rawBytes) {
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(rawBytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.raeperd.realworld.domain.jwt;

import io.github.raeperd.realworld.domain.User;

import java.nio.charset.StandardCharsets;

import static java.time.Instant.now;

class HS256JWTService implements JWTService {

private static final String BASE64URL_ENCODED_HEADER = Base64URL.encodeFromString("{\"alg\":\"HS256\",\"type\":\"JWT\"}");

private final byte[] secret;
private final long durationSeconds;

HS256JWTService(String secret, long durationSeconds) {
this.secret = secret.getBytes(StandardCharsets.UTF_8);
this.durationSeconds = durationSeconds;
}

@Override
public String generateTokenFromUser(User user) {
final var messageToSign = BASE64URL_ENCODED_HEADER + "." + base64EncodedPayLoadFromUser(user);
final var signature = HmacSHA256.sign(secret, messageToSign);
return messageToSign + "." + Base64URL.encodeFromBytes(signature);
}

private String base64EncodedPayLoadFromUser(User user) {
return Base64URL.encodeFromString(
JWTPayload.fromUser(user, now().getEpochSecond() + durationSeconds).toString());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.raeperd.realworld.domain.jwt;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

class HmacSHA256 {

private static final String ALGORITHM = "HmacSHA256";

private HmacSHA256() {
}

public static byte[] sign(byte[] secret, String message) {
try {
final var hmacSHA256 = Mac.getInstance(ALGORITHM);
hmacSHA256.init(new SecretKeySpec(secret, ALGORITHM));
return hmacSHA256.doFinal(message.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | IllegalArgumentException | InvalidKeyException exception) {
throw new HmacSHA256SignFailedException(exception);
}
}

private static class HmacSHA256SignFailedException extends RuntimeException {
public HmacSHA256SignFailedException(Throwable cause) {
super(cause);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.raeperd.realworld.domain.jwt;

import io.github.raeperd.realworld.domain.User;

import static java.lang.String.format;

class JWTPayload {

private final long sub;
private final String name;
private final long iat;

static JWTPayload fromUser(User user, long expireEpochSecond) {
return new JWTPayload(user.getId(), user.getUsername(), expireEpochSecond);
}

JWTPayload(long sub, String name, long iat) {
this.sub = sub;
this.name = name;
this.iat = iat;
}

@Override
public String toString() {
return format("{\"sub\":%d,\"name\":\"%s\",\"iat\":%d}", sub, name, iat);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.raeperd.realworld.domain.jwt;

import io.github.raeperd.realworld.domain.User;

public interface JWTService {

String generateTokenFromUser(User user);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.raeperd.realworld.domain.jwt;

import org.junit.jupiter.api.Test;

import static io.github.raeperd.realworld.domain.jwt.Base64URL.encodeFromString;
import static org.assertj.core.api.Assertions.assertThat;

class Base64URLTest {

@Test
void when_encode_return_expected_string() {
assertThat(encodeFromString("something")).isEqualTo("c29tZXRoaW5n");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.raeperd.realworld.domain.jwt;

import io.github.raeperd.realworld.domain.User;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;

import static io.github.raeperd.realworld.domain.User.createNewUser;
import static org.assertj.core.api.Assertions.assertThat;

class HS256JWTServiceTest {

private static final String SECRET = "SOME_SECRET";

private final JWTService jwtService = new HS256JWTService(SECRET, 1000);

private final User user = createNewUser("user", "user@email.com", "password");

@Test
void when_generateToken_expect_result_startsWith_encodedHeader() {
final var token = jwtService.generateTokenFromUser(user);

assertThat(token).startsWith(Base64URL.encodeFromString("{\"alg\":\"HS256\",\"type\":\"JWT\"}"));
}

@Test
void when_generateToken_return_value_can_be_verified() {
final var token = jwtService.generateTokenFromUser(user);
final var indexOfSignature = token.lastIndexOf('.') + 1;

final var message = token.substring(0, indexOfSignature - 1);
final var signature = token.substring(indexOfSignature);

assertThat(Base64URL.encodeFromBytes(HmacSHA256.sign(SECRET.getBytes(StandardCharsets.UTF_8), message)))
.isEqualTo(signature);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.raeperd.realworld.domain.jwt;

import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class HmacSHA256Test {

@Test
void when_invalid_secret_expect_throw_exception() {
assertThatThrownBy(
() -> HmacSHA256.sign(null, "test")
).isInstanceOf(RuntimeException.class);
}

@Test
void when_sign_expect_matched_return() {
assertThat(HmacSHA256.sign("secret".getBytes(StandardCharsets.UTF_8), "plain"))
.asHexString()
.isEqualTo("A237566E044B73E6A1E54BD59974547487FA5F8143025CE0D04D82E7EE4C5E34");
}
}

0 comments on commit 4888ad7

Please sign in to comment.