SignedApiTokenSecurity.java
/*
* Copyright 2025 Global Crop Diversity Trust
*
* 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 org.genesys.blocks.tokenauth.service.impl;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.tokenauth.service.ApiTokenSecurity;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SignedApiTokenSecurity implements ApiTokenSecurity {
SecretKeySpec secretKey;
Encoder base64encoder = Base64.getUrlEncoder();
Decoder base64decoder = Base64.getUrlDecoder();
public SignedApiTokenSecurity(byte[] signingKey) {
secretKey = new SecretKeySpec(signingKey, "HmacSHA256");
}
/**
* Does the {@code secureToken} look like something we support?
*
* @return {@code true} if in
*/
@Override
public boolean supports(@NonNull String secureToken) {
if (StringUtils.countMatches(secureToken, '.') == 1) {
return true;
}
log.trace("Secure token {} is not in the right format.", secureToken);
return false;
}
/**
* The secured token is Base64.encode(token) + "." + Base64.encode(signature)
* @throws GeneralSecurityException When stuff happens
* @return A signed token
*/
@Override
public String signToken(byte[] tokenBytes) throws GeneralSecurityException {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
sha256_HMAC.init(secretKey);
var digitalSignature = sha256_HMAC.doFinal(tokenBytes);
var signedToken = base64encoder.encodeToString(tokenBytes) + "." + base64encoder.encodeToString(DigestUtils.sha256(digitalSignature));
log.trace("For produced signed token {}", signedToken);
return signedToken;
}
@Override
public byte[] getRawToken(String secureToken) throws GeneralSecurityException {
// To read: formatted token -> UUID + Signature
// Check signature:
if (! supports(secureToken)) {
throw new GeneralSecurityException("Invalid token format");
}
var splitToken = secureToken.split("\\.", 2);
if (splitToken.length != 2) {
throw new GeneralSecurityException("Invalid token format");
}
log.trace("Split token into data={} and signature={}", splitToken[0], splitToken[1]);
byte[] tokenData = null;
byte[] tokenSignature = null;
try {
tokenData = base64decoder.decode(splitToken[0]);
tokenSignature = base64decoder.decode(splitToken[1]);
} catch (IllegalArgumentException e) {
log.warn("Token data={} or signature={} are not Base64 encoded", splitToken[0], splitToken[1]);
throw new GeneralSecurityException("Invalid token encoding");
}
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
sha256_HMAC.init(secretKey);
var digitalSignature = sha256_HMAC.doFinal(tokenData);
var signatureHash = DigestUtils.sha256(digitalSignature);
var isValid = Arrays.equals(signatureHash, tokenSignature);
log.debug("Token signature is {}", isValid ? "valid" : "not valid");
if (isValid) {
return tokenData;
} else {
throw new GeneralSecurityException("Invalid signature");
}
}
}