Oathkeeper: how to implement the OpenID connect (OIDC) authorization code flow?

TL;DR: which configuration or stack components are we missing in between Oathkeeper and Keycloak?

In our setup, Keycloak is the identity provider for our Istio Kubernetes cluster and we were planning on using Ory Oathkeeper as follows:

  1. User makes a call to https://our-service/our/path (an Istio service in Kubernetes)
  2. Istio Envoy forwards this call to Oathkeeper for validation
  3. Oathkeeper validates this call using Keycloak
    3a. Token absent: Oathkeeper redirects to Keycloak, user logs in, user is redirected back to https://our-service/our/path, with a code
    3b. Token present and valid: Oathkeeper introspects the token, returns a 200, and Istio/Kubernetes do their thing
    3c. Token present and invalid: Oathkeeper introspects the token, returns a 403, and the user may not proceed

We’re somewhat stuck in step 3 here, and we’re wondering where the problem lies.
Any input on the missing/misused/misconfigured components in this stack would be very welcome!

Our attempts so far:

  • the OIDC ‘authorization code flow’ first yields a code (3a) that you need to then transform into a token with Keycloak (step 3b/3c check tokens, not codes). Are we correct in having found out that Oathkeeper does not do this flow for you? If so, but which other component (of your stack) might we place in between here?
  • the OIDC ‘implicit flow’ does yield a token right away, but both the JWT authenticator and the oauth2_introspection authenticator fail on the ‘access_token’ query parameter that we get this way (see below for log messages). The oauth2_introspection even gives a 500 (!). Would this be expected, or have we probably misconfigured something here?
    Unfortunately the logging is not much help here: there are no messages like “token url successfully reached”, “unauthorized due to wrong client_secret”, “unauthorized because the given token ‘access_token’ was indeed expired”, or anything along those lines.

JWT failure logs:

{
  "authentication_handler": "jwt",
  "error": "Access credentials are invalid",
  "granted": false,
  "http_host": "our-service",
  "http_method": "GET",
  "http_url": "http://our-service/?session_state=abc123&access_token=def456&token_type=bearer&expires_in=900",
  "http_user_agent": "",
  "level": "warning",
  "msg": "The authentication handler encountered an error",
  "reason_id": "authentication_handler_error",
  "rule_id": "test-our-service",
  "time": "2020-03-20T07:59:23Z"
}

oauth2_introspection failure logs:

{
  "authentication_handler": "oauth2_introspection",
  "error": "Introspection returned status code 401 but expected 200",
  "granted": false,
  "http_host": "our-service",
  "http_method": "GET",
  "http_url": "http://our-service/?session_state=abc123&access_token=def456&token_type=bearer&expires_in=900",
  "http_user_agent": "",
  "level": "warning",
  "msg": "The authentication handler encountered an error",
  "reason_id": "authentication_handler_error",
  "rule_id": "test-our-service",
  "time": "2020-03-20T08:28:39Z"
}
{
  "error": "Introspection returned status code 401 but expected 200",
  "granted": false,
  "http_host": "our-service",
  "http_method": "GET",
  "http_url": "http://our-service/?session_state=abc123&access_token=def456&token_type=bearer&expires_in=900",
  "http_user_agent": "",
  "level": "warning",
  "msg": "Access request denied",
  "time": "2020-03-20T08:28:39Z"
}
{
  "code": 500,
  "debug": "",
  "details": {},
  "error": "An internal server error occurred, please contact the system administrator",
  "level": "error",
  "msg": "An error occurred while handling a request",
  "reason": "",
  "request-id": "",
  "status": 500,
  "time": "2020-03-20T08:28:39Z",
  "trace": "Stack trace: \ngithub.com/ory/oathkeeper/pipeline/authn.(*AuthenticatorOAuth2Introspection).Authenticate\n\t/home/ory/pipeline/authn/authenticator_oauth2_introspection.go:97\ngithub.com/ory/oathkeeper/proxy.(*RequestHandler).HandleRequest\n\t/home/ory/proxy/request_handler.go:213\ngithub.com/ory/oathkeeper/api.(*DecisionHandler).decisions\n\t/home/ory/api/decision.go:108\ngithub.com/ory/oathkeeper/api.(*DecisionHandler).ServeHTTP\n\t/home/ory/api/decision.go:62\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/ory/x/reqlog.(*Middleware).ServeHTTP\n\t/go/pkg/mod/github.com/ory/[email protected]/reqlog/middleware.go:140\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/ory/x/metricsx.(*Service).ServeHTTP\n\t/go/pkg/mod/github.com/ory/[email protected]/metricsx/middleware.go:261\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/urfave/negroni.(*Negroni).ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:96\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2802\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1890\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1357",
  "writer": "JSON"
}