From b1dbc1c0e9e0a40bb3bf40a9ad39bd43189b2d55 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 20:55:21 +0800 Subject: [PATCH 01/17] feat: added simple-jwt module to create, verify and read JSON Web Token natively --- settings.gradle.kts | 1 + simple-jwt/build.gradle.kts | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 simple-jwt/build.gradle.kts diff --git a/settings.gradle.kts b/settings.gradle.kts index 794f015..383643d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include( "key-pair-loader", "map-util-unsafe", "num4j", + "simple-jwt", "simple-jwt-facade", "simple-jwt-authzero", "simple-jwt-spring-boot-starter", diff --git a/simple-jwt/build.gradle.kts b/simple-jwt/build.gradle.kts new file mode 100644 index 0000000..cf66377 --- /dev/null +++ b/simple-jwt/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("java") +} + +val artefactVersion: String by project + +group = "com.onixbyte" +version = artefactVersion + +repositories { + mavenCentral() +} + +dependencies { + val jacksonVersion: String by project + implementation(project(":devkit-core")) + implementation(project(":devkit-utils")) + implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file From 6814718f2382531e1913be37f76f2eeda48e433b Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 20:55:46 +0800 Subject: [PATCH 02/17] feat: added predefined constants --- .../onixbyte/jwt/constant/HeaderClaims.java | 48 ++++++++++++ .../jwt/constant/RegisteredClaims.java | 78 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java new file mode 100644 index 0000000..728b2ee --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.constant; + +public final class HeaderClaims { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private HeaderClaims() { + } + + /** + * The algorithm used to sign a JWT. + */ + public static final String ALGORITHM = "alg"; + + /** + * The content type of the JWT. + */ + public static final String CONTENT_TYPE = "cty"; + + /** + * The media type of the JWT. + */ + public static final String TYPE = "typ"; + + /** + * The key ID of a JWT used to specify the key for signature validation. + */ + public static final String KEY_ID = "kid"; + +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java new file mode 100644 index 0000000..dcde8a9 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.constant; + +import java.util.List; + +/** + * + */ +public final class RegisteredClaims { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private RegisteredClaims() { + } + + /** + * The "iss" (issuer) claim identifies the principal that issued the JWT. + * Refer RFC 7529 Section 4.1.1 + */ + public static final String ISSUER = "iss"; + + /** + * The "sub" (subject) claim identifies the principal that is the subject of the JWT. + * Refer RFC 7529 Section 4.1.2 + */ + public static final String SUBJECT = "sub"; + + /** + * The "aud" (audience) claim identifies the recipients that the JWT is intended for. + * Refer RFC 7529 Section 4.1.3 + */ + public static final String AUDIENCE = "aud"; + + /** + * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be + * accepted for processing. + * Refer RFC 7529 Section 4.1.4 + */ + public static final String EXPIRES_AT = "exp"; + + /** + * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. + * Refer RFC 7529 Section 4.1.5 + */ + public static final String NOT_BEFORE = "nbf"; + + /** + * The "iat" (issued at) claim identifies the time at which the JWT was issued. + * Refer RFC 7529 Section 4.1.6 + */ + public static final String ISSUED_AT = "iat"; + + /** + * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + * Refer RFC 7529 Section 4.1.7 + */ + public static final String TOKEN_ID = "jti"; + + public static final List VALUES = List.of(ISSUER, SUBJECT, AUDIENCE, EXPIRES_AT, NOT_BEFORE, ISSUED_AT, TOKEN_ID); + +} From c37ffdb60478480bef211749808a72b89d59bf6d Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 21:02:45 +0800 Subject: [PATCH 03/17] feat: token payload information --- .../java/com/onixbyte/jwt/TokenPayload.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java new file mode 100644 index 0000000..506b715 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt; + +import com.onixbyte.jwt.constant.RegisteredClaims; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; + +/** + * + */ +public class TokenPayload { + + public static TokenPayload createPayload() { + return new TokenPayload(); + } + + private final Map payload; + private final List audiences; + + private String subject; + private String issuer; + private String tokenId; + private Long expiresAt; + private Long notBefore; + private Long issuedAt; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private TokenPayload() { + payload = new HashMap<>(); + audiences = new ArrayList<>(); + } + + public TokenPayload withAudience(String audience) { + audiences.add(audience); + return this; + } + + public TokenPayload withAudiences(String... audiences) { + this.audiences.addAll(Arrays.asList(audiences)); + return this; + } + + public TokenPayload withSubject(String subject) { + this.subject = subject; + return this; + } + + public TokenPayload withIssuer(String issuer) { + this.issuer = issuer; + return this; + } + + public TokenPayload withTokenId(String tokenId) { + this.tokenId = tokenId; + return this; + } + + public TokenPayload withExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt.atZone(ZoneId.systemDefault()) + .toInstant() + .getEpochSecond(); + return this; + } + + public TokenPayload withNotBefore(LocalDateTime notBefore) { + this.notBefore = notBefore.atZone(ZoneId.systemDefault()) + .toInstant() + .getEpochSecond(); + return this; + } + + public TokenPayload withIssuedAt(LocalDateTime issuedAt) { + this.issuedAt = issuedAt.atZone(ZoneId.systemDefault()) + .toInstant() + .getEpochSecond(); + return this; + } + + public TokenPayload withClaim(String name, String value) { + if (RegisteredClaims.VALUES.contains(name)) { + throw new IllegalStateException("Please set registered claims with pre-defined methods"); + } + + this.payload.put(name, value); + return this; + } + + public boolean hasIssuer() { + return Objects.nonNull(issuer) && !issuer.isBlank(); + } + + public Map getPayload() { + var _payload = new HashMap<>(payload); + + Optional.of(audiences) + .filter((aud) -> !aud.isEmpty()) + .ifPresent((aud) -> _payload.put(RegisteredClaims.AUDIENCE, aud)); + + Optional.ofNullable(subject) + .filter((sub) -> !sub.isBlank()) + .ifPresent((sub) -> _payload.put(RegisteredClaims.SUBJECT, subject)); + + Optional.ofNullable(expiresAt) + .ifPresent((exp) -> _payload.put(RegisteredClaims.EXPIRES_AT, exp)); + + Optional.ofNullable(tokenId) + .filter((jti) -> !jti.isBlank()) + .ifPresent((jti) -> _payload.put(RegisteredClaims.TOKEN_ID, jti)); + + Optional.ofNullable(issuer) + .map((iss) -> !iss.isBlank()) + .ifPresent((iss) -> _payload.put(RegisteredClaims.ISSUER, iss)); + + Optional.ofNullable(issuedAt) + .ifPresent((iat) -> _payload.put(RegisteredClaims.ISSUED_AT, iat)); + + Optional.ofNullable(notBefore) + .ifPresent((nbf) -> _payload.put(RegisteredClaims.NOT_BEFORE, nbf)); + + return _payload; + } + +} From adda608c7e26ba02cc000a765726fafe4f5bec4f Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 21:03:00 +0800 Subject: [PATCH 04/17] feat: object mapper holder --- .../jwt/holder/ObjectMapperHolder.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java b/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java new file mode 100644 index 0000000..653b776 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.holder; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import java.util.Objects; + +public class ObjectMapperHolder { + + private static ObjectMapperHolder HOLDER; + + private final ObjectMapper objectMapper; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private ObjectMapperHolder() { + this.objectMapper = JsonMapper.builder() + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .build(); + } + + /** + * Get singleton instance. + * + * @return the {@code ObjectMapperHolder} instance + */ + public static ObjectMapperHolder getInstance() { + if (Objects.isNull(HOLDER)) { + synchronized (ObjectMapperHolder.class) { + if (Objects.isNull(HOLDER)) { + HOLDER = new ObjectMapperHolder(); + } + } + } + return HOLDER; + } + + /** + * Get Object + * + * @return + */ + public ObjectMapper getObjectMapper() { + return objectMapper; + } +} From fdf32633736cb6cc85f65f0ea0c9c4bb3ba221a9 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 21:04:43 +0800 Subject: [PATCH 05/17] feat: a class to store the three parts of the token in raw type --- .../onixbyte/jwt/data/RawTokenComponent.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java b/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java new file mode 100644 index 0000000..6b5108c --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.data; + +/** + * + * @param header + * @param payload + * @param signature + * @author zihluwang + */ +public record RawTokenComponent( + String header, + String payload, + String signature +) { +} From ad5b5ba146605849e483609b22f4caeea9a45618 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Wed, 2 Apr 2025 22:40:22 +0800 Subject: [PATCH 06/17] feat: sign JSON Web Token with HmacSHA algorithms --- .../java/com/onixbyte/jwt/TokenCreator.java | 32 ++++ .../java/com/onixbyte/jwt/TokenResolver.java | 56 ++++++ .../com/onixbyte/jwt/constant/Algorithm.java | 115 +++++++++++ .../onixbyte/jwt/impl/HmacTokenCreator.java | 121 ++++++++++++ .../onixbyte/jwt/impl/HmacTokenResolver.java | 179 ++++++++++++++++++ .../com/onixbyte/jwt/util/CryptoUtil.java | 133 +++++++++++++ 6 files changed, 636 insertions(+) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenCreator.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/util/CryptoUtil.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java new file mode 100644 index 0000000..d0e8f73 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * + */ +public interface TokenCreator { + + String sign(TokenPayload payload); + +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java new file mode 100644 index 0000000..c8d1b81 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt; + +import com.onixbyte.jwt.data.RawTokenComponent; + +import java.util.Map; + +/** + * + */ +public interface TokenResolver { + + /** + * + * @param token + */ + void verify(String token); + + /** + * + * @param token + * @return + */ + Map getHeader(String token); + + /** + * + * @param payload + * @return + */ + Map getPayload(String payload); + + /** + * + * @param token + * @return + */ + RawTokenComponent splitToken(String token); + +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java new file mode 100644 index 0000000..46c3e07 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.constant; + +/** + * + */ +public enum Algorithm { + HS256(1, 256, "HmacSHA256"), + HS384(1, 384, "HmacSHA384"), + HS512(1, 512, "HmacSHA512"), + RS256(2, 256, "SHA256withRSA"), + RS384(2, 384, "SHA384withRSA"), + RS512(2, 512, "SHA512withRSA"), + ES256(3, 256, "SHA256withECDSA"), + ES384(3, 384, "SHA384withECDSA"), + ES512(3, 512, "SHA512withECDSA"); + + /** + * + */ + private static final int HS_FLAG = 1; // 001 + + /** + * + */ + private static final int RS_FLAG = 2; // 010 + + /** + * + */ + private static final int ES_FLAG = 3; // 011 + + /** + * + */ + private final int typeFlag; + private final int shaLength; + private final String algorithm; + + /** + * + * @param typeFlag + * @param shaLength + * @param algorithm + */ + Algorithm(int typeFlag, int shaLength, String algorithm) { + this.typeFlag = typeFlag; + this.shaLength = shaLength; + this.algorithm = algorithm; + } + + /** + * + * @return + */ + public boolean isHmac() { + return (this.typeFlag & HS_FLAG) != 0; + } + + /** + * + * @return + */ + public boolean isRsa() { + return (this.typeFlag & RS_FLAG) != 0; + } + + /** + * + * @return + */ + public boolean isEcdsa() { + return (this.typeFlag & ES_FLAG) != 0; + } + + /** + * + * @return + */ + public int getShaLength() { + return shaLength; + } + + /** + * + * @return + */ + public int getTypeFlag() { + return typeFlag; + } + + /** + * + * @return + */ + public String getAlgorithm() { + return algorithm; + } +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenCreator.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenCreator.java new file mode 100644 index 0000000..f31e193 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenCreator.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.onixbyte.jwt.TokenCreator; +import com.onixbyte.jwt.TokenPayload; +import com.onixbyte.jwt.constant.Algorithm; +import com.onixbyte.jwt.constant.HeaderClaims; +import com.onixbyte.jwt.holder.ObjectMapperHolder; +import com.onixbyte.jwt.util.CryptoUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; + +/** + * Implementation of {@link TokenCreator} that generates HMAC-signed JSON Web Tokens (JWTs). + *

+ * This class uses a specified HMAC algorithm to create signed tokens, incorporating a header, + * payload, and signature. It ensures the secret key meets the minimum length requirement for + * the chosen algorithm and handles JSON serialisation of the token components. + * + * @author zihluwang + */ +public class HmacTokenCreator implements TokenCreator { + + private final Algorithm algorithm; + private final String issuer; + private final byte[] secret; + + private final ObjectMapper objectMapper; + + /** + * Constructs an HMAC token creator with the specified algorithm, issuer, and secret key. + *

+ * Validates that the secret key length meets the minimum requirement for the chosen algorithm. + * + * @param algorithm the HMAC algorithm to use for signing (e.g., HS256, HS384, HS512) + * @param issuer the issuer identifier to include in the token payload if not already present + * @param secret the secret key as a string, used to generate the HMAC signature + * @throws IllegalArgumentException if the secret key is shorter than the minimum required + * length for the specified algorithm + */ + public HmacTokenCreator(Algorithm algorithm, String issuer, String secret) { + var _minSecretLength = algorithm.getShaLength() >> 3; + var secretBytesLength = secret.getBytes(StandardCharsets.UTF_8).length; + if (secretBytesLength < _minSecretLength) { + throw new IllegalArgumentException("Secret key too short for HS%d: minimum %d bytes required, got %d." + .formatted(algorithm.getShaLength(), _minSecretLength, secretBytesLength) + ); + } + + this.algorithm = algorithm; + this.issuer = issuer; + this.secret = secret.getBytes(StandardCharsets.UTF_8); + this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper(); + } + + /** + * Creates and signs a JWT using the HMAC algorithm. + *

+ * Generates a token by encoding the header and payload as Base64 URL-safe strings, + * creating an HMAC signature, and concatenating them with dots. If the payload does not + * include an issuer, the configured issuer is added. + * + * @param payload the {@link TokenPayload} containing claims to include in the token + * @return the signed JWT as a string in the format "header.payload.signature" + * @throws IllegalArgumentException if the payload cannot be serialised to JSON due to + * invalid data or structure + * @throws RuntimeException if an unexpected error occurs during JSON processing + */ + @Override + public String sign(TokenPayload payload) { + var header = new HashMap(); + + header.put(HeaderClaims.ALGORITHM, algorithm.name()); + if (!header.containsKey(HeaderClaims.TYPE)) { + header.put(HeaderClaims.TYPE, "JWT"); + } + + if (!payload.hasIssuer()) { + payload.withIssuer(issuer); + } + + try { + var encodedHeader = Base64.getUrlEncoder().withoutPadding() + .encodeToString(objectMapper.writeValueAsBytes(header)); + var encodedPayload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(objectMapper.writeValueAsBytes(payload.getPayload())); + + var signatureBytes = CryptoUtil.createSignatureFor(algorithm, + secret, + encodedHeader.getBytes(StandardCharsets.UTF_8), + encodedPayload.getBytes(StandardCharsets.UTF_8)); + var signature = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString((signatureBytes)); + + return "%s.%s.%s".formatted(encodedHeader, encodedPayload, signature); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to serialise token header or payload to JSON.", e); + } + } +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java new file mode 100644 index 0000000..e0eabf1 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.onixbyte.jwt.TokenResolver; +import com.onixbyte.jwt.constant.Algorithm; +import com.onixbyte.jwt.constant.RegisteredClaims; +import com.onixbyte.jwt.data.RawTokenComponent; +import com.onixbyte.jwt.holder.ObjectMapperHolder; +import com.onixbyte.jwt.util.CryptoUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +/** + * Implementation of {@link TokenResolver} that resolves and verifies HMAC-signed JSON Web + * Tokens (JWTs). + *

+ * This class splits a JWT into its components, verifies its signature using an HMAC algorithm, and + * deserialises the header and payload into usable data structures. It ensures the secret key meets + * the minimum length requirement for the specified algorithm. + * + * @author zihluwang + */ +public class HmacTokenResolver implements TokenResolver { + + private final Algorithm algorithm; + private final byte[] secret; + + private final ObjectMapper objectMapper; + + /** + * Constructs an HMAC token resolver with the specified algorithm and secret key. + *

+ * Validates that the secret key length meets the minimum requirement for the chosen algorithm. + * + * @param algorithm the HMAC algorithm used for signature verification (e.g., HS256, + * HS384, HS512) + * @param secret the secret key as a string, used to verify the HMAC signature + * @throws IllegalArgumentException if the secret key is shorter than the minimum required + * length for the specified algorithm + */ + public HmacTokenResolver(Algorithm algorithm, String secret) { + var _minSecretLength = algorithm.getShaLength() >> 3; + var secretBytesLength = secret.getBytes(StandardCharsets.UTF_8).length; + if (secretBytesLength < _minSecretLength) { + throw new IllegalArgumentException("Secret key too short for HS%d: minimum %d bytes required, got %d" + .formatted(algorithm.getShaLength(), _minSecretLength, secretBytesLength) + ); + } + + this.algorithm = algorithm; + this.secret = secret.getBytes(StandardCharsets.UTF_8); + this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper(); + } + + /** + * Splits a JWT into its raw components: header, payload, and signature. + * + * @param token the JWT string to split + * @return a {@link RawTokenComponent} containing the header, payload, and signature as strings + * @throws IllegalArgumentException if the token does not consist of exactly three parts + * separated by dots + */ + @Override + public RawTokenComponent splitToken(String token) { + var tokenTuple = token.split("\\."); + + if (tokenTuple.length != 3) { + throw new IllegalArgumentException( + "The provided JWT is invalid: it must consist of exactly three parts separated by dots."); + } + + return new RawTokenComponent(tokenTuple[0], tokenTuple[1], tokenTuple[2]); + } + + /** + * Verifies the HMAC signature of the provided JWT. + *

+ * Splits the token into its components and uses the configured algorithm and secret to check + * the signature's validity. If the signature does not match, an exception is thrown by the + * underlying cryptographic utility. + * + * @param token the JWT string to verify + * @throws IllegalArgumentException if the token is malformed or the signature verification + * fails due to an invalid algorithm, key, or + * mismatched signature + */ + @Override + public void verify(String token) { + var _token = splitToken(token); + + var isValid = CryptoUtil.verifySignatureFor(algorithm, + secret, + _token.header(), + _token.payload(), + _token.signature().getBytes(StandardCharsets.UTF_8) + ); + if (!isValid) throw new IllegalArgumentException( + "JWT signature verification failed: the token may be tampered with or invalid."); + } + + /** + * Retrieves the header claims from the provided JWT. + *

+ * Decodes the Base64-encoded header and deserialises it into a map of strings. + * + * @param token the JWT string from which to extract the header + * @return a map containing the header claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the header cannot be + * deserialised due to invalid JSON format + */ + @Override + public Map getHeader(String token) { + var _token = splitToken(token); + + var headerBytes = Base64.getDecoder().decode(_token.header()); + var headerJson = new String(headerBytes); + + try { + return objectMapper.readValue(headerJson, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException( + "Failed to deserialise JWT header: the header JSON is invalid or malformed.", e + ); + } + } + + /** + * Retrieves the payload claims from the provided JWT, excluding registered claims. + *

+ * Decodes the Base64-encoded payload, deserialises it into a map, and removes any registered + * claims as defined in {@link RegisteredClaims}. + * + * @param token the JWT string from which to extract the payload + * @return a map containing the custom payload claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the payload cannot be + * deserialised due to invalid JSON format + */ + @Override + public Map getPayload(String token) { + var _token = splitToken(token); + + var payloadBytes = Base64.getDecoder().decode(_token.payload()); + var payloadJson = new String(payloadBytes); + + try { + var payloadMap = objectMapper.readValue(payloadJson, new TypeReference>() { + }); + + payloadMap.keySet().removeIf(RegisteredClaims.VALUES::contains); + return payloadMap; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException( + "Failed to deserialise JWT payload: the payload JSON is invalid or malformed.", e + ); + } + } +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/util/CryptoUtil.java b/simple-jwt/src/main/java/com/onixbyte/jwt/util/CryptoUtil.java new file mode 100644 index 0000000..33a067b --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/util/CryptoUtil.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.jwt.util; + +import com.onixbyte.jwt.constant.Algorithm; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility class for cryptographic operations related to JWT processing. + *

+ * Provides methods for creating and verifying signatures using specified algorithms, primarily for + * JSON Web Token (JWT) authentication purposes. + * + * @author zihluwang + */ +public final class CryptoUtil { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private CryptoUtil() { + } + + private static final byte JWT_PART_SEPARATOR = (byte) 46; + + /** + * Creates a signature for the given header and payload using the specified algorithm + * and secret. + * + * @param algorithm the cryptographic algorithm to use (e.g., HMAC-SHA256) + * @param secret the secret key bytes used for signing + * @param header the header bytes to include in the signature + * @param payload the payload bytes to include in the signature + * @return the generated signature bytes + * @throws IllegalArgumentException if the algorithm is not supported or the key is invalid + */ + public static byte[] createSignatureFor( + Algorithm algorithm, + byte[] secret, + byte[] header, + byte[] payload) { + try { + final var mac = Mac.getInstance(algorithm.getAlgorithm()); + mac.init(new SecretKeySpec(secret, algorithm.getAlgorithm())); + mac.update(header); + mac.update(JWT_PART_SEPARATOR); + return mac.doFinal(payload); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("The provided secret key is invalid for the algorithm '%s'." + .formatted(algorithm.getAlgorithm()), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("The specified algorithm '%s' is not supported." + .formatted(algorithm.getAlgorithm()), e); + } + } + + /** + * Verifies the signature for the given header and payload using the specified algorithm + * and secret. + *

+ * This method converts the header and payload strings to UTF-8 bytes before verification. + * + * @param algorithm the cryptographic algorithm used for signing + * @param secretBytes the secret key bytes used for signing + * @param header the header string to verify + * @param payload the payload string to verify + * @param signatureBytes the signature bytes to check against + * @return {@code true} if the signature is valid, {@code false} otherwise + * @throws IllegalArgumentException if the algorithm is not supported or the key is invalid + */ + public static boolean verifySignatureFor( + Algorithm algorithm, + byte[] secretBytes, + String header, + String payload, + byte[] signatureBytes) { + return verifySignatureFor( + algorithm, + secretBytes, + header.getBytes(StandardCharsets.UTF_8), + payload.getBytes(StandardCharsets.UTF_8), + signatureBytes); + } + + /** + * Verifies the signature for the given header and payload bytes using the specified algorithm + * and secret. + * + * @param algorithm the cryptographic algorithm used for signing + * @param secretBytes the secret key bytes used for signing + * @param headerBytes the header bytes to verify + * @param payloadBytes the payload bytes to verify + * @param signatureBytes the signature bytes to check against + * @return {@code true} if the signature matches, {@code false} otherwise + * @throws IllegalArgumentException if the algorithm is not supported or the key is invalid + */ + public static boolean verifySignatureFor( + Algorithm algorithm, + byte[] secretBytes, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes) { + return MessageDigest.isEqual( + createSignatureFor( + algorithm, + secretBytes, + headerBytes, + payloadBytes), + signatureBytes); + } + +} From 68f42f6fba6c2052fd9a3a9bbd5aba36184154f0 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 3 Apr 2025 10:21:42 +0800 Subject: [PATCH 07/17] feat: `TokenManager` supports to read a bean from a token --- .../java/com/onixbyte/jwt/TokenCreator.java | 5 -- .../java/com/onixbyte/jwt/TokenManager.java | 7 +++ .../java/com/onixbyte/jwt/TokenResolver.java | 50 +++++++++++++++---- .../onixbyte/jwt/impl/HmacTokenManager.java | 50 +++++++++++++++++++ .../onixbyte/jwt/impl/HmacTokenResolver.java | 20 -------- 5 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java create mode 100644 simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java index d0e8f73..c05d38f 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java @@ -17,11 +17,6 @@ package com.onixbyte.jwt; -import com.fasterxml.jackson.core.JsonProcessingException; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - /** * */ diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java new file mode 100644 index 0000000..fcb596e --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java @@ -0,0 +1,7 @@ +package com.onixbyte.jwt; + +public interface TokenManager extends TokenCreator, TokenResolver { + + T extract(String token); + +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java index c8d1b81..1ff5fdb 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java @@ -17,6 +17,7 @@ package com.onixbyte.jwt; +import com.onixbyte.jwt.constant.RegisteredClaims; import com.onixbyte.jwt.data.RawTokenComponent; import java.util.Map; @@ -27,30 +28,61 @@ public interface TokenResolver { /** + * Verifies the HMAC signature of the provided JWT. + *

+ * Splits the token into its components and uses the configured algorithm and secret to check + * the signature's validity. If the signature does not match, an exception is thrown by the + * underlying cryptographic utility. * - * @param token + * @param token the JWT string to verify + * @throws IllegalArgumentException if the token is malformed or the signature verification + * fails due to an invalid algorithm, key, or + * mismatched signature */ void verify(String token); /** + * Retrieves the header claims from the provided JWT. + *

+ * Decodes the Base64-encoded header and deserialises it into a map of strings. * - * @param token - * @return + * @param token the JWT string from which to extract the header + * @return a map containing the header claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the header cannot be + * deserialised due to invalid JSON format */ Map getHeader(String token); /** + * Retrieves the payload claims from the provided JWT, excluding registered claims. + *

+ * Decodes the Base64-encoded payload, deserialises it into a map, and removes any registered + * claims as defined in {@link RegisteredClaims}. * - * @param payload - * @return + * @param token the JWT string from which to extract the payload + * @return a map containing the custom payload claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the payload cannot be + * deserialised due to invalid JSON format */ - Map getPayload(String payload); + Map getPayload(String token); /** + * Splits a JWT into its raw components: header, payload, and signature. * - * @param token - * @return + * @param token the JWT string to split + * @return a {@link RawTokenComponent} containing the header, payload, and signature as strings + * @throws IllegalArgumentException if the token does not consist of exactly three parts + * separated by dots */ - RawTokenComponent splitToken(String token); + default RawTokenComponent splitToken(String token) { + var tokenTuple = token.split("\\."); + + if (tokenTuple.length != 3) { + throw new IllegalArgumentException( + "The provided JWT is invalid: it must consist of exactly three parts separated by dots."); + } + + return new RawTokenComponent(tokenTuple[0], tokenTuple[1], tokenTuple[2]); + } } diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java new file mode 100644 index 0000000..fa3dd20 --- /dev/null +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java @@ -0,0 +1,50 @@ +package com.onixbyte.jwt.impl; + +import com.onixbyte.devkit.utils.MapUtil; +import com.onixbyte.devkit.utils.ObjectMapAdapter; +import com.onixbyte.jwt.TokenCreator; +import com.onixbyte.jwt.TokenManager; +import com.onixbyte.jwt.TokenPayload; +import com.onixbyte.jwt.TokenResolver; +import com.onixbyte.jwt.constant.Algorithm; + +import java.util.Map; + +public class HmacTokenManager implements TokenManager { + + private final TokenCreator tokenCreator; + private final TokenResolver tokenResolver; + private final ObjectMapAdapter adapter; + + public HmacTokenManager(Algorithm algorithm, String issuer, String secret, ObjectMapAdapter adapter) { + this.tokenCreator = new HmacTokenCreator(algorithm, issuer, secret); + this.tokenResolver = new HmacTokenResolver(algorithm, secret); + this.adapter = adapter; + } + + @Override + public T extract(String token) { + var payloadMap = getPayload(token); + return MapUtil.mapToObject(payloadMap, adapter); + } + + @Override + public String sign(TokenPayload payload) { + return tokenCreator.sign(payload); + } + + @Override + public void verify(String token) { + tokenResolver.verify(token); + } + + @Override + public Map getHeader(String token) { + return tokenResolver.getHeader(token); + } + + @Override + public Map getPayload(String token) { + return tokenResolver.getPayload(token); + } +} diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java index e0eabf1..b600a14 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java @@ -73,26 +73,6 @@ public HmacTokenResolver(Algorithm algorithm, String secret) { this.objectMapper = ObjectMapperHolder.getInstance().getObjectMapper(); } - /** - * Splits a JWT into its raw components: header, payload, and signature. - * - * @param token the JWT string to split - * @return a {@link RawTokenComponent} containing the header, payload, and signature as strings - * @throws IllegalArgumentException if the token does not consist of exactly three parts - * separated by dots - */ - @Override - public RawTokenComponent splitToken(String token) { - var tokenTuple = token.split("\\."); - - if (tokenTuple.length != 3) { - throw new IllegalArgumentException( - "The provided JWT is invalid: it must consist of exactly three parts separated by dots."); - } - - return new RawTokenComponent(tokenTuple[0], tokenTuple[1], tokenTuple[2]); - } - /** * Verifies the HMAC signature of the provided JWT. *

From 55a89f6d6892fc825b6a7b0544990ad762bb2429 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Sat, 12 Apr 2025 17:27:26 +0800 Subject: [PATCH 08/17] docs: added Javadoc --- .../java/com/onixbyte/jwt/TokenCreator.java | 19 +++ .../java/com/onixbyte/jwt/TokenManager.java | 44 ++++- .../java/com/onixbyte/jwt/TokenPayload.java | 151 +++++++++++++++++- .../java/com/onixbyte/jwt/TokenResolver.java | 18 ++- .../com/onixbyte/jwt/constant/Algorithm.java | 48 ++++-- .../onixbyte/jwt/constant/HeaderClaims.java | 9 ++ .../jwt/constant/RegisteredClaims.java | 14 ++ .../onixbyte/jwt/data/RawTokenComponent.java | 11 +- .../jwt/holder/ObjectMapperHolder.java | 21 ++- .../onixbyte/jwt/impl/HmacTokenManager.java | 94 +++++++++++ .../onixbyte/jwt/impl/HmacTokenResolver.java | 1 - 11 files changed, 400 insertions(+), 30 deletions(-) diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java index c05d38f..1e3300c 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenCreator.java @@ -18,10 +18,29 @@ package com.onixbyte.jwt; /** + * Interface for creating and signing JSON Web Tokens (JWTs). + *

+ * Defines a contract for implementations that generate signed JWTs from a given payload. The + * resulting token is typically a string in the format "header.payload.signature", where the + * signature is created using a cryptographic algorithm specific to the implementation. * + * @author zihluwang */ public interface TokenCreator { + /** + * Signs a token payload to create a JWT. + *

+ * Takes a {@link TokenPayload} object, serialises its claims, and generates a signed + * JWT string. The specific signing algorithm (e.g., HMAC, RSA, ECDSA) depends on + * the implementation. + * + * @param payload the {@link TokenPayload} containing claims to include in the token + * @return the signed JWT as a string in the format "header.payload.signature" + * @throws IllegalArgumentException if the payload cannot be serialised to JSON due to invalid + * data or structure, or if the signing process fails due to + * configuration issues + */ String sign(TokenPayload payload); } diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java index fcb596e..f8e8b52 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenManager.java @@ -1,7 +1,47 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.onixbyte.jwt; +/** + * Interface for managing JSON Web Tokens (JWTs) with support for signing, verification, and + * payload extraction. + *

+ * Combines the functionality of {@link TokenCreator} for creating signed JWTs and + * {@link TokenResolver} for verifying and parsing them, while adding the ability to extract the + * payload as a custom type {@code T}. Implementations are expected to handle both token generation + * and resolution, providing a unified interface for JWT operations. + * + * @param the type of object to which the token payload will be converted + * @author zihluwang + */ public interface TokenManager extends TokenCreator, TokenResolver { + /** + * Extracts the payload from a JWT and converts it to an object of type {@code T}. + *

+ * Retrieves the payload from the token and transforms it into the specified type using an + * implementation-specific mechanism, such as an adapter or mapper. + * + * @param token the JWT string from which to extract the payload + * @return the payload converted to an object of type {@code T} + * @throws IllegalArgumentException if the token is malformed, the signature is invalid, or the + * payload cannot be deserialised or converted to + * type {@code T} + */ T extract(String token); - -} +} \ No newline at end of file diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java index 506b715..c18b096 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java @@ -24,57 +24,158 @@ import java.util.*; /** + * A builder-style class for constructing JSON Web Token (JWT) payloads. + *

+ * Provides a fluent interface to set standard registered claims (e.g., subject, issuer, audience) + * and custom claims for a JWT payload. The class supports chaining method calls to build the + * payload incrementally, which can then be retrieved as a map for use in JWT creation. Ensures that + * registered claims are set using dedicated methods to prevent misuse. * + * @author zihluwang */ public class TokenPayload { + /** + * Creates a new instance of {@link TokenPayload} with an empty payload. + *

+ * Initialises the payload with empty collections for claims and audiences, ready for + * configuration via the builder methods. + * + * @return a new {@link TokenPayload} instance + */ public static TokenPayload createPayload() { return new TokenPayload(); } + /** + * The map storing custom claims for the JWT payload. + */ private final Map payload; + + /** + * The list of audience identifiers for the JWT. + */ private final List audiences; + /** + * The subject of the JWT, identifying the principal. + */ private String subject; + + /** + * The issuer of the JWT, identifying the entity that issued the token. + */ private String issuer; + + /** + * The unique identifier for the JWT. + */ private String tokenId; + + /** + * The expiration time of the JWT, as seconds since the Unix epoch. + */ private Long expiresAt; + + /** + * The time before which the JWT must not be accepted, as seconds since the Unix epoch. + */ private Long notBefore; + + /** + * The issuance time of the JWT, as seconds since the Unix epoch. + */ private Long issuedAt; /** - * Private constructor to prevent instantiation of this utility class. + * Private constructor to enforce use of the factory method. + *

+ * Initialises the internal collections for storing claims and audiences, preventing direct + * instantiation outside the class. */ private TokenPayload() { payload = new HashMap<>(); audiences = new ArrayList<>(); } + /** + * Adds a single audience to the JWT payload. + *

+ * Appends the specified audience identifier to the list of audiences, allowing the token to be + * validated for multiple recipients. + * + * @param audience the audience identifier to add + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withAudience(String audience) { audiences.add(audience); return this; } + /** + * Adds multiple audiences to the JWT payload. + *

+ * Appends all provided audience identifiers to the list of audiences, enabling the token to be + * validated for multiple recipients. + * + * @param audiences the audience identifiers to add + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withAudiences(String... audiences) { this.audiences.addAll(Arrays.asList(audiences)); return this; } + /** + * Sets the subject of the JWT payload. + *

+ * Specifies the principal that is the subject of the token, typically identifying the user or + * entity the token represents. + * + * @param subject the subject identifier + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withSubject(String subject) { this.subject = subject; return this; } + /** + * Sets the issuer of the JWT payload. + *

+ * Specifies the entity that issued the token, allowing recipients to verify the token's origin. + * + * @param issuer the issuer identifier + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withIssuer(String issuer) { this.issuer = issuer; return this; } + /** + * Sets the unique identifier for the JWT payload. + *

+ * Assigns a unique token ID to the JWT, which can be used to prevent token reuse or for + * tracking purposes. + * + * @param tokenId the unique token identifier + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withTokenId(String tokenId) { this.tokenId = tokenId; return this; } + /** + * Sets the expiration time for the JWT payload. + *

+ * Specifies when the token expires, converted to seconds since the Unix epoch based on the + * system's default time zone. + * + * @param expiresAt the expiration time as a {@link LocalDateTime} + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt.atZone(ZoneId.systemDefault()) .toInstant() @@ -82,6 +183,15 @@ public TokenPayload withExpiresAt(LocalDateTime expiresAt) { return this; } + /** + * Sets the time before which the JWT must not be accepted. + *

+ * Specifies the "not before" time, converted to seconds since the Unix epoch based on the + * system's default time zone. + * + * @param notBefore the time before which the token is invalid, as a {@link LocalDateTime} + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withNotBefore(LocalDateTime notBefore) { this.notBefore = notBefore.atZone(ZoneId.systemDefault()) .toInstant() @@ -89,6 +199,15 @@ public TokenPayload withNotBefore(LocalDateTime notBefore) { return this; } + /** + * Sets the issuance time for the JWT payload. + *

+ * Specifies when the token was issued, converted to seconds since the Unix epoch based on the + * system's default time zone. + * + * @param issuedAt the issuance time as a {@link LocalDateTime} + * @return this {@link TokenPayload} instance for method chaining + */ public TokenPayload withIssuedAt(LocalDateTime issuedAt) { this.issuedAt = issuedAt.atZone(ZoneId.systemDefault()) .toInstant() @@ -96,6 +215,17 @@ public TokenPayload withIssuedAt(LocalDateTime issuedAt) { return this; } + /** + * Adds a custom claim to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @param value the value of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ public TokenPayload withClaim(String name, String value) { if (RegisteredClaims.VALUES.contains(name)) { throw new IllegalStateException("Please set registered claims with pre-defined methods"); @@ -105,10 +235,26 @@ public TokenPayload withClaim(String name, String value) { return this; } + /** + * Checks if the JWT payload has a valid issuer. + *

+ * Returns {@code true} if the issuer is non-null and not blank, indicating that an issuer has + * been set. + * + * @return {@code true} if an issuer is set, {@code false} otherwise + */ public boolean hasIssuer() { return Objects.nonNull(issuer) && !issuer.isBlank(); } + /** + * Retrieves the complete JWT payload as a map. + *

+ * Constructs a map containing all custom claims, registered claims (if set), and audiences. + * Only non-empty or non-blank values are included to ensure a clean payload. + * + * @return a map containing the JWT payload + */ public Map getPayload() { var _payload = new HashMap<>(payload); @@ -128,7 +274,7 @@ public Map getPayload() { .ifPresent((jti) -> _payload.put(RegisteredClaims.TOKEN_ID, jti)); Optional.ofNullable(issuer) - .map((iss) -> !iss.isBlank()) + .filter((iss) -> !iss.isBlank()) .ifPresent((iss) -> _payload.put(RegisteredClaims.ISSUER, iss)); Optional.ofNullable(issuedAt) @@ -139,5 +285,4 @@ public Map getPayload() { return _payload; } - } diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java index 1ff5fdb..1f60c03 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenResolver.java @@ -23,16 +23,23 @@ import java.util.Map; /** + * Interface for resolving and verifying JSON Web Tokens (JWTs). + *

+ * Defines a contract for implementations that parse, verify, and extract components from JWTs. + * Provides methods to validate the token's signature, retrieve its header and payload, and split it + * into raw components. Implementations are expected to handle cryptographic verification and JSON + * deserialisation specific to their signing algorithm. * + * @author zihluwang */ public interface TokenResolver { /** - * Verifies the HMAC signature of the provided JWT. + * Verifies the signature of the provided JWT. *

- * Splits the token into its components and uses the configured algorithm and secret to check - * the signature's validity. If the signature does not match, an exception is thrown by the - * underlying cryptographic utility. + * Splits the token into its components and checks the signature's validity using the + * implementation's configured algorithm and key. If the signature does not match, an exception + * is thrown. * * @param token the JWT string to verify * @throws IllegalArgumentException if the token is malformed or the signature verification @@ -68,6 +75,9 @@ public interface TokenResolver { /** * Splits a JWT into its raw components: header, payload, and signature. + *

+ * Provides a default implementation that separates the token string into its three parts using + * dot separators and returns them as a {@link RawTokenComponent}. * * @param token the JWT string to split * @return a {@link RawTokenComponent} containing the header, payload, and signature as strings diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java index 46c3e07..6a37d57 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/Algorithm.java @@ -18,7 +18,14 @@ package com.onixbyte.jwt.constant; /** + * Enumeration of cryptographic algorithms supported for JSON Web Token (JWT) signing + * and verification. + *

+ * Defines a set of recognised algorithms including HMAC (HS*), RSA (RS*), and ECDSA (ES*) variants, + * each with a specific SHA length (256, 384, or 512 bits). Provides methods to identify the + * algorithm type and retrieve its properties. * + * @author zihluwang */ public enum Algorithm { HS256(1, 256, "HmacSHA256"), @@ -32,32 +39,41 @@ public enum Algorithm { ES512(3, 512, "SHA512withECDSA"); /** - * + * Bit flag indicating an HMAC-based algorithm. */ private static final int HS_FLAG = 1; // 001 /** - * + * Bit flag indicating an RSA-based algorithm. */ private static final int RS_FLAG = 2; // 010 /** - * + * Bit flag indicating an ECDSA-based algorithm. */ private static final int ES_FLAG = 3; // 011 /** - * + * The type flag identifying the algorithm family (HMAC, RSA, or ECDSA). */ private final int typeFlag; + + /** + * The length of the SHA hash in bits (256, 384, or 512). + */ private final int shaLength; + + /** + * The standard name of the algorithm as recognised by the Java Cryptography Architecture (JCA). + */ private final String algorithm; /** + * Constructs an algorithm enum constant with the specified type flag, SHA length, and algorithm name. * - * @param typeFlag - * @param shaLength - * @param algorithm + * @param typeFlag the bit flag identifying the algorithm type + * @param shaLength the length of the SHA hash in bits + * @param algorithm the JCA-compliant algorithm name */ Algorithm(int typeFlag, int shaLength, String algorithm) { this.typeFlag = typeFlag; @@ -66,48 +82,54 @@ public enum Algorithm { } /** + * Determines whether this algorithm is HMAC-based. * - * @return + * @return {@code true} if the algorithm uses HMAC (e.g., HS256, HS384, HS512), {@code false} otherwise */ public boolean isHmac() { return (this.typeFlag & HS_FLAG) != 0; } /** + * Determines whether this algorithm is RSA-based. * - * @return + * @return {@code true} if the algorithm uses RSA (e.g., RS256, RS384, RS512), {@code false} otherwise */ public boolean isRsa() { return (this.typeFlag & RS_FLAG) != 0; } /** + * Determines whether this algorithm is ECDSA-based. * - * @return + * @return {@code true} if the algorithm uses ECDSA (e.g., ES256, ES384, ES512), {@code false} otherwise */ public boolean isEcdsa() { return (this.typeFlag & ES_FLAG) != 0; } /** + * Retrieves the SHA length of this algorithm in bits. * - * @return + * @return the SHA length (256, 384, or 512) */ public int getShaLength() { return shaLength; } /** + * Retrieves the type flag of this algorithm. * - * @return + * @return the type flag (1 for HMAC, 2 for RSA, 3 for ECDSA) */ public int getTypeFlag() { return typeFlag; } /** + * Retrieves the JCA-compliant name of this algorithm. * - * @return + * @return the algorithm name (e.g., "HmacSHA256", "SHA256withRSA") */ public String getAlgorithm() { return algorithm; diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java index 728b2ee..cb681f7 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/HeaderClaims.java @@ -17,6 +17,15 @@ package com.onixbyte.jwt.constant; +/** + * Utility class defining standard header claim names for JSON Web Tokens (JWTs). + *

+ * Provides constants representing the recognised header claims as specified in the JWT standard. + * These claims are used in the header section of a JWT to describe its structure and cryptographic + * properties. + * + * @author zihluwang + */ public final class HeaderClaims { /** diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java index dcde8a9..6cf27a5 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/constant/RegisteredClaims.java @@ -20,7 +20,14 @@ import java.util.List; /** + * Utility class defining standard registered claim names for JSON Web Tokens (JWTs). + *

+ * Provides constants representing the registered claims as defined in RFC 7519. These claims are + * used in the payload section of a JWT to convey metadata about the token, such as its issuer, + * subject, and validity period. All claims are optional but widely recognised in + * JWT implementations. * + * @author zihluwang */ public final class RegisteredClaims { @@ -73,6 +80,13 @@ private RegisteredClaims() { */ public static final String TOKEN_ID = "jti"; + /** + * An immutable list of all registered claim names defined in this class. + *

+ * Contains the values of {@link #ISSUER}, {@link #SUBJECT}, {@link #AUDIENCE}, + * {@link #EXPIRES_AT}, {@link #NOT_BEFORE}, {@link #ISSUED_AT}, and {@link #TOKEN_ID} for + * convenient iteration or lookup. + */ public static final List VALUES = List.of(ISSUER, SUBJECT, AUDIENCE, EXPIRES_AT, NOT_BEFORE, ISSUED_AT, TOKEN_ID); } diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java b/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java index 6b5108c..8125230 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/data/RawTokenComponent.java @@ -18,10 +18,15 @@ package com.onixbyte.jwt.data; /** + * A record representing the raw components of a JSON Web Token (JWT). + *

+ * Holds the header, payload, and signature of a JWT as strings, typically in their Base64 URL-encoded + * form as extracted from a token string. This record is used to facilitate parsing and processing + * of JWTs without decoding or validating their contents. * - * @param header - * @param payload - * @param signature + * @param header the Base64 URL-encoded header string of the JWT + * @param payload the Base64 URL-encoded payload string of the JWT + * @param signature the Base64 URL-encoded signature string of the JWT * @author zihluwang */ public record RawTokenComponent( diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java b/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java index 653b776..28c5ff2 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/holder/ObjectMapperHolder.java @@ -23,6 +23,16 @@ import java.util.Objects; +/** + * Singleton holder for a configured {@link ObjectMapper} instance. + *

+ * Provides a thread-safe, lazily initialised singleton to manage a single {@link ObjectMapper} + * instance, configured to sort JSON properties alphabetically. This class is designed to ensure + * consistent JSON serialisation and deserialisation across the application, particularly for + * JSON Web Token (JWT) processing. + * + * @author zihluwang + */ public class ObjectMapperHolder { private static ObjectMapperHolder HOLDER; @@ -39,9 +49,12 @@ private ObjectMapperHolder() { } /** - * Get singleton instance. + * Retrieves the singleton instance of this holder. + *

+ * Uses double-checked locking to ensure thread-safe, lazy initialisation of the singleton. If + * the instance has not been created, it is initialised in a synchronised block. * - * @return the {@code ObjectMapperHolder} instance + * @return the singleton {@link ObjectMapperHolder} instance */ public static ObjectMapperHolder getInstance() { if (Objects.isNull(HOLDER)) { @@ -55,9 +68,9 @@ public static ObjectMapperHolder getInstance() { } /** - * Get Object + * Retrieves the configured {@link ObjectMapper} instance. * - * @return + * @return the {@link ObjectMapper} configured for alphabetical property sorting */ public ObjectMapper getObjectMapper() { return objectMapper; diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java index fa3dd20..b1925ed 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenManager.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.onixbyte.jwt.impl; import com.onixbyte.devkit.utils.MapUtil; @@ -10,39 +27,116 @@ import java.util.Map; +/** + * A generic token manager implementation for creating, verifying, and extracting data from + * HMAC-signed JSON Web Tokens (JWTs). + *

+ * This class integrates an {@link HmacTokenCreator} for signing tokens and an + * {@link HmacTokenResolver} for verification and parsing, using a specified HMAC algorithm. It + * supports converting token payloads to a custom type {@code T} via an {@link ObjectMapAdapter}. + * + * @param the type of object to which the token payload will be converted + * @author zihluwang + */ public class HmacTokenManager implements TokenManager { private final TokenCreator tokenCreator; private final TokenResolver tokenResolver; private final ObjectMapAdapter adapter; + /** + * Constructs an HMAC token manager with the specified algorithm, issuer, secret, and adapter. + *

+ * Initialises the {@link TokenCreator} and {@link TokenResolver} with the provided HMAC + * algorithm and secret key, and associates an adapter for converting token payloads to the + * generic type {@code T}. + * + * @param algorithm the HMAC algorithm to use for signing and verification + * @param issuer the issuer identifier to include in the token payload if not already present + * @param secret the secret key as a string, used to sign and verify the HMAC signature + * @param adapter the {@link ObjectMapAdapter} for converting payload maps to objects of + * type {@code T} + * @throws IllegalArgumentException if the secret key is too short for the specified algorithm + */ public HmacTokenManager(Algorithm algorithm, String issuer, String secret, ObjectMapAdapter adapter) { this.tokenCreator = new HmacTokenCreator(algorithm, issuer, secret); this.tokenResolver = new HmacTokenResolver(algorithm, secret); this.adapter = adapter; } + /** + * Extracts the payload from a JWT and converts it to an object of type {@code T}. + *

+ * Retrieves the payload as a map and uses the configured {@link ObjectMapAdapter} to transform + * it into the desired type. + * + * @param token the JWT string from which to extract the payload + * @return the payload converted to an object of type {@code T} + * @throws IllegalArgumentException if the token is malformed, the signature is invalid, or the + * payload cannot be deserialised + */ @Override public T extract(String token) { var payloadMap = getPayload(token); return MapUtil.mapToObject(payloadMap, adapter); } + /** + * Signs a token payload to create a JWT. + *

+ * Delegates to the {@link TokenCreator} to generate a signed JWT string from the + * provided payload. + * + * @param payload the {@link TokenPayload} containing claims to include in the token + * @return the signed JWT as a string in the format "header.payload.signature" + * @throws IllegalArgumentException if the payload cannot be serialised to JSON due to invalid + * data or structure + */ @Override public String sign(TokenPayload payload) { return tokenCreator.sign(payload); } + /** + * Verifies the validity of a JWT. + *

+ * Delegates to the {@link TokenResolver} to check the token's signature and structure. + * + * @param token the JWT string to verify + * @throws IllegalArgumentException if the token is malformed or the signature + * verification fails + */ @Override public void verify(String token) { tokenResolver.verify(token); } + /** + * Retrieves the header claims from the provided JWT. + *

+ * Decodes the Base64-encoded header and deserialises it into a map of strings. + * + * @param token the JWT string from which to extract the header + * @return a map containing the header claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the header cannot be + * deserialised due to invalid JSON format + */ @Override public Map getHeader(String token) { return tokenResolver.getHeader(token); } + /** + * Retrieves the payload claims from a JWT. + *

+ * Delegates to the {@link TokenResolver} to extract and deserialise the payload into a map, + * excluding registered claims. + * + * @param token the JWT string from which to extract the payload + * @return a map containing the custom payload claims as key-value pairs + * @throws IllegalArgumentException if the token is malformed or the payload cannot + * be deserialised + */ @Override public Map getPayload(String token) { return tokenResolver.getPayload(token); diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java index b600a14..4c4c54d 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/impl/HmacTokenResolver.java @@ -23,7 +23,6 @@ import com.onixbyte.jwt.TokenResolver; import com.onixbyte.jwt.constant.Algorithm; import com.onixbyte.jwt.constant.RegisteredClaims; -import com.onixbyte.jwt.data.RawTokenComponent; import com.onixbyte.jwt.holder.ObjectMapperHolder; import com.onixbyte.jwt.util.CryptoUtil; From f660f8af59c1499294f0468620b85e06dc3b311e Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 18 Apr 2025 19:35:06 +0800 Subject: [PATCH 09/17] refactor: added logback configuration --- simple-jwt/src/main/resources/logback.xml | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 simple-jwt/src/main/resources/logback.xml diff --git a/simple-jwt/src/main/resources/logback.xml b/simple-jwt/src/main/resources/logback.xml new file mode 100644 index 0000000..fd31eac --- /dev/null +++ b/simple-jwt/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + ${COLOURFUL_OUTPUT} + + + + + + \ No newline at end of file From b205698eb18f481714474369b01ab001d9d65e0c Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 18 Apr 2025 19:35:41 +0800 Subject: [PATCH 10/17] feat: added support for different types of token claims --- .../java/com/onixbyte/jwt/TokenPayload.java | 105 ++++++++++++++++-- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java index c18b096..0b964b3 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java @@ -227,14 +227,101 @@ public TokenPayload withIssuedAt(LocalDateTime issuedAt) { * @throws IllegalStateException if the claim name is a registered claim */ public TokenPayload withClaim(String name, String value) { - if (RegisteredClaims.VALUES.contains(name)) { - throw new IllegalStateException("Please set registered claims with pre-defined methods"); - } + checkClaimName(name); + + this.payload.put(name, value); + return this; + } + + /** + * Adds a custom claim to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @param value the value of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ + public TokenPayload withClaim(String name, Long value) { + checkClaimName(name); + + this.payload.put(name, value); + return this; + } + + /** + * Adds a custom claim to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @param value the value of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ + public TokenPayload withClaim(String name, Double value) { + checkClaimName(name); + + this.payload.put(name, value); + return this; + } + + /** + * Adds a custom claim to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @param value the value of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ + public TokenPayload withClaim(String name, Boolean value) { + checkClaimName(name); this.payload.put(name, value); return this; } + /** + * Adds a custom claim to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @param value the value of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ + public TokenPayload withClaim(String name, LocalDateTime value) { + checkClaimName(name); + + this.payload.put(name, value); + return this; + } + + /** + * Adds a custom claim with null value to the JWT payload. + *

+ * Stores a custom key-value pair in the payload, provided the key is not a registered claim. + * Registered claims must be set using their dedicated methods to ensure proper handling. + * + * @param name the name of the custom claim + * @return this {@link TokenPayload} instance for method chaining + * @throws IllegalStateException if the claim name is a registered claim + */ + public TokenPayload withNullClaim(String name) { + checkClaimName(name); + + this.payload.put(name, null); + return this; + } + /** * Checks if the JWT payload has a valid issuer. *

@@ -261,28 +348,28 @@ public Map getPayload() { Optional.of(audiences) .filter((aud) -> !aud.isEmpty()) .ifPresent((aud) -> _payload.put(RegisteredClaims.AUDIENCE, aud)); - Optional.ofNullable(subject) .filter((sub) -> !sub.isBlank()) .ifPresent((sub) -> _payload.put(RegisteredClaims.SUBJECT, subject)); - Optional.ofNullable(expiresAt) .ifPresent((exp) -> _payload.put(RegisteredClaims.EXPIRES_AT, exp)); - Optional.ofNullable(tokenId) .filter((jti) -> !jti.isBlank()) .ifPresent((jti) -> _payload.put(RegisteredClaims.TOKEN_ID, jti)); - Optional.ofNullable(issuer) .filter((iss) -> !iss.isBlank()) .ifPresent((iss) -> _payload.put(RegisteredClaims.ISSUER, iss)); - Optional.ofNullable(issuedAt) .ifPresent((iat) -> _payload.put(RegisteredClaims.ISSUED_AT, iat)); - Optional.ofNullable(notBefore) .ifPresent((nbf) -> _payload.put(RegisteredClaims.NOT_BEFORE, nbf)); return _payload; } + + private void checkClaimName(String name) { + if (RegisteredClaims.VALUES.contains(name)) { + throw new IllegalStateException("Please set registered claims with pre-defined methods"); + } + } } From e216ca87574299c6210b94bb56d9fc91e62d889a Mon Sep 17 00:00:00 2001 From: zihluwang Date: Fri, 18 Apr 2025 19:36:54 +0800 Subject: [PATCH 11/17] docs: added javadoc --- simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java index 0b964b3..3a0d25d 100644 --- a/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java +++ b/simple-jwt/src/main/java/com/onixbyte/jwt/TokenPayload.java @@ -367,6 +367,11 @@ public Map getPayload() { return _payload; } + /** + * Check the given claim name, make sure the name will not be one of the registered claim name. + * + * @param name a claim name + */ private void checkClaimName(String name) { if (RegisteredClaims.VALUES.contains(name)) { throw new IllegalStateException("Please set registered claims with pre-defined methods"); From 5503dc572badee2916c0c6936d177aecacb67281 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 11:43:41 +0800 Subject: [PATCH 12/17] refactor: changed to version catalogue and upgraded dependencies --- devkit-core/build.gradle.kts | 7 ++-- devkit-utils/build.gradle.kts | 14 ++----- gradle.properties | 10 +---- gradle/libs.versions.toml | 21 ++++++++++ guid/build.gradle.kts | 14 ++----- key-pair-loader/build.gradle.kts | 14 ++----- map-util-unsafe/build.gradle.kts | 14 ++----- num4j/build.gradle.kts | 14 ++----- .../build.gradle.kts | 26 +++++-------- simple-jwt-authzero/build.gradle.kts | 29 ++++++-------- simple-jwt-facade/build.gradle.kts | 17 +++------ .../build.gradle.kts | 36 +++++++----------- simple-jwt/build.gradle.kts | 38 +++++++++++++++---- .../build.gradle.kts | 33 ++++++---------- 14 files changed, 128 insertions(+), 159 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/devkit-core/build.gradle.kts b/devkit-core/build.gradle.kts index f71a423..a8cb743 100644 --- a/devkit-core/build.gradle.kts +++ b/devkit-core/build.gradle.kts @@ -56,11 +56,10 @@ dependencies { val logbackVersion: String by project val junitVersion: String by project - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + testImplementation(libs.junit) } tasks.test { diff --git a/devkit-utils/build.gradle.kts b/devkit-utils/build.gradle.kts index 190d2e4..34a4ceb 100644 --- a/devkit-utils/build.gradle.kts +++ b/devkit-utils/build.gradle.kts @@ -52,16 +52,10 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + testImplementation(libs.junit) } tasks.test { diff --git a/gradle.properties b/gradle.properties index 4856c15..ac0191f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,12 +19,4 @@ artefactVersion=2.1.0 projectUrl=https://onixbyte.com/JDevKit projectGithubUrl=https://github.com/OnixByte/JDevKit licenseName=The Apache License, Version 2.0 -licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt - -jacksonVersion=2.18.2 -javaJwtVersion=4.4.0 -junitVersion=5.11.4 -logbackVersion=1.5.16 -slf4jVersion=2.0.16 -springVersion=6.2.2 -springBootVersion=3.4.2 \ No newline at end of file +licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..08a2190 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,21 @@ +[versions] +slf4j = "2.0.17" +logback = "1.5.18" +jackson = "2.18.3" +jwt = "4.5.0" +spring = "6.2.6" +springBoot = "3.4.4" +junit = "5.12.2" + +[libraries] +slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } +jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } +jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } +jwt = { group = "com.auth0", name = "java-jwt", version.ref = "jwt"} +spring-boot-autoconfigure = { group = "org.springframework.boot", name = "spring-boot-autoconfigure", version.ref = "springBoot" } +spring-boot-starter-logging = { group = "org.springframework.boot", name = "spring-boot-starter-logging", version.ref = "springBoot" } +spring-boot-configuration-processor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "springBoot" } +spring-boot-starter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis", version.ref = "springBoot" } +spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "springBoot" } +junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } diff --git a/guid/build.gradle.kts b/guid/build.gradle.kts index 84cda10..acde71a 100644 --- a/guid/build.gradle.kts +++ b/guid/build.gradle.kts @@ -52,16 +52,10 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + testImplementation(libs.junit) } tasks.test { diff --git a/key-pair-loader/build.gradle.kts b/key-pair-loader/build.gradle.kts index 22e769c..a77dbb4 100644 --- a/key-pair-loader/build.gradle.kts +++ b/key-pair-loader/build.gradle.kts @@ -52,16 +52,10 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + testImplementation(libs.junit) } tasks.test { diff --git a/map-util-unsafe/build.gradle.kts b/map-util-unsafe/build.gradle.kts index 7d46c88..c3ce32b 100644 --- a/map-util-unsafe/build.gradle.kts +++ b/map-util-unsafe/build.gradle.kts @@ -52,16 +52,10 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + testImplementation(libs.junit) } tasks.test { diff --git a/num4j/build.gradle.kts b/num4j/build.gradle.kts index 60c0bd2..267689c 100644 --- a/num4j/build.gradle.kts +++ b/num4j/build.gradle.kts @@ -52,16 +52,10 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + testImplementation(libs.junit) } tasks.test { diff --git a/property-guard-spring-boot-starter/build.gradle.kts b/property-guard-spring-boot-starter/build.gradle.kts index 8db0a5e..4d80cd0 100644 --- a/property-guard-spring-boot-starter/build.gradle.kts +++ b/property-guard-spring-boot-starter/build.gradle.kts @@ -52,22 +52,16 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - val springBootVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation(project(":devkit-core")) - implementation(project(":devkit-utils")) - implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") - implementation("org.springframework.boot:spring-boot-starter-logging:$springBootVersion") - implementation("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") - - testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + api(project(":devkit-utils")) + implementation(libs.spring.boot.autoconfigure) + implementation(libs.spring.boot.starter.logging) + implementation(libs.spring.boot.configuration.processor) + + testImplementation(libs.junit) + testImplementation(libs.spring.boot.starter.test) } tasks.test { diff --git a/simple-jwt-authzero/build.gradle.kts b/simple-jwt-authzero/build.gradle.kts index 0a9e4a0..524df93 100644 --- a/simple-jwt-authzero/build.gradle.kts +++ b/simple-jwt-authzero/build.gradle.kts @@ -52,24 +52,17 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - val jacksonVersion: String by project - val javaJwtVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - - implementation(project(":devkit-utils")) - implementation(project(":guid")) - implementation(project(":key-pair-loader")) - implementation(project(":simple-jwt-facade")) - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("com.auth0:java-jwt:$javaJwtVersion") - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + + api(project(":devkit-utils")) + api(project(":guid")) + api(project(":key-pair-loader")) + api(project(":simple-jwt-facade")) + api(libs.jackson.databind) + api(libs.jwt) + + testImplementation(libs.junit) } tasks.test { diff --git a/simple-jwt-facade/build.gradle.kts b/simple-jwt-facade/build.gradle.kts index 147d9fd..ce7b130 100644 --- a/simple-jwt-facade/build.gradle.kts +++ b/simple-jwt-facade/build.gradle.kts @@ -52,19 +52,14 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project + compileOnly(libs.slf4j) + implementation(libs.logback) - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") + api(project(":devkit-core")) + api(project(":devkit-utils")) + api(project(":guid")) - implementation(project(":devkit-core")) - implementation(project(":devkit-utils")) - implementation(project(":guid")) - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") + testImplementation(libs.junit) } tasks.test { diff --git a/simple-jwt-spring-boot-starter/build.gradle.kts b/simple-jwt-spring-boot-starter/build.gradle.kts index 5e9a94d..b560d47 100644 --- a/simple-jwt-spring-boot-starter/build.gradle.kts +++ b/simple-jwt-spring-boot-starter/build.gradle.kts @@ -52,29 +52,19 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - val javaJwtVersion: String by project - val jacksonVersion: String by project - val springBootVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - - implementation(project(":guid")) - implementation(project(":simple-jwt-facade")) - compileOnly("com.auth0:java-jwt:$javaJwtVersion") - compileOnly(project(":simple-jwt-authzero")) - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") - implementation("org.springframework.boot:spring-boot-starter-logging:$springBootVersion") - implementation("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") - testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") + compileOnly(libs.slf4j) + implementation(libs.logback) + + api(project(":guid")) + api(project(":simple-jwt-facade")) + api(project(":simple-jwt-authzero")) + implementation(libs.spring.boot.autoconfigure) + implementation(libs.spring.boot.starter.logging) + implementation(libs.spring.boot.configuration.processor) + annotationProcessor(libs.spring.boot.configuration.processor) + + testImplementation(libs.junit) + testImplementation(libs.spring.boot.starter.test) } java { diff --git a/simple-jwt/build.gradle.kts b/simple-jwt/build.gradle.kts index cf66377..73f54dc 100644 --- a/simple-jwt/build.gradle.kts +++ b/simple-jwt/build.gradle.kts @@ -1,8 +1,32 @@ +/* + * Copyright (C) 2024-2024 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + plugins { id("java") + id("java-library") + id("maven-publish") + id("signing") } val artefactVersion: String by project +val projectUrl: String by project +val projectGithubUrl: String by project +val licenseName: String by project +val licenseUrl: String by project group = "com.onixbyte" version = artefactVersion @@ -12,13 +36,13 @@ repositories { } dependencies { - val jacksonVersion: String by project - implementation(project(":devkit-core")) - implementation(project(":devkit-utils")) - implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") + compileOnly(libs.slf4j) + implementation(libs.logback) + api(project(":devkit-core")) + api(project(":devkit-utils")) + implementation(libs.jackson.core) + implementation(libs.jackson.databind) + testImplementation(libs.junit) } tasks.test { diff --git a/simple-serial-spring-boot-starter/build.gradle.kts b/simple-serial-spring-boot-starter/build.gradle.kts index a2e1961..247f140 100644 --- a/simple-serial-spring-boot-starter/build.gradle.kts +++ b/simple-serial-spring-boot-starter/build.gradle.kts @@ -52,27 +52,18 @@ tasks.withType { } dependencies { - val slf4jVersion: String by project - val logbackVersion: String by project - val junitVersion: String by project - val jacksonVersion: String by project - val springBootVersion: String by project - - compileOnly("org.slf4j:slf4j-api:$slf4jVersion") - implementation("ch.qos.logback:logback-classic:$logbackVersion") - - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("org.springframework.boot:spring-boot-autoconfigure:$springBootVersion") - implementation("org.springframework.boot:spring-boot-starter-logging:$springBootVersion") - implementation("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") - implementation("org.springframework.boot:spring-boot-starter-data-redis:$springBootVersion") - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") - - testCompileOnly("org.slf4j:slf4j-api:$slf4jVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") - testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") + compileOnly(libs.slf4j) + implementation(libs.logback) + + implementation(libs.jackson.databind) + implementation(libs.spring.boot.autoconfigure) + implementation(libs.spring.boot.starter.logging) + implementation(libs.spring.boot.configuration.processor) + implementation(libs.spring.boot.starter.redis) + annotationProcessor(libs.spring.boot.configuration.processor) + + testImplementation(libs.junit) + testImplementation(libs.spring.boot.starter.test) } tasks.test { From e3500d0b50f31c0abefb679aa64977d0355c1854 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 11:51:34 +0800 Subject: [PATCH 13/17] refactor: updated copyright information --- gradle/libs.versions.toml | 15 +++++++++++++++ key-pair-loader/build.gradle.kts | 2 +- map-util-unsafe/build.gradle.kts | 2 +- num4j/build.gradle.kts | 2 +- .../build.gradle.kts | 2 +- simple-jwt-authzero/build.gradle.kts | 2 +- simple-jwt-facade/build.gradle.kts | 2 +- simple-jwt/build.gradle.kts | 2 +- 8 files changed, 22 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08a2190..8ef45f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,3 +1,18 @@ +# Copyright (C) 2024-2025 OnixByte. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# See the License for the specific language governing permissions and +# limitations under the License. + [versions] slf4j = "2.0.17" logback = "1.5.18" diff --git a/key-pair-loader/build.gradle.kts b/key-pair-loader/build.gradle.kts index a77dbb4..83af866 100644 --- a/key-pair-loader/build.gradle.kts +++ b/key-pair-loader/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/map-util-unsafe/build.gradle.kts b/map-util-unsafe/build.gradle.kts index c3ce32b..e1a64d5 100644 --- a/map-util-unsafe/build.gradle.kts +++ b/map-util-unsafe/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/num4j/build.gradle.kts b/num4j/build.gradle.kts index 267689c..d8d1dc3 100644 --- a/num4j/build.gradle.kts +++ b/num4j/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/property-guard-spring-boot-starter/build.gradle.kts b/property-guard-spring-boot-starter/build.gradle.kts index 4d80cd0..2f23b87 100644 --- a/property-guard-spring-boot-starter/build.gradle.kts +++ b/property-guard-spring-boot-starter/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/simple-jwt-authzero/build.gradle.kts b/simple-jwt-authzero/build.gradle.kts index 524df93..3958f0d 100644 --- a/simple-jwt-authzero/build.gradle.kts +++ b/simple-jwt-authzero/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/simple-jwt-facade/build.gradle.kts b/simple-jwt-facade/build.gradle.kts index ce7b130..b5ac744 100644 --- a/simple-jwt-facade/build.gradle.kts +++ b/simple-jwt-facade/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/simple-jwt/build.gradle.kts b/simple-jwt/build.gradle.kts index 73f54dc..2161129 100644 --- a/simple-jwt/build.gradle.kts +++ b/simple-jwt/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2024 OnixByte. + * Copyright (C) 2024-2025 OnixByte. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From fc945839d240f5805029e42eb06fe827f19315cb Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 13:15:46 +0800 Subject: [PATCH 14/17] refactor: extract common codes --- .../java/com/onixbyte/security/KeyLoader.java | 25 +++++++++++++++++++ .../onixbyte/security/impl/EcKeyLoader.java | 12 ++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java index 0fa6a63..097ae8e 100644 --- a/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java +++ b/key-pair-loader/src/main/java/com/onixbyte/security/KeyLoader.java @@ -49,4 +49,29 @@ public interface KeyLoader { */ PublicKey loadPublicKey(String pemKeyText); + /** + * Retrieves the raw content of a PEM formatted key by removing unnecessary headers, footers, + * and new line characters. + * + *

+ * This method processes the provided PEM key text to return a cleaned string that contains + * only the key content. The method strips away the + * {@code "-----BEGIN (EC )?(PRIVATE|PUBLIC) KEY-----"} and + * {@code "-----END (EC )?(PRIVATE|PUBLIC) KEY-----"} lines, as well as any new line characters, + * resulting in a continuous string representation of the key, which can be used for further + * cryptographic operations. + * + * @param pemKeyText the PEM formatted key as a string, which may include headers, footers and + * line breaks + * @return a string containing the raw key content devoid of any unnecessary formatting + * or whitespace + */ + default String getRawContent(String pemKeyText) { + // remove all unnecessary parts of the pem key text + return pemKeyText + .replaceAll("-----BEGIN (EC )?(PRIVATE|PUBLIC) KEY-----", "") + .replaceAll("-----END (EC )?(PRIVATE|PUBLIC) KEY-----", "") + .replaceAll("\n", ""); + } + } diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java index b94b1f7..96daa58 100644 --- a/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java +++ b/key-pair-loader/src/main/java/com/onixbyte/security/impl/EcKeyLoader.java @@ -82,11 +82,7 @@ public EcKeyLoader() { @Override public ECPrivateKey loadPrivateKey(String pemKeyText) { try { - // remove all unnecessary parts of the pem key text - pemKeyText = pemKeyText - .replaceAll("-----BEGIN (EC )?PRIVATE KEY-----", "") - .replaceAll("-----END (EC )?PRIVATE KEY-----", "") - .replaceAll("\n", ""); + pemKeyText = getRawContent(pemKeyText); var decodedKeyString = decoder.decode(pemKeyText); var keySpec = new PKCS8EncodedKeySpec(decodedKeyString); @@ -112,11 +108,7 @@ public ECPrivateKey loadPrivateKey(String pemKeyText) { @Override public ECPublicKey loadPublicKey(String pemKeyText) { try { - // remove all unnecessary parts of the pem key text - pemKeyText = pemKeyText - .replaceAll("-----BEGIN (EC )?PUBLIC KEY-----", "") - .replaceAll("-----END (EC )?PUBLIC KEY-----", "") - .replaceAll("\n", ""); + pemKeyText = getRawContent(pemKeyText); var keyBytes = decoder.decode(pemKeyText); var spec = new X509EncodedKeySpec(keyBytes); var key = keyFactory.generatePublic(spec); From 998923e47ecd6e249f78e87e5089ad3745a688f3 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 14:54:32 +0800 Subject: [PATCH 15/17] feat: load RSA key pair from pem-formatted key text --- .../onixbyte/security/impl/RsaKeyLoader.java | 93 +++++++++++++++++++ .../onixbyte/security/KeyPairLoaderTest.java | 45 --------- .../test/resources/ec_private_key_pkcs8.pem | 5 - .../src/test/resources/rsa_private_key.pem | 28 ++++++ .../src/test/resources/rsa_public_key.pem | 9 ++ 5 files changed, 130 insertions(+), 50 deletions(-) create mode 100644 key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java delete mode 100644 key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java delete mode 100644 key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem create mode 100644 key-pair-loader/src/test/resources/rsa_private_key.pem create mode 100644 key-pair-loader/src/test/resources/rsa_public_key.pem diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java new file mode 100644 index 0000000..6cea341 --- /dev/null +++ b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024-2025 OnixByte. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.onixbyte.security.impl; + +import com.onixbyte.security.KeyLoader; +import com.onixbyte.security.exception.KeyLoadingException; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class RsaKeyLoader implements KeyLoader { + + private final Base64.Decoder decoder; + private final KeyFactory keyFactory; + + public RsaKeyLoader() { + try { + this.decoder = Base64.getDecoder(); + this.keyFactory = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + throw new KeyLoadingException(e); + } + } + + @Override + public RSAPrivateKey loadPrivateKey(String pemKeyText) { + // Extract the raw key content + var rawKeyContent = getRawContent(pemKeyText); + + // Decode the Base64-encoded content + var keyBytes = decoder.decode(rawKeyContent); + + // Create a PKCS8EncodedKeySpec from the decoded bytes + var keySpec = new PKCS8EncodedKeySpec(keyBytes); + + try { + // Get an RSA KeyFactory and generate the private key + var _key = keyFactory.generatePrivate(keySpec); + if (_key instanceof RSAPrivateKey key) { + return key; + } else { + throw new KeyLoadingException("Unable to load private key from pem-formatted key text."); + } + } catch (InvalidKeySpecException e) { + throw new KeyLoadingException("Key spec is invalid.", e); + } + } + + @Override + public RSAPublicKey loadPublicKey(String pemKeyText) { + // Extract the raw key content + var rawKeyContent = getRawContent(pemKeyText); + + // Decode the Base64-encoded content + var keyBytes = decoder.decode(rawKeyContent); + + // Create an X509EncodedKeySpec from the decoded bytes + var keySpec = new X509EncodedKeySpec(keyBytes); + + // Get an RSA KeyFactory and generate the public key + try { + var _key = keyFactory.generatePublic(keySpec); + if (_key instanceof RSAPublicKey key) { + return key; + } else { + throw new KeyLoadingException("Unable to load public key from pem-formatted key text."); + } + } catch (InvalidKeySpecException e) { + throw new KeyLoadingException("Key spec is invalid.", e); + } + } +} diff --git a/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java b/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java deleted file mode 100644 index ca57f74..0000000 --- a/key-pair-loader/src/test/java/com/onixbyte/security/KeyPairLoaderTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024-2024 OnixByte. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.onixbyte.security; - -import com.onixbyte.security.impl.EcKeyLoader; -import org.junit.jupiter.api.Test; - -public class KeyPairLoaderTest { - - @Test - public void test() { - var keyLoader = new EcKeyLoader(); - // The following key pair is only used for test only, and is already exposed to public. - // DO NOT USE THEM FOR PRODUCTION! - var privateKey = keyLoader.loadPrivateKey(""" - -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 - +PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn - Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R - -----END PRIVATE KEY----- - """); - var publicKey = keyLoader.loadPublicKey(""" - -----BEGIN PUBLIC KEY----- - MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZAOxkWNouIIPKsdF1YgF1D/XD8Pa - ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ== - -----END PUBLIC KEY----- - """); - } - -} diff --git a/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem b/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem deleted file mode 100644 index 02dfcc8..0000000 --- a/key-pair-loader/src/test/resources/ec_private_key_pkcs8.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 -+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn -Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R ------END PRIVATE KEY----- diff --git a/key-pair-loader/src/test/resources/rsa_private_key.pem b/key-pair-loader/src/test/resources/rsa_private_key.pem new file mode 100644 index 0000000..f8b0fc4 --- /dev/null +++ b/key-pair-loader/src/test/resources/rsa_private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j +J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al +BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6 +pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13 +esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl +e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu ++h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m +pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus +d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg +dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA +M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo +wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9 +8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs +EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl +535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7 +N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd +wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo +J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr ++pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE +qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf +JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6 +w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv +qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y +U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq +iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0 +hXKe4HcuhQ3G0a2hjayiQ84= +-----END PRIVATE KEY----- diff --git a/key-pair-loader/src/test/resources/rsa_public_key.pem b/key-pair-loader/src/test/resources/rsa_public_key.pem new file mode 100644 index 0000000..edde855 --- /dev/null +++ b/key-pair-loader/src/test/resources/rsa_public_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn +B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8 +75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1 +UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM +pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8 +t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5 +gQIDAQAB +-----END PUBLIC KEY----- From 2b99dfb3f25f8f0937866ff235daa31f29782008 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 15:08:06 +0800 Subject: [PATCH 16/17] docs: completed javadocs --- .../onixbyte/security/impl/RsaKeyLoader.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java index 6cea341..7f1b6e4 100644 --- a/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java +++ b/key-pair-loader/src/main/java/com/onixbyte/security/impl/RsaKeyLoader.java @@ -29,11 +29,32 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +/** + * A class responsible for loading RSA keys from PEM formatted text. + *

+ * This class implements the {@link KeyLoader} interface and provides methods to load both private + * and public RSA keys. The keys are expected to be in the standard PEM format, which includes + * Base64-encoded key content surrounded by header and footer lines. The class handles the decoding + * of Base64 content and the generation of keys using the RSA key factory. + *

+ * Any exceptions encountered during the loading process are encapsulated in a + * {@link KeyLoadingException}, allowing for flexible error handling. + * + * @author siujamo + * @see KeyLoader + * @see KeyLoadingException + */ public class RsaKeyLoader implements KeyLoader { private final Base64.Decoder decoder; private final KeyFactory keyFactory; + /** + * Constructs an instance of {@code RsaKeyLoader}. + *

+ * This constructor initialises the Base64 decoder and the RSA {@link KeyFactory}. It may throw + * a {@link KeyLoadingException} if the RSA algorithm is not available. + */ public RsaKeyLoader() { try { this.decoder = Base64.getDecoder(); @@ -43,6 +64,17 @@ public RsaKeyLoader() { } } + /** + * Loads an RSA private key from a given PEM formatted key text. + *

+ * This method extracts the raw key content from the provided PEM text, decodes the + * Base64-encoded content, and generates an instance of {@link RSAPrivateKey}. If the key cannot + * be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown. + * + * @param pemKeyText the PEM formatted private key text + * @return an instance of {@link RSAPrivateKey} + * @throws KeyLoadingException if the key loading process encounters an error + */ @Override public RSAPrivateKey loadPrivateKey(String pemKeyText) { // Extract the raw key content @@ -67,6 +99,17 @@ public RSAPrivateKey loadPrivateKey(String pemKeyText) { } } + /** + * Loads an RSA public key from a given PEM formatted key text. + *

+ * This method extracts the raw key content from the provided PEM text, decodes the + * Base64-encoded content, and generates an instance of {@link RSAPublicKey}. If the key cannot + * be loaded due to invalid specifications or types, a {@link KeyLoadingException} is thrown. + * + * @param pemKeyText the PEM formatted public key text + * @return an instance of {@link RSAPublicKey} + * @throws KeyLoadingException if the key loading process encounters an error + */ @Override public RSAPublicKey loadPublicKey(String pemKeyText) { // Extract the raw key content From 31dd63efef99db35e227fabd318e5d73a783b032 Mon Sep 17 00:00:00 2001 From: siujamo Date: Thu, 24 Apr 2025 15:14:47 +0800 Subject: [PATCH 17/17] docs: completed readme --- key-pair-loader/README.md | 67 +++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/key-pair-loader/README.md b/key-pair-loader/README.md index 2a8186d..3d32826 100644 --- a/key-pair-loader/README.md +++ b/key-pair-loader/README.md @@ -43,22 +43,71 @@ ZyYNcH60ONRWjMqlQXozWMb2i7WphKxf8kopp42nzCflWQod+JQY+hM/EQ== -----END PUBLIC KEY----- ``` -#### Convert private key to EC formats which could be acceptable by Java +## RSA-based algorithm -Java's `PKCS8EncodedKeySpec` requires the private key to be in PKCS#8 format, while OpenSSL by -default generates private keys in traditional PEM format. To convert the private key, run the -following command: +### Generate key pair + +#### Generate private key + +Generate a private key by `genpkey` command provided by OpenSSL: ```shell -openssl pkcs8 -topk8 -inform PEM -outform PEM -in ec_private_key.pem -out ec_private_key_pkcs8.pem -nocrypt +openssl genpkey -algorithm RSA -out rsa_private_key.pem -pkeyopt rsa_keygen_bits:2048 ``` -The converted private key will look like this: +The output of this command is a file called `rsa_private_key.pem` and its content looks like the +following: ```text -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7 -+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn -Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4VIFYJFMAs15j +J3V3IicHd7sI2TIFqTZME40zlOlVAlPKLZmTQvZFLNgaUAAsvPi5i1DR2ywwK6Al +BfnwVnzvmDXC5mKHOz4oxOQVA6Nlp2yVaQMzidmfYNSkMtcv/4HRPsatc7K/M5l6 +pCP20DVRjkikBdIy8e9w+x6BrIFp5Q8PZc/X2BGNAUMMYACdeYH5R/A0CxqkND13 +esc4gkynMOrvZrZGHCz51usfSCqyDWWwsN+GG6LYWia4GkNlS0erQnP8gS93dfjl +e96BIfy3z7Iv+kUrf5ikNW2P8jMxLAv6LO+dcUAu9k477wIAF7Iq5KMuH/otsDOu ++h+2qXmBAgMBAAECggEAdRqcmC0g+y6arxV3fkObthjPGYAa57KBCWUa7B0n30+m +pavVRS2Jpttb2SSqwG4ouI6rARti/iBEd9EWqTCP4AieKZetFOpqCJ24lPRPRGus +d9S6jr5N4qut+vSCp37NABijZj4uJ540nTH0R7qtuhTnynl4Q0/1wwiYvTvVF1Lg +dn+I/8aRbshwDhdAOWOUe6GL7/eaCYgN8/UmlKIpp8tg0w2iWxbaFiR7gZiM41LA +M6SXXfcCas+ZVXsGbzQ3SNiVurCGuuRNcCScXS3/WoEDIb3cNtp49iOmQS+nmEoo +wh4uiEd+0+BrzxngS4o5+mKnHJnwgY0+veGVYLMR5QKBgQD9WKQmevMDU5c+NPq9 +8jaR457Fuxq1gwzeFNJdWfOc/K2LEWh+nFNFCb++EboEj6FdxWaWNMxbrmJps5gs +EoBUYy/Tl7UycDqDfiYLmDdTsf2pVjjh9jaIADiLcJ8S6wwJMZKub7Tp8UVkenAl +535MqShLUC11Y7VxLb3Tsll4XwKBgQD67mm6iCmshr/eszPfNE3ylZ+PiNa7nat7 +N7lQzBIiRJflT1kmVidC5gE+jASqH728ChkZZKxbHsjxpmWdAhLOITdXoTB4sDsd +wtV1lxkXxK9FnrpFvO3y1wZ/QsD3Z2KXxHYZqawkUETO9F3nqAXW0b2GDar5Qiyo +J3Tx/43aHwKBgDC0NMJtCoDONhowZy/S+6iqQKC0qprQec3L5PErVMkOTnKYwyTr ++pogGKt6ju9HiXcUdvdTaSIK8UJu00dNuzv94XjlBmGO78DNpJTAC4rcge5m9AKE +qdEVcclkukARzbuKuy8rrHT4/CUn4J141m/4aRWpcUPLCluato6XD9ozAoGBANvf +JhOFFgcPd3YazfvpZ9eE1XA+tfFlYYmxNRcgCU+vjO0oDvSxjutmgHae18N91pG6 +w21lskSRf/+GDwl5dKLbphOJsOA/gz07qDDGOf2CoRW+1Hcg6drcINxH0K+4DkLv +qZApBSY4k2JH6zR+HMeztn6M4WBRZLHfCPC3PUN/AoGAA3AoHbLTZvqMIKSDkP4Y +U/tTsSFDY4aYo7LG/jk8af3oPU3KyGh4ZFBd6aMmXbS8f8FjvmrM+/e+y9OOGAlq +iOl0hYrs5cJSMLW6i4KnJYuYbMkgmk3bN2t9apu64xKR94gbPrI6AGnPZp+iIzp0 +hXKe4HcuhQ3G0a2hjayiQ84= -----END PRIVATE KEY----- +``` + +#### Generate public key by private key + +Export public key from private key by OpenSSL: + +```shell +openssl pkey -in rsa_private_key.pem -pubout -out rsa_public_key.pem +``` + +The output of this command is a file called `rsa_public_key.pem` and its content looks like the +following: + +```text +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+FSBWCRTALNeYyd1dyIn +B3e7CNkyBak2TBONM5TpVQJTyi2Zk0L2RSzYGlAALLz4uYtQ0dssMCugJQX58FZ8 +75g1wuZihzs+KMTkFQOjZadslWkDM4nZn2DUpDLXL/+B0T7GrXOyvzOZeqQj9tA1 +UY5IpAXSMvHvcPsegayBaeUPD2XP19gRjQFDDGAAnXmB+UfwNAsapDQ9d3rHOIJM +pzDq72a2Rhws+dbrH0gqsg1lsLDfhhui2FomuBpDZUtHq0Jz/IEvd3X45XvegSH8 +t8+yL/pFK3+YpDVtj/IzMSwL+izvnXFALvZOO+8CABeyKuSjLh/6LbAzrvoftql5 +gQIDAQAB +-----END PUBLIC KEY----- ``` \ No newline at end of file