Title: ext_authz: Add decoded OAuth2 JWT token
Description:
Please add the decoded JWT token to the metadata that is send as input to the external authorization daemon like e.g. Open-Policy-Agent.
This feature does not need to be enabled by default but a switch would be nice.
It would help to test Open-Policy-Agent (OPA) Polices as the relevant input for the rules would then be plaintext and easy to fake to test several variants of a request.
Currently we have to extract attributes (called "claims") from the JWT using the functions provided by the OPA rule language. This has the drawback that the JWT gets verified which makes hand crafted input impossible and will eventually fail even for once valid requests when the lifetime of the tokens ends.
[optional Relevant Links:]
This is how Gluu does it, see "request_token_data" at https://gluu.org/docs/gg/plugin/gluu-opa-pep/
This issue has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in the next 7 days unless it is tagged "help wanted" or other activity occurs. Thank you for your contributions.
Please can you tag it at least with "help wanted" so that it does not get closed?
@lathspell sorry I don't really understand this. Could you clarify who decodes the JWT token? Then you want envoy (the ext_authz) to send decoded JWT to OPA service in a specific format?
And if you can draw a simple flow diagram, I'll be very thankful.
I try to explain:
Envoy has a nice feature (ext_authz) that allows it to ask a "policy server" like Open Policy Agent if a certain request should be permitted or denied. To do so, it sends the request header and some other meta information JSON encoded to the policy agent and awaits a true/false decision.
As you only need such policy agents in bigger setups, the chances that you also use a protocol like OAuth2 for authentication are pretty high.
The policy agent usually wants to know which user made the request. This information is stored in the OAuth2 JWT which is part of the request (either as cookie or authorization header). The policy agent must therefore decode the JWT and will, depending on the library it uses, automatically check the signature and time-to-life dates within the JWT.
Those checks are were not only already done by envoy, they also make writing tests for the policy agent hard because one has to encode the user information in a valid JWT for the tests.
My idea now was that envoy, which already verified and parsed the JWT, stores the middle part with the user information as part of the JSON that it passes to the policy agent.
Then the policy agent could act only on the plain information that it receives without having to bother with validating JWT fields and signatures. Also my policy unit tests would only be plain JSON that is easy to write and read.
Example:
service.auth.v2.AttributeContext.HttpRequest
{
"id": "...",
"method": "...",
"headers": "{...}",
"jwt": "{ ... }", <-------------- base64 decoded JWT
"path": "...",
...
}
The Gluu proxy already behaves that way as seen in the link I posted.
I think if you exercise jwt_authn before ext_authz, and you set the payload_in_metadata of that jwt_authn*, you can then set the metadata_context_namespaces of the ext_authz with:
metadata_context_namespaces:
- envoy.filters.http.jwt_authn
So ext_authz will send that jwt_authn payload to the authorization-server (in your case OPA server).
*For example, if payload_in_metadata is my_payload:
envoy.filters.http.jwt_authn:
my_payload:
iss: https://example.com
sub: [email protected]
aud: https://example.com
exp: 1501281058
The JWT contents are indeed copied to the metadata, thanks!
Although, it does look almost like a bug that a simple JSON structure like:
"demo-app-react": {
"roles": [
"users"
]
},
gets converted into this monster here (you may have to scroll to the right):
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "demo-app-react"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "Kind"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "StructValue"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "fields"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "roles"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "Kind"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "ListValue"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "values"
opa_1 | },
opa_1 | {
opa_1 | "type": "array",
opa_1 | "value": [
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "Kind"
opa_1 | },
opa_1 | {
opa_1 | "type": "object",
opa_1 | "value": [
opa_1 | [
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "StringValue"
opa_1 | },
opa_1 | {
opa_1 | "type": "string",
opa_1 | "value": "users"
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ]
opa_1 | ]
opa_1 | }
opa_1 | ],
Where do you put that json structure again? Can you paste out your setup here (config, etc)?
Envoy Metadata is a protobuf::Struct. So the a protobuf;:Struct is converted to JSON, it will look like that.
Well, JWT token is also forwarded to OPA by ext_authz, you can safely assume it has been verified by Envoy. You just need to get its payload by splitting it and base64 decode the payload.
@dio : This is my complete config:
static_resources:
listeners:
- address:
socket_address: { address: 0.0.0.0, port_value: 8081 }
# use_original_dst: true
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
access_log:
- name: envoy.file_access_log
typed_config:
"@type": type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog
path: /dev/stdout
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: service
http_filters:
- name: envoy.filters.http.jwt_authn
config:
providers:
keycloak_stage:
issuer: https://keycloak.REDACTED/auth/realms/internal
forward: true
payload_in_metadata: "jwt_payload"
local_jwks:
filename: /etc/envoy/keycloak_stage_internal.jwks
rules:
- match:
prefix: /
requires:
provider_name: keycloak_stage
- name: envoy.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.config.filter.http.ext_authz.v2.ExtAuthz
failure_mode_allow: false
metadata_context_namespaces:
- envoy.filters.http.jwt_authn
with_request_body:
max_request_bytes: 100000000
allow_partial_message: false
grpc_service:
google_grpc:
target_uri: opa:9191
stat_prefix: ext_authz
- name: envoy.router
typed_config: {}
clusters:
- name: keycloak_jwks_cluster
type: strict_dns
connect_timeout: 5s
load_assignment:
cluster_name: keycloak_jwks_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: keycloak.REDACTED
port_value: 443
- name: service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: docker.for.mac.localhost
port_value: 8080
admin:
access_log_path: "/dev/stdout"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
@qiwzhang : The current JSON representation of the data is not very helpful. The rest of the data that is transmitted to the Policy Agent looks like one would expect a JSON structure to look like e.g.:
opa_1 | "request": {
opa_1 | "http": {
opa_1 | "headers": {
opa_1 | ":authority": "envoy:8081",
opa_1 | ":method": "GET",
opa_1 | ":path": "/favicon.ico",
opa_1 | "accept": "image/webp,*/*",
opa_1 | "accept-encoding": "gzip",
...
Regarding your second comment: I concur that verification of the JWT should be only done in envoy and/or a Keycloak Gatekeeper before that. My concern is really only how easy it is to write unit tests for the policies of the external Policy Agent. Having to to encode the requestors username into a JWT structure and then base64 encode it makes it a bit harder. If it would just be a normal part of the input JSON like the request headers are, then you could have a bunch of plain JSON files in a folder and fire them as input against the policy agent to check which decision they would produce.
@lathspell How about using "payload_in_header", if you set that, jwt_authn will add a http header with value as the jwt payload (which is base64url encoded), ext_authz will send it to you, you only need to base64 decode it.
My envoy (1.13.1) isn't forwarding anything in ext_authz filter to the external HTTP service. The docs don't specify exactly how the jwt payload will be forwarded when the following config is used:
metadata_context_namespaces:
- envoy.filters.http.jwt_authn
(yes, i have the corresponding jwt_authn config option payload_in_metadata: client_data). what http verb is used when calling the ext_authz http server? how will the jwt payload be handed to the ext_authz http server?
The only thing that is currently forwarding any JWT information for me is to set forward: true in the jwt_authn filter. Then I see the http Authorization header being forwarded to the ext_authz http server.
Here's a full example that @shanesoh and I have been working on https://github.com/shanesoh/envoy-opa-compose
@lathspell can we close this? Seems like @qiwzhang's recommendation to put it on the header could be a solution. I'm closing this. Please let me know if you want to see a different approach to this. Thank you!
@jpermar sounds like a doc issue. Could you submit a new issue/PR for that? Thanks!