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

net.optionfactory.keycloak.cookies.SiteLocalAsSecureCookieProvider Maven / Gradle / Ivy

There is a newer version: 6.18
Show newest version
package net.optionfactory.keycloak.cookies;

import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.NewCookie;
import java.net.Inet4Address;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.cookie.CookieMaxAge;
import org.keycloak.cookie.CookieProvider;
import org.keycloak.cookie.CookieProviderFactory;
import org.keycloak.cookie.CookieType;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.utils.SecureContextResolver;

/**
 * must be used together with
 * 
 * --unsafely-treat-insecure-origin-as-secure=http://172.18.xxx.xxx:8080/
 * 
 * on chrome:
 * 
 * chrome://flags/#unsafely-treat-insecure-origin-as-secure 
 * 
 * 
 *
 * @author rferranti
 */
public class SiteLocalAsSecureCookieProvider implements CookieProvider {

    private static final Logger logger = Logger.getLogger(SiteLocalAsSecureCookieProvider.class);

    private final KeycloakSession session;

    private final CookiePathResolver pathResolver;

    private final boolean secure;
    private final boolean allowed;

    private final Map cookies;

    public SiteLocalAsSecureCookieProvider(KeycloakSession session) {
        KeycloakContext context = session.getContext();
        this.session = session;
        this.cookies = context.getRequestHeaders().getCookies();
        this.pathResolver = new CookiePathResolver(context);
        this.secure = SecureContextResolver.isSecureContext(session);
        this.allowed = isAllowed(context.getUri().getRequestUri());

        if (logger.isTraceEnabled()) {
            logger.tracef("Received cookies: %s, path: %s", String.join(", ", this.cookies.keySet()), context.getUri().getRequestUri().getRawPath());
        }

        if (!secure && !allowed) {
            logger.warnf("Non-secure context detected; cookies are not secured, and will not be available in cross-origin POST requests");
        }

        expireOldUnusedCookies();
    }

    @Override
    public void set(CookieType cookieType, String value) {
        if (cookieType.getDefaultMaxAge() == null) {
            throw new IllegalArgumentException(cookieType + " has no default max-age");
        }

        set(cookieType, value, cookieType.getDefaultMaxAge());
    }

    @Override
    public void set(CookieType cookieType, String value, int maxAge) {
        String name = cookieType.getName();
        NewCookie.SameSite sameSite = cookieType.getScope().getSameSite();
        if (NewCookie.SameSite.NONE.equals(sameSite) && !secure && !allowed) {
            sameSite = NewCookie.SameSite.LAX;
        }

        String path = pathResolver.resolvePath(cookieType);
        boolean httpOnly = cookieType.getScope().isHttpOnly();

        NewCookie newCookie = new NewCookie.Builder(name)
                .version(1)
                .value(value)
                .path(path)
                .maxAge(maxAge)
                .secure(secure || allowed)
                .httpOnly(httpOnly)
                .sameSite(sameSite)
                .build();

        session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);

        logger.tracef("Setting cookie: name: %s, path: %s, same-site: %s, secure: %s, http-only: %s, max-age: %d", name, path, sameSite, secure, httpOnly, maxAge);
    }

    @Override
    public String get(CookieType cookieType) {
        Cookie cookie = cookies.get(cookieType.getName());
        return cookie != null ? cookie.getValue() : null;
    }

    @Override
    public void expire(CookieType cookieType) {
        String cookieName = cookieType.getName();
        Cookie cookie = cookies.get(cookieName);
        if (cookie != null) {
            String path = pathResolver.resolvePath(cookieType);
            NewCookie newCookie = new NewCookie.Builder(cookieName)
                    .version(1)
                    .path(path)
                    .maxAge(CookieMaxAge.EXPIRED)
                    .build();

            session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);

            logger.tracef("Expiring cookie: name: %s, path: %s", cookie.getName(), path);
        }
    }

    private void expireOldUnusedCookies() {
        for (CookieType cookieType : CookieType.OLD_UNUSED_COOKIES) {
            expire(cookieType);
        }
    }

    @Override
    public void close() {
    }

    private boolean isAllowed(URI uri) {
        String host = uri.getHost();
        if (host == null) {
            return false;
        }
        try {
            return Inet4Address.getByName(host).isSiteLocalAddress();
        } catch (UnknownHostException ex) {
            return false;
        }
    }

    /**
     * duplicated from keycloak for visibility
     */
    public static class CookiePathResolver {

        private final KeycloakContext context;
        private String realmPath;

        private String requestPath;

        CookiePathResolver(KeycloakContext context) {
            this.context = context;
        }

        String resolvePath(CookieType cookieType) {
            switch (cookieType.getPath()) {
                case REALM:
                    if (realmPath == null) {
                        realmPath = RealmsResource.realmBaseUrl(context.getUri()).path("/").build(context.getRealm().getName()).getRawPath();
                    }
                    return realmPath;
                case REQUEST:
                    if (requestPath == null) {
                        requestPath = context.getUri().getRequestUri().getRawPath();
                    }
                    return requestPath;
                default:
                    throw new IllegalArgumentException("Unsupported enum value " + cookieType.getPath().name());
            }
        }

    }

    public static class Factory implements CookieProviderFactory {

        @Override
        public CookieProvider create(KeycloakSession session) {
            return new SiteLocalAsSecureCookieProvider(session);
        }

        @Override
        public void init(Config.Scope config) {
        }

        @Override
        public void postInit(KeycloakSessionFactory factory) {
        }

        @Override
        public void close() {
        }

        @Override
        public String getId() {
            return "site-local-as-secure";
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy