All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.crypto.tink.jwt.RawJwt Maven / Gradle / Ivy

// Copyright 2020 Google LLC
//
// 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.google.crypto.tink.jwt;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
 * An unencoded and unsigned JSON Web Token (JWT).
 *
 * 

It contains all payload claims and a subset of the headers. It does not contain any headers * that depend on the key, such as "alg" or "kid", because these headers are chosen when the token * is signed and encoded, and should not be chosen by the user. This ensures that the key can be * changed without any changes to the user code. */ @Immutable public final class RawJwt { private static final long MAX_TIMESTAMP_VALUE = 253402300799L; // 31 Dec 9999, 23:59:59 GMT @SuppressWarnings("Immutable") // We do not mutate the payload. private final JsonObject payload; private final Optional typeHeader; private RawJwt(Builder builder) { if (!builder.payload.has(JwtNames.CLAIM_EXPIRATION) && !builder.withoutExpiration) { throw new IllegalArgumentException( "neither setExpiration() nor withoutExpiration() was called"); } if (builder.payload.has(JwtNames.CLAIM_EXPIRATION) && builder.withoutExpiration) { throw new IllegalArgumentException( "setExpiration() and withoutExpiration() must not be called together"); } this.typeHeader = builder.typeHeader; this.payload = builder.payload.deepCopy(); } private RawJwt(Optional typeHeader, String jsonPayload) throws JwtInvalidException { this.typeHeader = typeHeader; this.payload = JsonUtil.parseJson(jsonPayload); validateStringClaim(JwtNames.CLAIM_ISSUER); validateStringClaim(JwtNames.CLAIM_SUBJECT); validateStringClaim(JwtNames.CLAIM_JWT_ID); validateTimestampClaim(JwtNames.CLAIM_EXPIRATION); validateTimestampClaim(JwtNames.CLAIM_NOT_BEFORE); validateTimestampClaim(JwtNames.CLAIM_ISSUED_AT); validateAudienceClaim(); } private void validateStringClaim(String name) throws JwtInvalidException { if (!this.payload.has(name)) { return; } if (!this.payload.get(name).isJsonPrimitive() || !this.payload.get(name).getAsJsonPrimitive().isString()) { throw new JwtInvalidException("invalid JWT payload: claim " + name + " is not a string."); } } private void validateTimestampClaim(String name) throws JwtInvalidException { if (!this.payload.has(name)) { return; } if (!this.payload.get(name).isJsonPrimitive() || !this.payload.get(name).getAsJsonPrimitive().isNumber()) { throw new JwtInvalidException("invalid JWT payload: claim " + name + " is not a number."); } double timestamp = this.payload.get(name).getAsJsonPrimitive().getAsDouble(); if ((timestamp > MAX_TIMESTAMP_VALUE) || (timestamp < 0)) { throw new JwtInvalidException( "invalid JWT payload: claim " + name + " has an invalid timestamp"); } } private void validateAudienceClaim() throws JwtInvalidException { if (!this.payload.has(JwtNames.CLAIM_AUDIENCE)) { return; } if (this.payload.get(JwtNames.CLAIM_AUDIENCE).isJsonPrimitive() && this.payload.get(JwtNames.CLAIM_AUDIENCE).getAsJsonPrimitive().isString()) { return; } // aud is not a string, it must be an JsonArray of strings. // getAudiences makes sure that all entries are strings. List audiences = this.getAudiences(); if (audiences.size() < 1) { throw new JwtInvalidException( "invalid JWT payload: claim " + JwtNames.CLAIM_AUDIENCE + " is present but empty."); } } static RawJwt fromJsonPayload(Optional typeHeader, String jsonPayload) throws JwtInvalidException { return new RawJwt(typeHeader, jsonPayload); } /** * Returns a new RawJwt.Builder. */ public static Builder newBuilder() { return new Builder(); } /** Builder for RawJwt */ public static final class Builder { private Optional typeHeader; private boolean withoutExpiration; private final JsonObject payload; private Builder() { typeHeader = Optional.empty(); withoutExpiration = false; payload = new JsonObject(); } /** * Sets the Type Header Parameter. * *

When set, this value should be set to a shortended IANA MediaType, see * https://tools.ietf.org/html/rfc7519#section-5.1 and * https://tools.ietf.org/html/rfc8725#section-3.11 */ @CanIgnoreReturnValue public Builder setTypeHeader(String value) { typeHeader = Optional.of(value); return this; } /** * Sets the issuer claim that identifies the principal that issued the JWT. * *

https://tools.ietf.org/html/rfc7519#section-4.1.1 */ @CanIgnoreReturnValue public Builder setIssuer(String value) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException(); } payload.add(JwtNames.CLAIM_ISSUER, new JsonPrimitive(value)); return this; } /** * Sets the subject claim identifying the principal that is the subject of the JWT. * *

https://tools.ietf.org/html/rfc7519#section-4.1.2 */ @CanIgnoreReturnValue public Builder setSubject(String value) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException(); } payload.add(JwtNames.CLAIM_SUBJECT, new JsonPrimitive(value)); return this; } /** * Sets the audience that the JWT is intended for. * *

Sets the {@code aud} claim as a string. This method can't be used together with {@code * setAudiences} or {@code addAudience}. * *

https://tools.ietf.org/html/rfc7519#section-4.1.3 */ @CanIgnoreReturnValue public Builder setAudience(String value) { if (payload.has(JwtNames.CLAIM_AUDIENCE) && payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) { throw new IllegalArgumentException( "setAudience can't be used together with setAudiences or addAudience"); } if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException("invalid string"); } payload.add(JwtNames.CLAIM_AUDIENCE, new JsonPrimitive(value)); return this; } /** * Sets the audiences that the JWT is intended for. * *

Sets the {@code aud} claim as an array of strings. This method can't be used together with * {@code setAudience}. * *

https://tools.ietf.org/html/rfc7519#section-4.1.3 */ @CanIgnoreReturnValue public Builder setAudiences(List values) { if (payload.has(JwtNames.CLAIM_AUDIENCE) && !payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) { throw new IllegalArgumentException("setAudiences can't be used together with setAudience"); } if (values.isEmpty()) { throw new IllegalArgumentException("audiences must not be empty"); } JsonArray audiences = new JsonArray(); for (String value : values) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException("invalid string"); } audiences.add(value); } payload.add(JwtNames.CLAIM_AUDIENCE, audiences); return this; } /** * Adds an audience that the JWT is intended for. * *

The {@code aud} claim will always be encoded as an array of strings. This method can't be * used together with {@code setAudience}. * *

https://tools.ietf.org/html/rfc7519#section-4.1.3 */ @CanIgnoreReturnValue public Builder addAudience(String value) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException("invalid string"); } JsonArray audiences; if (payload.has(JwtNames.CLAIM_AUDIENCE)) { JsonElement aud = payload.get(JwtNames.CLAIM_AUDIENCE); if (!aud.isJsonArray()) { throw new IllegalArgumentException( "addAudience can't be used together with setAudience"); } audiences = aud.getAsJsonArray(); } else { audiences = new JsonArray(); } audiences.add(value); payload.add(JwtNames.CLAIM_AUDIENCE, audiences); return this; } /** * Sets the JWT ID claim that provides a unique identifier for the JWT. * *

https://tools.ietf.org/html/rfc7519#section-4.1.7 */ @CanIgnoreReturnValue public Builder setJwtId(String value) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException(); } payload.add(JwtNames.CLAIM_JWT_ID, new JsonPrimitive(value)); return this; } private void setTimestampClaim(String name, Instant value) { // We round the timestamp to a whole number. We always round down. long timestamp = value.getEpochSecond(); if ((timestamp > MAX_TIMESTAMP_VALUE) || (timestamp < 0)) { throw new IllegalArgumentException( "timestamp of claim " + name + " is out of range"); } payload.add(name, new JsonPrimitive(timestamp)); } /** * Sets the {@code exp} claim that identifies the instant on or after which the token MUST NOT * be accepted for processing. * *

This API requires {@link java.time.Instant} which is unavailable on Android until API * level 26. To use it on older Android devices, enable API desugaring as shown in * https://developer.android.com/studio/write/java8-support#library-desugaring. * *

https://tools.ietf.org/html/rfc7519#section-4.1.4 */ @CanIgnoreReturnValue public Builder setExpiration(Instant value) { setTimestampClaim(JwtNames.CLAIM_EXPIRATION, value); return this; } /** * Allow generating tokens without an expiration. * *

For most applications of JWT, an expiration date should be set. This function makes sure * that this is not forgotten, by requiring to user to explicitly state that no expiration * should be set. */ @CanIgnoreReturnValue public Builder withoutExpiration() { this.withoutExpiration = true; return this; } /** * Sets the {@code nbf} claim that identifies the instant before which the token MUST NOT be * accepted for processing. * *

This API requires {@link java.time.Instant} which is unavailable on Android until API * level 26. To use it on older Android devices, enable API desugaring as shown in * https://developer.android.com/studio/write/java8-support#library-desugaring. * *

https://tools.ietf.org/html/rfc7519#section-4.1.5 */ @CanIgnoreReturnValue public Builder setNotBefore(Instant value) { setTimestampClaim(JwtNames.CLAIM_NOT_BEFORE, value); return this; } /** * Sets the {@code iat} claim that identifies the instant at which the JWT was issued. * *

This API requires {@link java.time.Instant} which is unavailable on Android until API * level 26. To use it on older Android devices, enable API desugaring as shown in * https://developer.android.com/studio/write/java8-support#library-desugaring. * *

https://tools.ietf.org/html/rfc7519#section-4.1.6 */ @CanIgnoreReturnValue public Builder setIssuedAt(Instant value) { setTimestampClaim(JwtNames.CLAIM_ISSUED_AT, value); return this; } /** Adds a custom claim of type {@code boolean} to the JWT. */ @CanIgnoreReturnValue public Builder addBooleanClaim(String name, boolean value) { JwtNames.validate(name); payload.add(name, new JsonPrimitive(value)); return this; } /** Adds a custom claim of type {@code long} to the JWT. */ @CanIgnoreReturnValue public Builder addNumberClaim(String name, long value) { JwtNames.validate(name); payload.add(name, new JsonPrimitive(value)); return this; } /** Adds a custom claim of type {@code double} to the JWT. */ @CanIgnoreReturnValue public Builder addNumberClaim(String name, double value) { JwtNames.validate(name); payload.add(name, new JsonPrimitive(value)); return this; } /** Adds a custom claim of type {@code String} to the JWT. */ @CanIgnoreReturnValue public Builder addStringClaim(String name, String value) { if (!JsonUtil.isValidString(value)) { throw new IllegalArgumentException(); } JwtNames.validate(name); payload.add(name, new JsonPrimitive(value)); return this; } /** Adds a custom claim with value null. */ @CanIgnoreReturnValue public Builder addNullClaim(String name) { JwtNames.validate(name); payload.add(name, JsonNull.INSTANCE); return this; } /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */ @CanIgnoreReturnValue public Builder addJsonObjectClaim(String name, String encodedJsonObject) throws JwtInvalidException { JwtNames.validate(name); payload.add(name, JsonUtil.parseJson(encodedJsonObject)); return this; } /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */ @CanIgnoreReturnValue public Builder addJsonArrayClaim(String name, String encodedJsonArray) throws JwtInvalidException { JwtNames.validate(name); payload.add(name, JsonUtil.parseJsonArray(encodedJsonArray)); return this; } public RawJwt build() { return new RawJwt(this); } } public String getJsonPayload() { return payload.toString(); } boolean hasBooleanClaim(String name) { JwtNames.validate(name); return (payload.has(name) && payload.get(name).isJsonPrimitive() && payload.get(name).getAsJsonPrimitive().isBoolean()); } Boolean getBooleanClaim(String name) throws JwtInvalidException { JwtNames.validate(name); if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonPrimitive() || !payload.get(name).getAsJsonPrimitive().isBoolean()) { throw new JwtInvalidException("claim " + name + " is not a boolean"); } return payload.get(name).getAsBoolean(); } boolean hasNumberClaim(String name) { JwtNames.validate(name); return (payload.has(name) && payload.get(name).isJsonPrimitive() && payload.get(name).getAsJsonPrimitive().isNumber()); } Double getNumberClaim(String name) throws JwtInvalidException { JwtNames.validate(name); if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonPrimitive() || !payload.get(name).getAsJsonPrimitive().isNumber()) { throw new JwtInvalidException("claim " + name + " is not a number"); } return payload.get(name).getAsDouble(); } boolean hasStringClaim(String name) { JwtNames.validate(name); return (payload.has(name) && payload.get(name).isJsonPrimitive() && payload.get(name).getAsJsonPrimitive().isString()); } String getStringClaim(String name) throws JwtInvalidException { JwtNames.validate(name); return getStringClaimInternal(name); } private String getStringClaimInternal(String name) throws JwtInvalidException { if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonPrimitive() || !payload.get(name).getAsJsonPrimitive().isString()) { throw new JwtInvalidException("claim " + name + " is not a string"); } return payload.get(name).getAsString(); } boolean isNullClaim(String name) { JwtNames.validate(name); try { return JsonNull.INSTANCE.equals(payload.get(name)); } catch (JsonParseException ex) { return false; } } boolean hasJsonObjectClaim(String name) { JwtNames.validate(name); return (payload.has(name) && payload.get(name).isJsonObject()); } String getJsonObjectClaim(String name) throws JwtInvalidException { JwtNames.validate(name); if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonObject()) { throw new JwtInvalidException("claim " + name + " is not a JSON object"); } return payload.get(name).getAsJsonObject().toString(); } boolean hasJsonArrayClaim(String name) { JwtNames.validate(name); return (payload.has(name) && payload.get(name).isJsonArray()); } String getJsonArrayClaim(String name) throws JwtInvalidException { JwtNames.validate(name); if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonArray()) { throw new JwtInvalidException("claim " + name + " is not a JSON array"); } return payload.get(name).getAsJsonArray().toString(); } boolean hasTypeHeader() { return typeHeader.isPresent(); } String getTypeHeader() throws JwtInvalidException { if (!typeHeader.isPresent()) { throw new JwtInvalidException("type header is not set"); } return typeHeader.get(); } boolean hasIssuer() { return payload.has(JwtNames.CLAIM_ISSUER); } String getIssuer() throws JwtInvalidException { return getStringClaimInternal(JwtNames.CLAIM_ISSUER); } boolean hasSubject() { return payload.has(JwtNames.CLAIM_SUBJECT); } String getSubject() throws JwtInvalidException { return getStringClaimInternal(JwtNames.CLAIM_SUBJECT); } boolean hasJwtId() { return payload.has(JwtNames.CLAIM_JWT_ID); } String getJwtId() throws JwtInvalidException { return getStringClaimInternal(JwtNames.CLAIM_JWT_ID); } boolean hasAudiences() { // If an audience claim is present, it is always a JsonArray with length > 0. return payload.has(JwtNames.CLAIM_AUDIENCE); } List getAudiences() throws JwtInvalidException { if (!hasAudiences()) { throw new JwtInvalidException("claim aud does not exist"); } JsonElement aud = payload.get(JwtNames.CLAIM_AUDIENCE); if (aud.isJsonPrimitive()) { if (!aud.getAsJsonPrimitive().isString()) { throw new JwtInvalidException( String.format("invalid audience: got %s; want a string", aud)); } return Collections.unmodifiableList(Arrays.asList(aud.getAsString())); } if (!aud.isJsonArray()) { throw new JwtInvalidException("claim aud is not a string or a JSON array"); } JsonArray audiences = aud.getAsJsonArray(); List result = new ArrayList<>(audiences.size()); for (int i = 0; i < audiences.size(); i++) { if (!audiences.get(i).isJsonPrimitive() || !audiences.get(i).getAsJsonPrimitive().isString()) { throw new JwtInvalidException( String.format("invalid audience: got %s; want a string", audiences.get(i))); } String audience = audiences.get(i).getAsString(); result.add(audience); } return Collections.unmodifiableList(result); } private Instant getInstant(String name) throws JwtInvalidException { if (!payload.has(name)) { throw new JwtInvalidException("claim " + name + " does not exist"); } if (!payload.get(name).isJsonPrimitive() || !payload.get(name).getAsJsonPrimitive().isNumber()) { throw new JwtInvalidException("claim " + name + " is not a timestamp"); } try { double millis = payload.get(name).getAsJsonPrimitive().getAsDouble() * 1000; return Instant.ofEpochMilli((long) millis); } catch (NumberFormatException ex) { throw new JwtInvalidException("claim " + name + " is not a timestamp: " + ex); } } boolean hasExpiration() { return payload.has(JwtNames.CLAIM_EXPIRATION); } Instant getExpiration() throws JwtInvalidException { return getInstant(JwtNames.CLAIM_EXPIRATION); } boolean hasNotBefore() { return payload.has(JwtNames.CLAIM_NOT_BEFORE); } Instant getNotBefore() throws JwtInvalidException { return getInstant(JwtNames.CLAIM_NOT_BEFORE); } boolean hasIssuedAt() { return payload.has(JwtNames.CLAIM_ISSUED_AT); } Instant getIssuedAt() throws JwtInvalidException { return getInstant(JwtNames.CLAIM_ISSUED_AT); } /** Returns all custom claim names. */ Set customClaimNames() { HashSet names = new HashSet<>(); for (String name : this.payload.keySet()) { if (!JwtNames.isRegisteredName(name)) { names.add(name); } } return Collections.unmodifiableSet(names); } /** * Returns a brief description of a RawJwt object. The exact details of the representation are * unspecified and subject to change. */ @Override public String toString() { JsonObject header = new JsonObject(); if (typeHeader.isPresent()) { header.add("typ", new JsonPrimitive(typeHeader.get())); } return header + "." + payload; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy