Merge pull request #53 from onixbyte/feature/rsa-key-loader
feat: add functionality to load RSA key pairs from text
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -52,16 +52,10 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+1
-9
@@ -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
|
||||
licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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"
|
||||
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" }
|
||||
+4
-10
@@ -52,16 +52,10 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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-----
|
||||
```
|
||||
@@ -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.
|
||||
@@ -52,16 +52,10 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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", "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,11 +82,7 @@ public class EcKeyLoader implements KeyLoader {
|
||||
@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 class EcKeyLoader implements KeyLoader {
|
||||
@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);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A class responsible for loading RSA keys from PEM formatted text.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* <p>
|
||||
* 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();
|
||||
this.keyFactory = KeyFactory.getInstance("RSA");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new KeyLoadingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an RSA private key from a given PEM formatted key text.
|
||||
* <p>
|
||||
* 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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an RSA public key from a given PEM formatted key text.
|
||||
* <p>
|
||||
* 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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-----
|
||||
""");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgs79JlARgXEf6EDV7
|
||||
+PHQCTHEMtqIoHOy1GZ1+ynQJ6yhRANCAARkA7GRY2i4gg8qx0XViAXUP9cPw9pn
|
||||
Jg1wfrQ41FaMyqVBejNYxvaLtamErF/ySimnjafMJ+VZCh34lBj6Ez8R
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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.
|
||||
@@ -52,16 +52,10 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+5
-11
@@ -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.
|
||||
@@ -52,16 +52,10 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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.
|
||||
@@ -52,22 +52,16 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val slf4jVersion: String by project
|
||||
val logbackVersion: String by project
|
||||
val junitVersion: String by project
|
||||
val springBootVersion: String by project
|
||||
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)
|
||||
|
||||
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")
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -52,24 +52,17 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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(libs.slf4j)
|
||||
implementation(libs.logback)
|
||||
|
||||
compileOnly("org.slf4j:slf4j-api:$slf4jVersion")
|
||||
implementation("ch.qos.logback:logback-classic:$logbackVersion")
|
||||
api(project(":devkit-utils"))
|
||||
api(project(":guid"))
|
||||
api(project(":key-pair-loader"))
|
||||
api(project(":simple-jwt-facade"))
|
||||
api(libs.jackson.databind)
|
||||
api(libs.jwt)
|
||||
|
||||
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")
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@@ -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.
|
||||
@@ -52,19 +52,14 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -52,29 +52,19 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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(libs.slf4j)
|
||||
implementation(libs.logback)
|
||||
|
||||
compileOnly("org.slf4j:slf4j-api:$slf4jVersion")
|
||||
implementation("ch.qos.logback:logback-classic:$logbackVersion")
|
||||
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)
|
||||
|
||||
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")
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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 {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 creating and signing JSON Web Tokens (JWTs).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
}
|
||||
@@ -0,0 +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.
|
||||
* <p>
|
||||
* 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 <T> the type of object to which the token payload will be converted
|
||||
* @author zihluwang
|
||||
*/
|
||||
public interface TokenManager<T> extends TokenCreator, TokenResolver {
|
||||
|
||||
/**
|
||||
* Extracts the payload from a JWT and converts it to an object of type {@code T}.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
/**
|
||||
* A builder-style class for constructing JSON Web Token (JWT) payloads.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String, Object> payload;
|
||||
|
||||
/**
|
||||
* The list of audience identifiers for the JWT.
|
||||
*/
|
||||
private final List<String> 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 enforce use of the factory method.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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()
|
||||
.getEpochSecond();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time before which the JWT must not be accepted.
|
||||
* <p>
|
||||
* 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()
|
||||
.getEpochSecond();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the issuance time for the JWT payload.
|
||||
* <p>
|
||||
* 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()
|
||||
.getEpochSecond();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom claim to the JWT payload.
|
||||
* <p>
|
||||
* 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) {
|
||||
checkClaimName(name);
|
||||
|
||||
this.payload.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom claim to the JWT payload.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String, Object> 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)
|
||||
.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 com.onixbyte.jwt.data.RawTokenComponent;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface for resolving and verifying JSON Web Tokens (JWTs).
|
||||
* <p>
|
||||
* 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 signature of the provided JWT.
|
||||
* <p>
|
||||
* 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
|
||||
* fails due to an invalid algorithm, key, or
|
||||
* mismatched signature
|
||||
*/
|
||||
void verify(String token);
|
||||
|
||||
/**
|
||||
* Retrieves the header claims from the provided JWT.
|
||||
* <p>
|
||||
* 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
|
||||
*/
|
||||
Map<String, String> getHeader(String token);
|
||||
|
||||
/**
|
||||
* Retrieves the payload claims from the provided JWT, excluding registered claims.
|
||||
* <p>
|
||||
* 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
|
||||
*/
|
||||
Map<String, Object> getPayload(String token);
|
||||
|
||||
/**
|
||||
* Splits a JWT into its raw components: header, payload, and signature.
|
||||
* <p>
|
||||
* 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
|
||||
* @throws IllegalArgumentException if the token does not consist of exactly three parts
|
||||
* separated by dots
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Enumeration of cryptographic algorithms supported for JSON Web Token (JWT) signing
|
||||
* and verification.
|
||||
* <p>
|
||||
* 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"),
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
this.shaLength = shaLength;
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this algorithm is HMAC-based.
|
||||
*
|
||||
* @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 {@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 {@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 the SHA length (256, 384, or 512)
|
||||
*/
|
||||
public int getShaLength() {
|
||||
return shaLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type flag of this algorithm.
|
||||
*
|
||||
* @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 the algorithm name (e.g., "HmacSHA256", "SHA256withRSA")
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Utility class defining standard header claim names for JSON Web Tokens (JWTs).
|
||||
* <p>
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Utility class defining standard registered claim names for JSON Web Tokens (JWTs).
|
||||
* <p>
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* 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 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1">Section 4.1.1</a>
|
||||
*/
|
||||
public static final String ISSUER = "iss";
|
||||
|
||||
/**
|
||||
* The "sub" (subject) claim identifies the principal that is the subject of the JWT.
|
||||
* Refer RFC 7529 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2">Section 4.1.2</a>
|
||||
*/
|
||||
public static final String SUBJECT = "sub";
|
||||
|
||||
/**
|
||||
* The "aud" (audience) claim identifies the recipients that the JWT is intended for.
|
||||
* Refer RFC 7529 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3">Section 4.1.3</a>
|
||||
*/
|
||||
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 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4">Section 4.1.4</a>
|
||||
*/
|
||||
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 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5">Section 4.1.5</a>
|
||||
*/
|
||||
public static final String NOT_BEFORE = "nbf";
|
||||
|
||||
/**
|
||||
* The "iat" (issued at) claim identifies the time at which the JWT was issued.
|
||||
* Refer RFC 7529 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6">Section 4.1.6</a>
|
||||
*/
|
||||
public static final String ISSUED_AT = "iat";
|
||||
|
||||
/**
|
||||
* The "jti" (JWT ID) claim provides a unique identifier for the JWT.
|
||||
* Refer RFC 7529 <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7">Section 4.1.7</a>
|
||||
*/
|
||||
public static final String TOKEN_ID = "jti";
|
||||
|
||||
/**
|
||||
* An immutable list of all registered claim names defined in this class.
|
||||
* <p>
|
||||
* 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<String> VALUES = List.of(ISSUER, SUBJECT, AUDIENCE, EXPIRES_AT, NOT_BEFORE, ISSUED_AT, TOKEN_ID);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A record representing the raw components of a JSON Web Token (JWT).
|
||||
* <p>
|
||||
* 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 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(
|
||||
String header,
|
||||
String payload,
|
||||
String signature
|
||||
) {
|
||||
}
|
||||
@@ -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.holder;
|
||||
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Singleton holder for a configured {@link ObjectMapper} instance.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the singleton instance of this holder.
|
||||
* <p>
|
||||
* 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 singleton {@link ObjectMapperHolder} instance
|
||||
*/
|
||||
public static ObjectMapperHolder getInstance() {
|
||||
if (Objects.isNull(HOLDER)) {
|
||||
synchronized (ObjectMapperHolder.class) {
|
||||
if (Objects.isNull(HOLDER)) {
|
||||
HOLDER = new ObjectMapperHolder();
|
||||
}
|
||||
}
|
||||
}
|
||||
return HOLDER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configured {@link ObjectMapper} instance.
|
||||
*
|
||||
* @return the {@link ObjectMapper} configured for alphabetical property sorting
|
||||
*/
|
||||
public ObjectMapper getObjectMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String, String>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* A generic token manager implementation for creating, verifying, and extracting data from
|
||||
* HMAC-signed JSON Web Tokens (JWTs).
|
||||
* <p>
|
||||
* 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 <T> the type of object to which the token payload will be converted
|
||||
* @author zihluwang
|
||||
*/
|
||||
public class HmacTokenManager<T> implements TokenManager<T> {
|
||||
|
||||
private final TokenCreator tokenCreator;
|
||||
private final TokenResolver tokenResolver;
|
||||
private final ObjectMapAdapter<T> adapter;
|
||||
|
||||
/**
|
||||
* Constructs an HMAC token manager with the specified algorithm, issuer, secret, and adapter.
|
||||
* <p>
|
||||
* 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<T> 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}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String, String> getHeader(String token) {
|
||||
return tokenResolver.getHeader(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the payload claims from a JWT.
|
||||
* <p>
|
||||
* 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<String, Object> getPayload(String token) {
|
||||
return tokenResolver.getPayload(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.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).
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the HMAC signature of the provided JWT.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String, String> 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.
|
||||
* <p>
|
||||
* 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<String, Object> 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<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<configuration>
|
||||
<property name="COLOURFUL_OUTPUT" value="%black(%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK}) %highlight(%-5level) %black(---) %black([%10.10t]) %cyan(%-20.20logger{20}) %black(:) %msg%n"/>
|
||||
<property name="STANDARD_OUTPUT" value="%date{'dd MMM, yyyy HH:mm:ss', Asia/Hong_Kong, en-UK} %-5level %black(---) [%10.10t] %-20.20logger{20} : %msg%n"/>
|
||||
|
||||
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>${COLOURFUL_OUTPUT}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -52,27 +52,18 @@ tasks.withType<Jar> {
|
||||
}
|
||||
|
||||
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(libs.slf4j)
|
||||
implementation(libs.logback)
|
||||
|
||||
compileOnly("org.slf4j:slf4j-api:$slf4jVersion")
|
||||
implementation("ch.qos.logback:logback-classic:$logbackVersion")
|
||||
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)
|
||||
|
||||
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")
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
Reference in New Issue
Block a user