0

Keyclock return 403 instead of 401 for unauthenticated requests when enabling policy enforcer config. When removing policy enforcer config it returns 401.

with this config am getting a 403 empty response.

keycloak:
  realm: ${KEYCLOAK_REALM}
  auth-server-url: ${KEYCLOAK_AUTH_SERVER_URL}
  ssl-required: external
  resource: ${KEYCLOAK_CLIENT_ID}
  credentials.secret: ${KEYCLOAK_CLIENT_SECRET}
  use-resource-role-mappings: true
  cors: true
  public-client: false
  bearer-only: true
  policy-enforcer-config:
    lazy-load-paths: true
    http-method-as-scope: true
    path-cache-config:
      max-entries: 1000
      lifespan: 1000
    paths:
      - name: Insecure Resource
        path: /
        enforcement-mode: DISABLED
      - name: Swagger UI
        path: /swagger-ui/*
        enforcement-mode: DISABLED
      - name: Swagger Resources
        path: /swagger-resources/*
        enforcement-mode: DISABLED
      - name: Swagger api Resources
        path: /api-docs
        enforcement-mode: DISABLED
  securityConstraints:
    - authRoles:
       - '*'
      securityCollections:
        - name: protected
          patterns:
            - '/v1/*'
            - '/intranet/*'

if I remove policy enforcer like this

keycloak:
  realm: ${KEYCLOAK_REALM}
  auth-server-url: ${KEYCLOAK_AUTH_SERVER_URL}
  ssl-required: external
  resource: ${KEYCLOAK_CLIENT_ID}
  credentials.secret: ${KEYCLOAK_CLIENT_SECRET}
  use-resource-role-mappings: true
  cors: true
  public-client: false
  bearer-only: true
#  policy-enforcer-config:
#    lazy-load-paths: true
#    http-method-as-scope: true
#    path-cache-config:
#      max-entries: 1000
#      lifespan: 1000
#    paths:
#      - name: Insecure Resource
#        path: /
#        enforcement-mode: DISABLED
#      - name: Swagger UI
#        path: /swagger-ui/*
#        enforcement-mode: DISABLED
#      - name: Swagger Resources
#        path: /swagger-resources/*
#        enforcement-mode: DISABLED
#      - name: Swagger api Resources
#        path: /api-docs
#        enforcement-mode: DISABLED
  securityConstraints:
    - authRoles:
       - '*'
      securityCollections:
        - name: protected
          patterns:
            - '/v1/*'
            - '/intranet/*'

returns 401

{
    "timestamp": "2021-10-05T11:25:33.116+0000",
    "status": 401,
    "error": "Unauthorized",
    "message": "No message available",
    "path": "/v1/approve-documents"
}

The policy enforcement is happening for all request even its not authenticated or not. How return 401 if token is invalid or missing.

complete code https://github.com/prajintst/keyclock-permissions

PRAJIN PRAKASH
  • 1,366
  • 1
  • 15
  • 31
  • Can you share stacktrace for both scenarios? for enabling policy enforcer I'm getting `There was an unexpected error (type=Internal Server Error, status=500). Failed to obtain policy enforcer` – Abhijeet Oct 08 '21 at 14:55
  • Policy Enforcement should happen to all the requests whether they are authenticated or unauthenticated. What do you want to achieve here? – Abhijeet Oct 08 '21 at 16:30
  • @Abhijeet I want to skip the policy enforcement if invalid token is provided. trace here https://github.com/prajintst/log – PRAJIN PRAKASH Oct 10 '21 at 15:29

4 Answers4

2

According to Keycloak architecture diagram, policy enforcement check happens before autherization/authentication. So you can not achieve expected output using policy enforcement. enter image description here

I suggest you to use policy evaluator/provider or use role based authorization to achieve this.

Abhijeet
  • 4,069
  • 1
  • 22
  • 38
1

See my answer from this post: Spring Security Plugin Should Respond with 401 instead of 403

It helped me to properly set Status codes

Michael Ushakov
  • 1,639
  • 1
  • 10
  • 18
0

A hacky but working solution.

In you spring boot project create a package:

org.keycloak.adapters.tomcat

In the above package create a JAVA file called AbstractKeycloakAuthenticatorValve.java with the following content:

package org.keycloak.adapters.tomcat;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.catalina.*;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.*;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.enums.TokenStore;
import org.springframework.http.MediaType;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
    public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
    private static final Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class);
    protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
    protected AdapterDeploymentContext deploymentContext;
    protected NodesRegistrationManagement nodesRegistrationManagement;

    public AbstractKeycloakAuthenticatorValve() {
    }

    public void lifecycleEvent(LifecycleEvent event) {
        if ("start".equals(event.getType())) {
            this.cache = false;
        } else if ("after_start".equals(event.getType())) {
            this.keycloakInit();
        } else if (event.getType() == "before_stop") {
            this.beforeStop();
        }

    }

    protected void logoutInternal(Request request) {
        KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
        if (ksc != null) {
            CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, (HttpServletResponse)null);
            KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
            if (ksc instanceof RefreshableKeycloakSecurityContext) {
                ((RefreshableKeycloakSecurityContext)ksc).logout(deployment);
            }

            AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
            tokenStore.logout();
            request.removeAttribute(KeycloakSecurityContext.class.getName());
        }

        request.setUserPrincipal((Principal)null);
    }

    protected void beforeStop() {
        if (this.nodesRegistrationManagement != null) {
            this.nodesRegistrationManagement.stop();
        }

    }

    public void keycloakInit() {
        String configResolverClass = this.context.getServletContext().getInitParameter("keycloak.config.resolver");
        if (configResolverClass != null) {
            try {
                KeycloakConfigResolver configResolver = (KeycloakConfigResolver)this.context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
                this.deploymentContext = new AdapterDeploymentContext(configResolver);
                log.debugv("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
            } catch (Exception var4) {
                log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, var4.getMessage());
                this.deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
            }
        } else {
            InputStream configInputStream = getConfigInputStream(this.context);
            KeycloakDeployment kd;
            if (configInputStream == null) {
                log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
                kd = new KeycloakDeployment();
            } else {
                kd = KeycloakDeploymentBuilder.build(configInputStream);
            }

            this.deploymentContext = new AdapterDeploymentContext(kd);
            log.debug("Keycloak is using a per-deployment configuration.");
        }

        this.context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), this.deploymentContext);
        AbstractAuthenticatedActionsValve actions = this.createAuthenticatedActionsValve(this.deploymentContext, this.getNext(), this.getContainer());
        this.setNext(actions);
        this.nodesRegistrationManagement = new NodesRegistrationManagement();
    }

    private static InputStream getJSONFromServletContext(ServletContext servletContext) {
        String json = servletContext.getInitParameter("org.keycloak.json.adapterConfig");
        if (json == null) {
            return null;
        } else {
            log.trace("**** using org.keycloak.json.adapterConfig");
            return new ByteArrayInputStream(json.getBytes());
        }
    }

    private static InputStream getConfigInputStream(Context context) {
        InputStream is = getJSONFromServletContext(context.getServletContext());
        if (is == null) {
            String path = context.getServletContext().getInitParameter("keycloak.config.file");
            if (path == null) {
                log.trace("**** using /WEB-INF/keycloak.json");
                is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
            } else {
                try {
                    is = new FileInputStream(path);
                } catch (FileNotFoundException var4) {
                    log.errorv("NOT FOUND {0}", path);
                    throw new RuntimeException(var4);
                }
            }
        }

        return (InputStream)is;
    }

    public void invoke(Request request, Response response) throws IOException, ServletException {
        CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
        Manager sessionManager = request.getContext().getManager();
        CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(this.userSessionManagement, sessionManager);
        PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, this.deploymentContext, facade);
        if (!handler.handleRequest()) {
            this.checkKeycloakSession(request, facade);
            super.invoke(request, response);
        }
    }

    protected abstract PrincipalFactory createPrincipalFactory();

    protected abstract boolean forwardToErrorPageInternal(Request var1, HttpServletResponse var2, Object var3) throws IOException;

    protected abstract AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext var1, Valve var2, Container var3);

    protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
        CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
        KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
        if (deployment != null && deployment.isConfigured()) {
            AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
            this.nodesRegistrationManagement.tryRegister(deployment);
            CatalinaRequestAuthenticator authenticator = this.createRequestAuthenticator(request, facade, deployment, tokenStore);
            AuthOutcome outcome = authenticator.authenticate();
            if (outcome == AuthOutcome.AUTHENTICATED) {
                return !facade.isEnded();
            } else {
                AuthChallenge challenge = authenticator.getChallenge();
                if (challenge != null) {
                    challenge.challenge(facade);
                }
                writeResponse(response, 401, "Failed to verify token");
                return false;
            }
        } else {
            facade.getResponse().sendError(401);
            return false;
        }
    }

    private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {

        // Generate your intended response here, e.g.:
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        Map<String, String> body = new HashMap<>();
        body.put("message", message);
        response.getOutputStream().println(objectMapper.writerWithDefaultPrettyPrinter()
                .writeValueAsString(body));
    }

    protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
        return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, this.createPrincipalFactory());
    }

    protected void checkKeycloakSession(Request request, HttpFacade facade) {
        KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
        AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
        tokenStore.checkCurrentToken();
    }

    public void keycloakSaveRequest(Request request) throws IOException {
        this.saveRequest(request, request.getSessionInternal(true));
    }

    public boolean keycloakRestoreRequest(Request request) {
        try {
            return this.restoreRequest(request, request.getSessionInternal());
        } catch (IOException var3) {
            throw new RuntimeException(var3);
        }
    }

    protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
        AdapterTokenStore store = (AdapterTokenStore)request.getNote("TOKEN_STORE_NOTE");
        if (store != null) {
            return store;
        } else {
            Object store1;
            if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
                store1 = this.createSessionTokenStore(request, resolvedDeployment);
            } else {
                store1 = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, this.createPrincipalFactory());
            }

            request.setNote("TOKEN_STORE_NOTE", store1);
            return (AdapterTokenStore)store1;
        }
    }

    private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
        AdapterTokenStore store = new CatalinaSessionTokenStore(request, resolvedDeployment, this.userSessionManagement, this.createPrincipalFactory(), this);
        return store;
    }
}

Basically we are modifying the file AbstractKeycloakAuthenticatorValve present in Keycloak to handle failed Authentication.

0

Create new class CustomContainer.java

import org.apache.catalina.Valve;
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
import java.util.Optional;

@Component
public class CustomContainer implements
        WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        Optional<Valve> valve = factory.getContextValves().stream().filter(currentValve -> KeycloakAuthenticatorValve.class.getClass().equals(currentValve.getClass())).findAny();
        valve.ifPresentOrElse(v -> factory.getContextValves().remove(v),()->{});
        factory.addContextValves(new CustomKeycloakAuthenticationValve());
    }
}

Create new class CustomKeycloakAuthenticationValve

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.catalina.connector.Request;
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static ai.g42hc.omicsbackend.constants.ResponseConstants.AUTHENTICATION_FAILED;

public class CustomKeycloakAuthenticationValve extends KeycloakAuthenticatorValve {
    
    public boolean authenticate(Request request, HttpServletResponse response) throws IOException {
        boolean val = authenticateInternal(request, response, request.getContext().getLoginConfig());
        if(Boolean.FALSE == val) {
            writeResponse(response, 401, AUTHENTICATION_FAILED);
        }
        return val;
    }

    private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        Map<String, String> map = new HashMap<>();
        map.put("message", message);
        response.getOutputStream().println(objectMapper.writeValueAsString(map));
    }
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 01 '23 at 05:10