BasicDPoPParser.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2026 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.pdd.core.token.parser;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.cxf.rs.security.jose.jwk.JsonWebKey;
import org.apache.cxf.rs.security.jose.jwk.JwkReaderWriter;
import org.openspcoop2.pdd.core.token.Costanti;
import org.openspcoop2.pdd.core.token.TokenUtilities;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.security.JWTParser;
/**
* BasicDPoPParser
*
* @author Poli Andrea (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class BasicDPoPParser implements IDPoPParser {
private TipologiaClaimsDPoP tipologiaClaims;
private Properties parserConfig;
private String raw;
private Map<String,Serializable> header;
private Map<String,Serializable> payload;
public BasicDPoPParser(TipologiaClaimsDPoP tipologiaClaims) {
this(tipologiaClaims, null);
}
public BasicDPoPParser(TipologiaClaimsDPoP tipologiaClaims, Properties parserConfig) {
this.tipologiaClaims = tipologiaClaims;
this.parserConfig = parserConfig;
}
@Override
public void init(String raw) throws UtilsException {
this.raw = raw;
// Parse header e payload con JWTParser
JWTParser jwtParser = new JWTParser(this.raw);
this.header = convert(jwtParser.getHeaderClaims());
this.payload = convert(jwtParser.getPayloadClaims());
}
private Map<String,Serializable> convert(Map<String,String> m){
Map<String,Serializable> newM = null;
if(m!=null) {
newM = new HashMap<>();
if(!m.isEmpty()) {
for (Map.Entry<String,String> entry : m.entrySet()) {
newM.put(entry.getKey(), entry.getValue());
}
}
}
return newM;
}
@Override
public String getRaw() throws UtilsException{
return this.raw;
}
@Override
public void checkHttpTransaction(Integer httpResponseCode) throws UtilsException {
// Per DPoP non serve controllare httpResponseCode come per introspection/userInfo
}
@Override
public boolean isValid() {
// Verifica base che i claim necessari siano presenti
try {
return this.header != null && this.payload != null &&
this.getType() != null &&
this.getAlgorithm() != null &&
this.getJsonWebKeyAsString() != null &&
this.getHttpMethod() != null &&
this.getHttpUri() != null &&
this.getIssuedAt() != null &&
this.getAccessTokenHash() != null &&
this.getJWTIdentifier() != null;
}catch(Exception e) {
return false;
}
}
// Header claims
@Override
public String getType() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.header, Claims.JSON_WEB_TOKEN_RFC_7515_TYPE);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_TYP);
tmp = TokenUtilities.getFirstClaimAsString(this.header, claimNames);
break;
case CUSTOM:
// Il plugin custom deve implementare direttamente questo metodo
break;
}
return tmp;
}
@Override
public String getAlgorithm() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.header, Claims.JSON_WEB_TOKEN_RFC_7515_ALGORITHM);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_ALG);
tmp = TokenUtilities.getFirstClaimAsString(this.header, claimNames);
break;
case CUSTOM:
break;
}
return tmp;
}
private JsonWebKey jsonWebKeyObject = null;
@Override
public JsonWebKey getJsonWebKey() throws UtilsException {
if(this.jsonWebKeyObject != null) {
return this.jsonWebKeyObject;
}
String jwkJson = getJsonWebKeyAsString();
// Parse JWK using Apache CXF
JwkReaderWriter jwkReader = new JwkReaderWriter();
this.jsonWebKeyObject = jwkReader.jsonToJwk(jwkJson);
return this.jsonWebKeyObject;
}
private String jsonWebKey = null;
@Override
public String getJsonWebKeyAsString() throws UtilsException {
if(this.jsonWebKey != null) {
return this.jsonWebKey;
}
// RFC 9449 specifies that jwk is a JSON object in the header
// JWTParser flattens nested objects (jwk.alg, jwk.n, etc.), so we need to parse the raw JWT header directly
String jwkClaimName = getJsonWebKeyClaimName();
if(jwkClaimName==null) {
return null;
}
try {
// Parse JWT header directly from raw token to preserve nested structure
if(this.raw==null || this.raw.isEmpty()) {
return null;
}
String[] parts = this.raw.split("\\.");
if(parts.length < 2) {
return null;
}
// Decode Base64URL header
byte[] headerBytes = java.util.Base64.getUrlDecoder().decode(parts[0]);
String headerJson = new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8);
// Parse header as JSON to extract jwk object
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, Object> headerMap = mapper.readValue(headerJson, Map.class);
Object jwkObject = headerMap.get(jwkClaimName);
if(jwkObject==null) {
return null;
}
// Serialize jwk object back to JSON string
String jwkJson = null;
if(jwkObject instanceof Map) {
jwkJson = mapper.writeValueAsString(jwkObject);
}
else if(jwkObject instanceof String) {
jwkJson = (String) jwkObject;
}
else {
return null;
}
if(jwkJson==null || jwkJson.trim().isEmpty()) {
return null;
}
this.jsonWebKey = jwkJson;
return jwkJson;
}catch(Exception e) {
throw new UtilsException(e.getMessage(),e);
}
}
private String getJsonWebKeyClaimName() {
String jwkClaimName = null;
switch (this.tipologiaClaims) {
case RFC9449:
jwkClaimName = Claims.DPOP_RFC9449_JWK;
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_JWK);
if(claimNames!=null && !claimNames.isEmpty()) {
jwkClaimName = claimNames.get(0); // Use first configured name
}
break;
case CUSTOM:
return null;
}
return jwkClaimName;
}
// Payload claims
@Override
public String getJWTIdentifier() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.payload, Claims.JSON_WEB_TOKEN_RFC_7519_JWT_ID);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_JTI);
tmp = TokenUtilities.getFirstClaimAsString(this.payload, claimNames);
break;
case CUSTOM:
break;
}
return tmp;
}
@Override
public String getHttpMethod() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.payload, Claims.DPOP_RFC9449_HTTP_METHOD);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_HTM);
tmp = TokenUtilities.getFirstClaimAsString(this.payload, claimNames);
break;
case CUSTOM:
break;
}
return tmp;
}
@Override
public String getHttpUri() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.payload, Claims.DPOP_RFC9449_HTTP_URI);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_HTU);
tmp = TokenUtilities.getFirstClaimAsString(this.payload, claimNames);
break;
case CUSTOM:
break;
}
return tmp;
}
@Override
public Date getIssuedAt() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.payload, Claims.JSON_WEB_TOKEN_RFC_7519_ISSUED_AT);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_IAT);
tmp = TokenUtilities.getFirstClaimAsString(this.payload, claimNames);
break;
case CUSTOM:
break;
}
return tmp!=null ? TokenUtils.parseTimeInSecond(tmp) : null;
}
@Override
public String getAccessTokenHash() {
String tmp = null;
switch (this.tipologiaClaims) {
case RFC9449:
tmp = TokenUtilities.getClaimAsString(this.payload, Claims.DPOP_RFC9449_ACCESS_TOKEN_HASH);
break;
case MAPPING:
List<String> claimNames = TokenUtilities.getClaims(this.parserConfig, Costanti.DPOP_TOKEN_PARSER_ATH);
tmp = TokenUtilities.getFirstClaimAsString(this.payload, claimNames);
break;
case CUSTOM:
break;
}
return tmp;
}
}