io.helidon.security.jwt.JwtHeaders Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of helidon-security-jwt Show documentation
Show all versions of helidon-security-jwt Show documentation
Implementation of JWT and JWK to be used in other modules.
/*
* Copyright (c) 2021, 2024 Oracle and/or its affiliates.
*
* 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 io.helidon.security.jwt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.helidon.common.Errors;
import io.helidon.common.GenericType;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
/**
* Representation of the header section of a JWT.
* This can be used to partially parse a token to understand what kind of
* processing should be done further, whether {@link io.helidon.security.jwt.SignedJwt}
* or {@link io.helidon.security.jwt.EncryptedJwt}.
*
* @see #parseToken(String)
*/
public class JwtHeaders extends JwtClaims {
static final String ALGORITHM = "alg";
static final String ENCRYPTION = "enc";
static final String TYPE = "typ";
static final String CONTENT_TYPE = "cty";
static final String KEY_ID = "kid";
static final String JWK_SET_URL = "jku";
static final String JSON_WEB_KEY = "jwk";
static final String X509_URL = "x5u";
static final String X509_CERT_CHAIN = "x5c";
static final String X509_CERT_SHA1_THUMB = "x5t";
static final String X509_CERT_SHA256_THUMB = "x5t#S256";
static final String CRITICAL = "crit";
static final String COMPRESSION_ALGORITHM = "zip";
static final String AGREEMENT_PARTYUINFO = "apu";
static final String AGREEMENT_PARTYVINFO = "apv";
static final String EPHEMERAL_PUBLIC_KEY = "epk";
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
private final Optional algorithm;
private final Optional encryption;
private final Optional contentType;
private final Optional keyId;
private final Optional type;
private final Optional> critical;
// intended for replication into header in encrypted JWT
private final Optional subject;
private final Optional issuer;
private final Optional> audience;
private final Map headerClaims;
private JwtHeaders(Builder builder) {
this.algorithm = Optional.ofNullable(builder.algorithm);
this.encryption = Optional.ofNullable(builder.encryption);
this.contentType = Optional.ofNullable(builder.contentType);
this.keyId = Optional.ofNullable(builder.keyId);
this.type = Optional.ofNullable(builder.type);
this.critical = Optional.ofNullable(builder.critical);
this.subject = Optional.ofNullable(builder.subject);
this.issuer = Optional.ofNullable(builder.issuer);
this.audience = Optional.ofNullable(builder.audience);
this.headerClaims = new LinkedHashMap<>(builder.claims);
}
/**
* Create a new builder for header claims.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Parse a token to retrieve the JWT header.
* This method only cares about the first section of the token, and ignores the rest (even if not valid).
* Text before the first dot is considered to be base64 value of the header JSON.
*
* @param token token, expected to be JWT (encrypted or signed)
* @return header parsed from the token
* @throws io.helidon.security.jwt.JwtException in case the token is not valid
*/
public static JwtHeaders parseToken(String token) {
Errors.Collector collector = Errors.collector();
int firstDot = token.indexOf('.');
if (firstDot < 0) {
throw new JwtException("Not a JWT token: " + token);
}
String headerBase64 = token.substring(0, firstDot);
JwtHeaders jwtHeader = parseBase64(headerBase64, collector);
collector.collect().checkValid();
return jwtHeader;
}
static JwtHeaders parseBase64(String base64, Errors.Collector collector) {
String headerJsonString = decode(base64, collector, "JWT header");
// if failed, do not continue
if (collector.hasFatal()) {
return null;
}
// this is either a signed JWT or encrypted JWT, first section is always
// base64 encoded header
JsonObject headerJson = parseJson(headerJsonString, collector, base64, "JWT header");
// if failed, do not continue
if (collector.hasFatal()) {
return null;
}
Builder builder = builder();
builder.fromJson(headerJson);
collector.collect().checkValid();
return builder.build();
}
/**
* Create a JSON header object.
*
* @return JsonObject for header
*/
public JsonObject headerJson() {
JsonObjectBuilder objectBuilder = JSON.createObjectBuilder();
headerClaims.forEach(objectBuilder::add);
return objectBuilder.build();
}
/**
* Get a claim by its name from header.
*
* @param claim name of a claim
* @return claim value if present
*/
public Optional headerClaim(String claim) {
return Optional.ofNullable(headerClaims.get(claim));
}
/**
* Algorithm claim.
*
* @return algorithm or empty if claim is not defined
*/
public Optional algorithm() {
return algorithm;
}
/**
* Encryption algorithm claim.
*
* @return algorithm or empty if not encrypted
*/
public Optional encryption() {
return encryption;
}
/**
* Content type claim.
*
* @return content type or empty if claim is not defined
*/
public Optional contentType() {
return contentType;
}
/**
* Key id claim.
*
* @return key id or empty if claim is not defined
*/
public Optional keyId() {
return keyId;
}
/**
* Type claim.
*
* @return type or empty if claim is not defined
*/
public Optional type() {
return type;
}
/**
* Subject claim.
*
* @return subject or empty if claim is not defined
*/
public Optional subject() {
return subject;
}
/**
* Issuer claim.
*
* @return Issuer or empty if claim is not defined
*/
public Optional issuer() {
return issuer;
}
/**
* Audience claim.
*
* @return audience or empty optional if claim is not defined; list would be empty if the audience claim is defined as
* an empty array
*/
public Optional> audience() {
return audience;
}
/**
* Critical claim.
*
* @return critical claims or empty optional if not defined; list would be empty if the critical claim is defined as
* an empty array
*/
public Optional> critical() {
return critical;
}
/**
* Return map of all header claims.
*
* @return header claims
*/
public Map headerClaims() {
return Collections.unmodifiableMap(headerClaims);
}
/**
* Fluent API builder to create JWT Header.
*/
public static class Builder implements io.helidon.common.Builder {
private static final GenericType> STRING_LIST_TYPE = new GenericType<>() { };
private static final Map> KNOWN_HEADER_CLAIMS;
private static final KnownField TYPE_FIELD = KnownField.create(TYPE, Builder::type);
private static final KnownField ALG_FIELD = KnownField.create(ALGORITHM, Builder::algorithm);
private static final KnownField ENC_FIELD = KnownField.create(ENCRYPTION, Builder::encryption);
private static final KnownField CTY_FIELD = KnownField.create(CONTENT_TYPE, Builder::contentType);
private static final KnownField KID_FIELD = KnownField.create(KEY_ID, Builder::keyId);
private static final KnownField SUB_FIELD = KnownField.create(Jwt.SUBJECT, Builder::headerSubject);
private static final KnownField ISS_FIELD = KnownField.create("iss", Builder::headerIssuer);
private static final KnownField> CRIT_FIELD = new KnownField<>(CRITICAL,
STRING_LIST_TYPE,
Builder::headerCritical,
Builder::jsonToStringList);
private static final KnownField> AUD_FIELD = new KnownField<>("aud",
STRING_LIST_TYPE,
Builder::headerAudience,
Builder::jsonToStringList);
static {
Map> map = new HashMap<>();
addKnownField(map, TYPE_FIELD);
addKnownField(map, ALG_FIELD);
addKnownField(map, ENC_FIELD);
addKnownField(map, CTY_FIELD);
addKnownField(map, KID_FIELD);
addKnownField(map, SUB_FIELD);
addKnownField(map, ISS_FIELD);
addKnownField(map, AUD_FIELD);
addKnownField(map, CRIT_FIELD);
KNOWN_HEADER_CLAIMS = Map.copyOf(map);
}
private final Map claims = new LinkedHashMap<>();
private String type;
private String algorithm;
private String encryption;
private String contentType;
private String keyId;
private String subject;
private String issuer;
private List audience;
private List critical;
private Builder() {
}
@Override
public JwtHeaders build() {
if (audience != null) {
// this may be changing throughout the build
AUD_FIELD.set(claims, audience);
}
if (critical != null) {
CRIT_FIELD.set(claims, critical);
}
return new JwtHeaders(this);
}
/**
* Add a header claim.
*
* @param claim name of the claim
* @param value claim value, must be of expected type
* @return updated builder
* @throws java.lang.IllegalArgumentException if a known header (such as {@code iss}, {@code aud}) is set to a non-string
* type
*/
public Builder addHeaderClaim(String claim, Object value) {
setFromGeneric(claim, value);
this.claims.put(claim, JwtUtil.toJson(value));
return this;
}
/**
* The "alg" claim is used to define the signature algorithm.
* Note that this algorithm should be the same as is supported by
* the JWK used to sign (or verify) the JWT.
*
* @param algorithm algorithm to use, {@link io.helidon.security.jwt.jwk.Jwk#ALG_NONE} for none
* @return updated builder instance
*/
public Builder algorithm(String algorithm) {
ALG_FIELD.set(claims, algorithm);
this.algorithm = algorithm;
return this;
}
/**
* Encryption algorithm to use.
*
* @param encryption encryption to use
* @return updated builder
*/
public Builder encryption(String encryption) {
ENC_FIELD.set(claims, encryption);
this.encryption = encryption;
return this;
}
/**
* This header claim should only be used when nesting or encrypting JWT.
* See RFC 7519, section 5.2.
*
* @param contentType content type to use, use "JWT" if nested
* @return updated builder instance
*/
public Builder contentType(String contentType) {
CTY_FIELD.set(claims, contentType);
this.contentType = contentType;
return this;
}
/**
* Key id to be used to sign/verify this JWT.
*
* @param keyId key id (pointing to a JWK)
* @return updated builder instance
*/
public Builder keyId(String keyId) {
KID_FIELD.set(claims, keyId);
this.keyId = keyId;
return this;
}
/**
* Type of this JWT.
*
* @param type type definition (JWT, JWE)
* @return updated builder instance
*/
public Builder type(String type) {
TYPE_FIELD.set(claims, type);
this.type = type;
return this;
}
/**
* Subject defines the principal this JWT was issued for (e.g. user id).
* This configures subject in header claims (usually it is part of payload).
*
* See RFC 7519, section 4.1.2.
*
* @param subject subject of this JWt
* @return updated builder instance
*/
public Builder headerSubject(String subject) {
SUB_FIELD.set(claims, subject);
this.subject = subject;
return this;
}
/**
* The issuer claim identifies the principal that issued the JWT.
* This configures issuer in header claims (usually it is part of payload).
*
* See RFC 7519, section 4.1.1.
*
* @param issuer issuer name or URL
* @return updated builder instance
*/
public Builder headerIssuer(String issuer) {
ISS_FIELD.set(claims, issuer);
this.issuer = issuer;
return this;
}
/**
* The critical claim is used to indicate that certain claims are critical and must be understood (optional).
* If a recipient does not understand or support any of the critical claims, it must reject the token.
* Multiple critical claims may be added.
* This configures critical claims in header claims.
*
* See RFC 7519, section 4.1.1.
*
* @param critical required critical claim to understand
* @return updated builder instance
*/
public Builder addHeaderCritical(String critical) {
if (this.critical == null) {
this.critical = new LinkedList<>();
}
this.critical.add(critical);
return this;
}
/**
* Audience identifies the expected recipients of this JWT (optional).
* Multiple audience may be added.
* This configures audience in header claims, usually this is defined in payload.
*
* See RFC 7515, section 4.1.11.
*
* @param audience audience of this JWT
* @return updated builder instance
* @see #headerAudience(java.util.List)
*/
public Builder addHeaderAudience(String audience) {
if (this.audience == null) {
this.audience = new LinkedList<>();
}
this.audience.add(audience);
return this;
}
/**
* Audience identifies the expected recipients of this JWT (optional).
* Replaces existing configured audiences.
* This configures audience in header claims, usually this is defined in payload.
*
* See RFC 7519, section 4.1.3.
*
* @param audience audience of this JWT
* @return updated builder instance
*/
public Builder headerAudience(List audience) {
this.audience = new LinkedList<>(audience);
return this;
}
/**
* The critical claim is used to indicate that certain claims are critical and must be understood (optional).
* If a recipient does not understand or support any of the critical claims, it must reject the token.
* Replaces existing configured critical claims.
* This configures critical claims in header claims.
*
* See RFC 7515, section 4.1.11.
*
* @param critical required critical claims to understand
* @return updated builder instance
*/
public Builder headerCritical(List critical) {
this.critical = new ArrayList<>(critical);
return this;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void setFromGeneric(String claim, Object value) {
KnownField knownField = KNOWN_HEADER_CLAIMS.get(claim);
if (knownField == null) {
return;
}
if (knownField.supports(value)) {
knownField.valueConsumer().accept(this, value);
} else {
throw new IllegalArgumentException("Claim \"" + claim
+ " is expected to be of type " + knownField.type
+ ", but is " + value.getClass().getName());
}
}
private static List jsonToStringList(JsonValue jsonValue) {
if (jsonValue instanceof JsonString) {
return List.of(((JsonString) jsonValue).getString());
}
if (jsonValue instanceof JsonArray) {
return ((JsonArray) jsonValue)
.stream()
.map(KnownField::jsonToString)
.collect(Collectors.toList());
}
throw new JwtException("Json value should have been a String or an array of Strings, but is " + jsonValue);
}
private static void addKnownField(Map> map, KnownField field) {
map.put(field.name, field);
}
void fromJson(JsonObject headerJson) {
headerJson.forEach((claim, value) -> {
KnownField> knownField = KNOWN_HEADER_CLAIMS.get(claim);
if (knownField == null) {
addHeaderClaim(claim, value);
} else {
knownField.set(this, value);
}
});
}
}
private static final class KnownField {
private final String name;
private final GenericType type;
private final BiConsumer valueConsumer;
private final Function fromJson;
private KnownField(String name,
GenericType type,
BiConsumer valueConsumer,
Function fromJson) {
this.name = name;
this.type = type;
this.valueConsumer = valueConsumer;
this.fromJson = fromJson;
}
static KnownField create(String name, BiConsumer valueConsumer) {
return new KnownField<>(name, GenericType.STRING, valueConsumer, KnownField::jsonToString);
}
private static String jsonToString(JsonValue jsonValue) {
if (jsonValue instanceof JsonString) {
return ((JsonString) jsonValue).getString();
}
throw new JwtException("Json value should have been a String, but is " + jsonValue);
}
BiConsumer valueConsumer() {
return valueConsumer;
}
void set(Map claims, T value) {
claims.put(name, JwtUtil.toJson(value));
}
void set(Builder builder, JsonValue value) {
valueConsumer.accept(builder, fromJson.apply(value));
}
public boolean supports(Object value) {
return type.rawType().isAssignableFrom(value.getClass());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy