Skip to content

Commit ad5b5ba

Browse files
author
zihluwang
committed
feat: sign JSON Web Token with HmacSHA algorithms
1 parent fdf3263 commit ad5b5ba

File tree

6 files changed

+636
-0
lines changed

6 files changed

+636
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2024-2025 OnixByte.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.onixbyte.jwt;
19+
20+
import com.fasterxml.jackson.core.JsonProcessingException;
21+
22+
import java.security.InvalidKeyException;
23+
import java.security.NoSuchAlgorithmException;
24+
25+
/**
26+
*
27+
*/
28+
public interface TokenCreator {
29+
30+
String sign(TokenPayload payload);
31+
32+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (C) 2024-2025 OnixByte.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.onixbyte.jwt;
19+
20+
import com.onixbyte.jwt.data.RawTokenComponent;
21+
22+
import java.util.Map;
23+
24+
/**
25+
*
26+
*/
27+
public interface TokenResolver {
28+
29+
/**
30+
*
31+
* @param token
32+
*/
33+
void verify(String token);
34+
35+
/**
36+
*
37+
* @param token
38+
* @return
39+
*/
40+
Map<String, String> getHeader(String token);
41+
42+
/**
43+
*
44+
* @param payload
45+
* @return
46+
*/
47+
Map<String, Object> getPayload(String payload);
48+
49+
/**
50+
*
51+
* @param token
52+
* @return
53+
*/
54+
RawTokenComponent splitToken(String token);
55+
56+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (C) 2024-2025 OnixByte.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.onixbyte.jwt.constant;
19+
20+
/**
21+
*
22+
*/
23+
public enum Algorithm {
24+
HS256(1, 256, "HmacSHA256"),
25+
HS384(1, 384, "HmacSHA384"),
26+
HS512(1, 512, "HmacSHA512"),
27+
RS256(2, 256, "SHA256withRSA"),
28+
RS384(2, 384, "SHA384withRSA"),
29+
RS512(2, 512, "SHA512withRSA"),
30+
ES256(3, 256, "SHA256withECDSA"),
31+
ES384(3, 384, "SHA384withECDSA"),
32+
ES512(3, 512, "SHA512withECDSA");
33+
34+
/**
35+
*
36+
*/
37+
private static final int HS_FLAG = 1; // 001
38+
39+
/**
40+
*
41+
*/
42+
private static final int RS_FLAG = 2; // 010
43+
44+
/**
45+
*
46+
*/
47+
private static final int ES_FLAG = 3; // 011
48+
49+
/**
50+
*
51+
*/
52+
private final int typeFlag;
53+
private final int shaLength;
54+
private final String algorithm;
55+
56+
/**
57+
*
58+
* @param typeFlag
59+
* @param shaLength
60+
* @param algorithm
61+
*/
62+
Algorithm(int typeFlag, int shaLength, String algorithm) {
63+
this.typeFlag = typeFlag;
64+
this.shaLength = shaLength;
65+
this.algorithm = algorithm;
66+
}
67+
68+
/**
69+
*
70+
* @return
71+
*/
72+
public boolean isHmac() {
73+
return (this.typeFlag & HS_FLAG) != 0;
74+
}
75+
76+
/**
77+
*
78+
* @return
79+
*/
80+
public boolean isRsa() {
81+
return (this.typeFlag & RS_FLAG) != 0;
82+
}
83+
84+
/**
85+
*
86+
* @return
87+
*/
88+
public boolean isEcdsa() {
89+
return (this.typeFlag & ES_FLAG) != 0;
90+
}
91+
92+
/**
93+
*
94+
* @return
95+
*/
96+
public int getShaLength() {
97+
return shaLength;
98+
}
99+
100+
/**
101+
*
102+
* @return
103+
*/
104+
public int getTypeFlag() {
105+
return typeFlag;
106+
}
107+
108+
/**
109+
*
110+
* @return
111+
*/
112+
public String getAlgorithm() {
113+
return algorithm;
114+
}
115+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (C) 2024-2025 OnixByte.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.onixbyte.jwt.impl;
19+
20+
import com.fasterxml.jackson.core.JsonProcessingException;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.onixbyte.jwt.TokenCreator;
23+
import com.onixbyte.jwt.TokenPayload;
24+
import com.onixbyte.jwt.constant.Algorithm;
25+
import com.onixbyte.jwt.constant.HeaderClaims;
26+
import com.onixbyte.jwt.holder.ObjectMapperHolder;
27+
import com.onixbyte.jwt.util.CryptoUtil;
28+
29+
import java.nio.charset.StandardCharsets;
30+
import java.util.Base64;
31+
import java.util.HashMap;
32+
33+
/**
34+
* Implementation of {@link TokenCreator} that generates HMAC-signed JSON Web Tokens (JWTs).
35+
* <p>
36+
* This class uses a specified HMAC algorithm to create signed tokens, incorporating a header,
37+
* payload, and signature. It ensures the secret key meets the minimum length requirement for
38+
* the chosen algorithm and handles JSON serialisation of the token components.
39+
*
40+
* @author zihluwang
41+
*/
42+
public class HmacTokenCreator implements TokenCreator {
43+
44+
private final Algorithm algorithm;
45+
private final String issuer;
46+
private final byte[] secret;
47+
48+
private final ObjectMapper objectMapper;
49+
50+
/**
51+
* Constructs an HMAC token creator with the specified algorithm, issuer, and secret key.
52+
* <p>
53+
* Validates that the secret key length meets the minimum requirement for the chosen algorithm.
54+
*
55+
* @param algorithm the HMAC algorithm to use for signing (e.g., HS256, HS384, HS512)
56+
* @param issuer the issuer identifier to include in the token payload if not already present
57+
* @param secret the secret key as a string, used to generate the HMAC signature
58+
* @throws IllegalArgumentException if the secret key is shorter than the minimum required
59+
* length for the specified algorithm
60+
*/
61+
public HmacTokenCreator(Algorithm algorithm, String issuer, String secret) {
62+
var _minSecretLength = algorithm.getShaLength() >> 3;
63+
var secretBytesLength = secret.getBytes(StandardCharsets.UTF_8).length;
64+
if (secretBytesLength < _minSecretLength) {
65+
throw new IllegalArgumentException("Secret key too short for HS%d: minimum %d bytes required, got %d."
66+
.formatted(algorithm.getShaLength(), _minSecretLength, secretBytesLength)
67+
);
68+
}
69+
70+
this.algorithm = algorithm;
71+
this.issuer = issuer;
72+
this.secret = secret.getBytes(StandardCharsets.UTF_8);
73+
this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper();
74+
}
75+
76+
/**
77+
* Creates and signs a JWT using the HMAC algorithm.
78+
* <p>
79+
* Generates a token by encoding the header and payload as Base64 URL-safe strings,
80+
* creating an HMAC signature, and concatenating them with dots. If the payload does not
81+
* include an issuer, the configured issuer is added.
82+
*
83+
* @param payload the {@link TokenPayload} containing claims to include in the token
84+
* @return the signed JWT as a string in the format "header.payload.signature"
85+
* @throws IllegalArgumentException if the payload cannot be serialised to JSON due to
86+
* invalid data or structure
87+
* @throws RuntimeException if an unexpected error occurs during JSON processing
88+
*/
89+
@Override
90+
public String sign(TokenPayload payload) {
91+
var header = new HashMap<String, String>();
92+
93+
header.put(HeaderClaims.ALGORITHM, algorithm.name());
94+
if (!header.containsKey(HeaderClaims.TYPE)) {
95+
header.put(HeaderClaims.TYPE, "JWT");
96+
}
97+
98+
if (!payload.hasIssuer()) {
99+
payload.withIssuer(issuer);
100+
}
101+
102+
try {
103+
var encodedHeader = Base64.getUrlEncoder().withoutPadding()
104+
.encodeToString(objectMapper.writeValueAsBytes(header));
105+
var encodedPayload = Base64.getUrlEncoder().withoutPadding()
106+
.encodeToString(objectMapper.writeValueAsBytes(payload.getPayload()));
107+
108+
var signatureBytes = CryptoUtil.createSignatureFor(algorithm,
109+
secret,
110+
encodedHeader.getBytes(StandardCharsets.UTF_8),
111+
encodedPayload.getBytes(StandardCharsets.UTF_8));
112+
var signature = Base64.getUrlEncoder()
113+
.withoutPadding()
114+
.encodeToString((signatureBytes));
115+
116+
return "%s.%s.%s".formatted(encodedHeader, encodedPayload, signature);
117+
} catch (JsonProcessingException e) {
118+
throw new IllegalArgumentException("Failed to serialise token header or payload to JSON.", e);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)