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

io.helidon.security.jwt.JwtUtil Maven / Gradle / Ivy

There is a newer version: 4.1.6
Show newest version
/*
 * Copyright (c) 2018, 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.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.crypto.Mac;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import jakarta.json.spi.JsonProvider;

/**
 * Utilities for JWT and JWK parsing.
 */
public final class JwtUtil {
    private static final DateTimeFormatter YEAR_FORMAT = DateTimeFormatter.ofPattern("yyyy");
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    // both are thread safe according to javadoc
    private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
    private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder();
    private static final Pattern LOCALE_PATTERN = Pattern.compile("(\\w+)_(\\w+)");

    // Avoid reloading JSON providers. See https://github.com/eclipse-ee4j/jsonp/issues/154
    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
    private static final JsonProvider JSON_PROVIDER = JsonProvider.provider();

    private JwtUtil() {
    }

    static String base64Url(byte[] bytesToEncode) {
        return URL_ENCODER.encodeToString(bytesToEncode);
    }

    /**
     * Extract a key value from json object that is base64-url encoded and convert it to big integer.
     *
     * @param json        JsonObject to read key from
     * @param key         key of the value we want to read
     * @param description description of the field for error handling
     * @return BigInteger value
     * @throws JwtException in case the key is not present or is of invalid content
     */
    public static BigInteger asBigInteger(JsonObject json, String key, String description) throws JwtException {
        return getBigInteger(json, key, description)
                .orElseThrow(() -> new JwtException("Key \"" + key + "\" is mandatory for " + description));
    }

    /**
     * Extract a key value from json object that is string.
     *
     * @param json        JsonObject to read key from
     * @param key         key of the value we want to read
     * @param description description of the field for error handling
     * @return String value
     * @throws JwtException in case the key is not present or is of invalid content
     */
    public static String asString(JsonObject json, String key, String description) throws JwtException {
        return getString(json, key)
                .orElseThrow(() -> new JwtException("Key \"" + key + "\" is mandatory for " + description));
    }

    /**
     * Extract a key value from json object that is base64-url encoded and convert it to big integer if present.
     *
     * @param json        JsonObject to read key from
     * @param key         key of the value we want to read
     * @param description description of the field for error handling
     * @return BigInteger value if present
     * @throws JwtException in case the key is of invalid content
     */
    public static Optional getBigInteger(JsonObject json, String key, String description) throws JwtException {
        return getByteArray(json, key, description)
                .map(byteValue -> {
                    // create BigInteger
                    try {
                        return new BigInteger(1, byteValue);
                    } catch (Exception e) {
                        throw new JwtException("Failed to get a big decimal for: " + description + ", from value of key " + key,
                                               e);
                    }
                });
    }

    /**
     * Extract a key value from json object that is a list of strings if present.
     *
     * @param json JsonObject to read key from
     * @param key  key of the value we want to read
     * @return List of String value if present
     * @throws JwtException in case the key is of invalid content
     */
    public static Optional> getStrings(JsonObject json, String key) throws JwtException {
        return Optional.ofNullable(json.getJsonArray(key))
                .map(it -> {
                    try {
                        return it.stream().map(it2 -> ((JsonString) it2).getString()).collect(Collectors.toList());
                    } catch (Exception e) {
                        throw new JwtException("Invalid value. Expecting a string array for key " + key);
                    }
                });
    }

    /**
     * Extract a key value from json object that is string if present.
     *
     * @param json JsonObject to read key from
     * @param key  key of the value we want to read
     * @return String value if present
     * @throws JwtException in case the key is of invalid content
     */
    public static Optional getString(JsonObject json, String key) throws JwtException {
        return Optional.ofNullable(json.getString(key, null));
    }

    /**
     * Extract a key value from json object that is a base64-url encoded byte array, if present.
     *
     * @param json        JsonObject to read key from
     * @param key         key of the value we want to read
     * @param description description of the field for error handling
     * @return byte array value if present
     * @throws JwtException in case the key is of invalid content or not base64 encoded
     */
    public static Optional getByteArray(JsonObject json, String key, String description) throws JwtException {
        return getString(json, key)
                .map(base64 -> {
                    try {
                        return URL_DECODER.decode(base64);
                    } catch (Exception e) {
                        throw new JwtException("Failed to decode base64 from json for: " + description + ", value: \""
                                                       + base64 + '"', e);
                    }
                });
    }

    /**
     * Extract a key value from json object that is a base64-url encoded byte array.
     *
     * @param json        JsonObject to read key from
     * @param key         key of the value we want to read
     * @param description description of the field for error handling
     * @return byte array value
     * @throws JwtException in case the key is not present, is of invalid content or not base64 encoded
     */
    public static byte[] asByteArray(JsonObject json, String key, String description) throws JwtException {
        return getByteArray(json, key, description)
                .orElseThrow(() -> new JwtException("Key \"" + key + "\" is mandatory for " + description));
    }

    /**
     * Create a key factory for algorithm.
     *
     * @param algorithm security algorithm (such as RSA, EC)
     * @return KeyFactory instance
     * @throws JwtException in case the algorithm is invalid
     */
    public static KeyFactory getKeyFactory(String algorithm) throws JwtException {
        try {
            return KeyFactory.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new JwtException("Failed to create key factory for algorithm: \"" + algorithm + "\"", e);
        }
    }

    /**
     * Create a signature for algorithm.
     *
     * @param signatureAlgorithm security signature algorithm (such as "SHA512withRSA")
     * @return Signature instance
     * @throws JwtException in case the algorithm is invalid or not supported by this JVM
     */
    public static Signature getSignature(String signatureAlgorithm) throws JwtException {
        try {
            return Signature.getInstance(signatureAlgorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new JwtException("Failed to get Signature instance for algorithm \"" + signatureAlgorithm + "\"");
        }
    }

    /**
     * Create a MAC for algorithm. Similar to signature for symmetric ciphers (such as "HmacSHA256").
     *
     * @param algorithm security MAC algorithm
     * @return Mac instance
     * @throws JwtException in case the algorithm is invalid or not supported by this JVM
     */
    public static Mac getMac(String algorithm) throws JwtException {
        try {
            return Mac.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new JwtException("Failed to get Mac instance for algorithm \"" + algorithm + "\"");
        }
    }

    /**
     * Transform a map of strings to objects to a map of string to JSON values.
     * Each object is checked for type and if supported, transformed to appropriate
     * JSON value.
     *
     * @param claims map to transform from
     * @return resulting map
     */
    public static Map transformToJson(Map claims) {
        Map result = new HashMap<>();

        claims.forEach((s, o) -> result.put(s, toJson(o)));

        return result;
    }

    /**
     * Create a {@link jakarta.json.JsonValue} from an object.
     * This will use correct types for known primitives, {@link io.helidon.security.jwt.JwtUtil.Address}
     * otherwise it uses String value.
     *
     * @param object object to create json value from
     * @return json value
     */
    public static JsonValue toJson(Object object) {
        if (object instanceof JsonValue) {
            return (JsonValue) object;
        }

        if (object instanceof String) {
            return JSON_PROVIDER.createValue((String) object);
        }
        if (object instanceof Integer) {
            return JSON_PROVIDER.createValue((Integer) object);
        }
        if (object instanceof Double) {
            return JSON_PROVIDER.createValue((Double) object);
        }
        if (object instanceof Long) {
            return JSON_PROVIDER.createValue((Long) object);
        }
        if (object instanceof BigDecimal) {
            return JSON_PROVIDER.createValue((BigDecimal) object);
        }
        if (object instanceof BigInteger) {
            return JSON_PROVIDER.createValue((BigInteger) object);
        }
        if (object instanceof Boolean) {
            return ((Boolean) object) ? JsonValue.TRUE : JsonValue.FALSE;
        }
        if (object instanceof Address) {
            return ((Address) object).getJson();
        }
        if (object instanceof Collection) {
            return JSON.createArrayBuilder((Collection) object).build();
        }
        return JSON_PROVIDER.createValue(String.valueOf(object));
    }

    private static Locale toLocale(String locale) {
        Matcher matcher = LOCALE_PATTERN.matcher(locale);
        Locale result;
        if (matcher.matches()) {
            result = Locale.of(matcher.group(1), matcher.group(2));
        } else {
            result = Locale.forLanguageTag(locale);
        }
        return result;
    }

    static Optional
toAddress(JsonObject json, String name) { return Optional.ofNullable(json.getJsonObject(name)) .map(Address::new); } static Optional> toScopes(JsonObject json) { if (json.get(Jwt.SCOPE) instanceof JsonArray) { return getStrings(json, Jwt.SCOPE); } else { return getString(json, Jwt.SCOPE) .map(it -> Arrays.asList(it.split(" "))); } } static Optional toTimeZone(JsonObject json, String name) { return getString(json, name) .map(ZoneId::of); } static Optional toDate(JsonObject json, String name) { return getString(json, name) .map(it -> { if (it.length() == 4) { return LocalDate.parse(it, YEAR_FORMAT); } else { return LocalDate.parse(it, DATE_FORMAT); } }); } static Optional toUri(JsonObject json, String name) { return getString(json, name) .map(URI::create); } static Optional toLocale(JsonObject json, String name) { return getString(json, name) .map(JwtUtil::toLocale); } static Optional toBoolean(JsonObject json, String name) { if (json.containsKey(name)) { return Optional.of(json.getBoolean(name)); } return Optional.empty(); } static Optional toInstant(JsonObject json, String name) { return Optional.ofNullable(json.getJsonNumber(name)) .map(JsonNumber::longValue) .map(Instant::ofEpochSecond); } static String toDate(LocalDate it) { return it.format(JwtUtil.DATE_FORMAT); } /** * Transform from json to object. * * @param jsonValue json value * @return object most correct for the type, or string value if not understood (e.g. json object) */ public static Object toObject(JsonValue jsonValue) { switch (jsonValue.getValueType()) { case ARRAY: return jsonValue.toString(); case OBJECT: return jsonValue.toString(); case STRING: return ((JsonString) jsonValue).getString(); case NUMBER: return ((JsonNumber) jsonValue).numberValue(); case TRUE: return true; case FALSE: return false; case NULL: return null; default: return jsonValue.toString(); } } /** * Address class representing the JSON object for address. */ public static class Address { // Full mailing address, formatted for display or use on a mailing label. This field MAY contain multiple lines, // separated by newlines. Newlines can be represented either as a carriage return/line feed pair ("\r\n") or as a // single line feed character ("\n"). private final Optional formatted; // street_address private final Optional streetAddress; // locality (City or locality) private final Optional locality; // State, province, prefecture, or region component. private final Optional region; // postal_code private final Optional postalCode; private final Optional country; /** * Create an address object from json representation. * * @param jsonObject object with expected keys */ public Address(JsonObject jsonObject) { this.formatted = getString(jsonObject, "formatted"); this.streetAddress = getString(jsonObject, "street_address"); this.locality = getString(jsonObject, "locality"); this.region = getString(jsonObject, "region"); this.postalCode = getString(jsonObject, "postal_code"); this.country = getString(jsonObject, "country"); } public Optional getFormatted() { return formatted; } public Optional getStreetAddress() { return streetAddress; } public Optional getLocality() { return locality; } public Optional getRegion() { return region; } public Optional getPostalCode() { return postalCode; } public Optional getCountry() { return country; } /** * Create a json representation of this address. * * @return Address as a Json object */ public JsonObject getJson() { JsonObjectBuilder objectBuilder = JSON.createObjectBuilder(); formatted.ifPresent(it -> objectBuilder.add("formatted", it)); streetAddress.ifPresent(it -> objectBuilder.add("street_address", it)); locality.ifPresent(it -> objectBuilder.add("locality", it)); region.ifPresent(it -> objectBuilder.add("region", it)); postalCode.ifPresent(it -> objectBuilder.add("postal_code", it)); country.ifPresent(it -> objectBuilder.add("country", it)); return objectBuilder.build(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy