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

io.helidon.security.providers.oidc.common.OidcCookieHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2021, 2023 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.providers.oidc.common;

import java.lang.System.Logger.Level;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

import io.helidon.http.SetCookie;

/**
 * Handler of cookies used in OIDC.
 */
public class OidcCookieHandler {
    private static final System.Logger LOGGER = System.getLogger(OidcCookieHandler.class.getName());

    private final String createCookieOptions;
    private final List> removeCookieUpdaters = new LinkedList<>();
    private final List> createCookieUpdaters = new LinkedList<>();
    private final String cookieName;
    private final String valuePrefix;
    private final Function encryptFunction;
    private final Function decryptFunction;

    private OidcCookieHandler(Builder builder) {
        this.cookieName = builder.cookieName;
        this.valuePrefix = cookieName + "=";

        // need to copy the values here, so we do not use future values of the builder
        String path = builder.path;
        boolean httpOnly = builder.httpOnly;
        SetCookie.SameSite sameSite = builder.sameSite;
        String domain = builder.domain;
        boolean secure = builder.secure;
        Long maxAge = builder.maxAge;

        removeCookieUpdaters.add(it -> it.path(path));
        if (httpOnly) {
            removeCookieUpdaters.add(it -> it.httpOnly(true));
        }
        if (sameSite != null) {
            removeCookieUpdaters.add(it -> it.sameSite(sameSite));
        }
        if (domain != null) {
            removeCookieUpdaters.add(it -> it.domain(domain));
        }
        if (secure) {
            removeCookieUpdaters.add(it -> it.secure(true));
        }
        // now we can share the updaters, from this point the two lists diverge
        createCookieUpdaters.addAll(removeCookieUpdaters);

        if (maxAge != null) {
            createCookieUpdaters.add(it -> it.maxAge(Duration.ofSeconds(maxAge)));
        }
        // set expires to 0 - this removes the cookie from browsers
        removeCookieUpdaters.add(it -> it.expires(Instant.ofEpochMilli(0)));

        String value = createCookieDirectValue("value").build().toString();
        int index = value.indexOf(';');
        if (index < 0) {
            this.createCookieOptions = "";
        } else {
            this.createCookieOptions = value.substring(index);
        }

        if (builder.encryptionEnabled) {
            var cookieEncryption = OidcEncryption.create("Cookie(" + cookieName + ")",
                                                         builder.encryptionName,
                                                         builder.encryptionPassword);
            this.encryptFunction = it -> cookieEncryption.encrypt(it.getBytes(StandardCharsets.UTF_8));
            this.decryptFunction = it -> new String(cookieEncryption.decrypt(it), StandardCharsets.UTF_8);
        } else {
            this.encryptFunction = Function.identity();
            this.decryptFunction = Function.identity();
        }

        if (LOGGER.isLoggable(Level.TRACE)) {
            LOGGER.log(Level.TRACE, () -> "OIDC Create cookie example: " + value);
            LOGGER.log(Level.TRACE, () -> "OIDC Remove cookie example: " + removeCookie().build());
        }
    }

    static Builder builder() {
        return new Builder();
    }

    /**
     * {@link io.helidon.http.SetCookie} builder to set a new cookie,
     * returns a future, as the value may need to be encrypted using a remote service.
     *
     * @param value value of the cookie
     * @return a new builder to configure set cookie configured from OIDC Config
     */
    public SetCookie.Builder createCookie(String value) {
        return createCookieDirectValue(encryptFunction.apply(value));
    }

    /**
     * Cookie name.
     *
     * @return name of the cookie to use
     */
    public String cookieName() {
        return cookieName;
    }

    /**
     * {@link io.helidon.http.SetCookie} builder to remove an existing cookie (such as during logout).
     *
     * @return a new builder to configure set cookie configured from OIDC Config with expiration set to epoch begin and
     *  empty value
     */
    public SetCookie.Builder removeCookie() {
        SetCookie.Builder builder = SetCookie.builder(cookieName, "");
        removeCookieUpdaters.forEach(it -> it.accept(builder));
        return builder;
    }

    /**
     * Locate cookie in a map of headers and return its value.
     * If the cookie is encrypted, decrypts the cookie value.
     *
     * @param headers headers to process
     * @return cookie value, or empty if the cookie could not be found
     */
    public Optional findCookie(Map> headers) {
        Objects.requireNonNull(headers);

        List cookies = headers.get("Cookie");
        if ((cookies == null) || cookies.isEmpty()) {
            return Optional.empty();
        }

        for (String cookie : cookies) {
            //a=b; c=d; e=f
            String[] cookieValues = cookie.split(";\\s?");
            for (String cookieValue : cookieValues) {
                String trimmed = cookieValue.trim();
                if (trimmed.startsWith(valuePrefix)) {
                    return Optional.of(decrypt(trimmed.substring(valuePrefix.length())));
                }
            }
        }

        return Optional.empty();
    }

    /**
     * Decrypt a cipher text into clear text (if encryption is enabled).
     *
     * @param cipherText cipher text to decrypt
     * @return secret
     */
    public String decrypt(String cipherText) {
        return decryptFunction.apply(cipherText);
    }

    String createCookieOptions() {
        return createCookieOptions;
    }

    String cookieValuePrefix() {
        return valuePrefix;
    }

    private SetCookie.Builder createCookieDirectValue(String value) {
        SetCookie.Builder builder = SetCookie.builder(cookieName, value);
        createCookieUpdaters.forEach(it -> it.accept(builder));
        return builder;
    }

    static class Builder implements io.helidon.common.Builder {
        static final String DEFAULT_PATH = "/";
        static final boolean DEFAULT_HTTP_ONLY = true;
        static final boolean DEFAULT_SECURE = false;
        static final SetCookie.SameSite DEFAULT_SAME_SITE = SetCookie.SameSite.LAX;

        private String path = DEFAULT_PATH;
        private boolean httpOnly = DEFAULT_HTTP_ONLY;
        private SetCookie.SameSite sameSite = DEFAULT_SAME_SITE;
        private String domain;
        private boolean secure = DEFAULT_SECURE;
        private Long maxAge;
        private String cookieName;
        private String encryptionName;
        private char[] encryptionPassword;
        private boolean encryptionEnabled;

        private Builder() {
        }

        @Override
        public OidcCookieHandler build() {
            return new OidcCookieHandler(this);
        }

        Builder path(String cookiePath) {
            this.path = cookiePath;
            return this;
        }

        Builder httpOnly(boolean cookieHttpOnly) {
            this.httpOnly = cookieHttpOnly;
            return this;
        }

        Builder sameSite(SetCookie.SameSite cookieSameSite) {
            this.sameSite = cookieSameSite;
            return this;
        }

        Builder domain(String cookieDomain) {
            this.domain = cookieDomain;
            return this;
        }

        Builder secure(boolean secure) {
            this.secure = secure;
            return this;
        }

        Builder maxAge(Long maxAge) {
            this.maxAge = maxAge;
            return this;
        }

        Builder cookieName(String cookieName) {
            this.cookieName = cookieName;
            return this;
        }

        public Builder encryptionName(String encryptionName) {
            this.encryptionName = encryptionName;
            return this;
        }

        public Builder encryptionPassword(char[] encryptionPassword) {
            this.encryptionPassword = encryptionPassword;
            return this;
        }

        public Builder encryptionEnabled(Boolean encryptionEnabled) {
            this.encryptionEnabled = encryptionEnabled;
            return this;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy