From 581be5f6b8bc5576acfecfd0f293dc83792dadb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Mass=C3=A9?= Date: Wed, 14 Feb 2018 19:24:50 +0100 Subject: [PATCH] OpenID Connect policies: decode the token and explode it as HTTP headers --- examples/configuration/oidc.json | 53 ++++++ .../src/custom/policy/decode_oidc_token.lua | 22 +++ .../explode_oidc_token_as_http_headers.lua | 178 ++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 examples/configuration/oidc.json create mode 100644 gateway/src/custom/policy/decode_oidc_token.lua create mode 100644 gateway/src/custom/policy/explode_oidc_token_as_http_headers.lua diff --git a/examples/configuration/oidc.json b/examples/configuration/oidc.json new file mode 100644 index 000000000..f8c0f7bc5 --- /dev/null +++ b/examples/configuration/oidc.json @@ -0,0 +1,53 @@ +{ + "services": [ + { + "id": 123, + "backend_version": "oauth", + "backend_authentication_type": "service_token", + "backend_authentication_value": "456", + "proxy": { + "api_backend": "http://echo-api.3scale.net", + "hosts": [ + "localhost", + "127.0.0.1" + ], + "backend": { + "endpoint": "https://su1.3scale.net" + }, + "oidc_issuer_endpoint": "https://sso.app.openshift.test/auth/realms/3scale", + "authentication_method": "oidc", + "proxy_rules": [ + { + "http_method": "GET", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1 + }, + { + "http_method": "POST", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1 + }, + { + "http_method": "PUT", + "pattern": "/", + "metric_system_name": "hits", + "delta": 1 + } + ] + } + } + ], + "oidc": [ + { + "issuer": "https://sso.app.openshift.test/auth/realms/3scale", + "config": { + "public_key": "YOUR-REALM-PUBLIC-KEY", + "openid": { + "id_token_signing_alg_values_supported": [ "RS256" ] + } + } + } + ] +} diff --git a/gateway/src/custom/policy/decode_oidc_token.lua b/gateway/src/custom/policy/decode_oidc_token.lua new file mode 100644 index 000000000..b13ca5080 --- /dev/null +++ b/gateway/src/custom/policy/decode_oidc_token.lua @@ -0,0 +1,22 @@ +local policy = require('apicast.policy') +local _M = policy.new('Decode the OpenID Connect token and make it available in the context') + +local jwt = require("resty.jwt") + +function _M:access(context, host) + local credentials, err = ngx.ctx.service:extract_credentials() + local access_token = credentials.access_token + + -- Decode the JWT token but we do NOT validate it since it has already + -- been done by the 'apicast.policy.apicast' policy before. + local jwt_obj = jwt:load_jwt(access_token) + if not jwt_obj.valid then + ngx.log(ngx.WARN, "Could not decode JWT: ", jwt_obj.reason) + return + end + + local payload = jwt_obj.payload + context.jwt = payload +end + +return _M diff --git a/gateway/src/custom/policy/explode_oidc_token_as_http_headers.lua b/gateway/src/custom/policy/explode_oidc_token_as_http_headers.lua new file mode 100644 index 000000000..f75b132ae --- /dev/null +++ b/gateway/src/custom/policy/explode_oidc_token_as_http_headers.lua @@ -0,0 +1,178 @@ +local policy = require('apicast.policy') +local _M = policy.new('Explode the OpenID Connect JWT token into HTTP Headers') + +local jwt = require("resty.jwt") +local cjson = require "cjson" + +-- Valid OpenID Connect fields +local openid_fields = { + -- Issuer Identifier for the Issuer of the response. + -- The iss value is a case sensitive URL using the https scheme that contains + -- scheme, host, and optionally, port number and path components and no query + -- or fragment components. + iss = true, + + -- Subject Identifier. A locally unique and never reassigned identifier within + -- the Issuer for the End-User, which is intended to be consumed by the Client, + -- e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT + -- exceed 255 ASCII characters in length. The sub value is a case sensitive + -- string. + sub = true, + + -- Audience(s) that this ID Token is intended for. It MUST contain the OAuth + -- 2.0 client_id of the Relying Party as an audience value. It MAY also contain + -- identifiers for other audiences. In the general case, the aud value is an + -- array of case sensitive strings. In the common special case when there is + -- one audience, the aud value MAY be a single case sensitive string. + aud = true, + + -- Expiration time on or after which the ID Token MUST NOT be accepted for + -- processing. The processing of this parameter requires that the current + -- date/time MUST be before the expiration date/time listed in the value. + exp = true, + + -- Time at which the JWT was issued. Its value is a JSON number representing + -- the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the + -- date/time. + iat = true, + + -- Time when the End-User authentication occurred. Its value is a JSON number + -- representing the number of seconds from 1970-01-01T0:0:0Z as measured in + -- UTC until the date/time. When a max_age request is made or when auth_time + -- is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, + -- its inclusion is OPTIONAL. + auth_time = true, + + -- String value used to associate a Client session with an ID Token, and to + -- mitigate replay attacks. + nonce = true, + + -- Authentication Context Class Reference. String specifying an Authentication + -- Context Class Reference value that identifies the Authentication Context + -- Class that the authentication performed satisfied. + acr = true, + + -- Authentication Methods References. JSON array of strings that are + -- identifiers for authentication methods used in the authentication. + amr = true, + + -- Authorized party - the party to which the ID Token was issued. If present, + -- it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only + -- needed when the ID Token has a single audience value and that audience + -- is different than the authorized party. It MAY be included even when the + -- authorized party is the same as the sole audience. The azp value is a case + -- sensitive string containing a StringOrURI value. + azp = true, + + -- End-User's full name in displayable form including all name parts, possibly + -- including titles and suffixes, ordered according to the End-User's locale + -- and preferences. + name = true, + + -- Given name(s) or first name(s) of the End-User. Note that in some cultures, + -- people can have multiple given names; all can be present, with the names + -- being separated by space characters. + given_name = true, + + -- Surname(s) or last name(s) of the End-User. Note that in some cultures, + -- people can have multiple family names or no family name; all can be present, + -- with the names being separated by space characters. + family_name = true, + + -- Middle name(s) of the End-User. Note that in some cultures, people can have + -- multiple middle names; all can be present, with the names being separated + -- by space characters. Also note that in some cultures, middle names are not + -- used. + middle_name = true, + + -- Casual name of the End-User that may or may not be the same as the + -- given_name. For instance, a nickname value of Mike might be returned + -- alongside a given_name value of Michael. + nickname = true, + + -- Shorthand name by which the End-User wishes to be referred to at the RP, + -- such as janedoe or j.doe. This value MAY be any valid JSON string including + -- special characters such as @, /, or whitespace. + preferred_username = true, + + -- URL of the End-User's profile page. + profile = true, + + -- URL of the End-User's profile picture. + picture = true, + + -- URL of the End-User's Web page or blog. + website = true, + + -- End-User's preferred e-mail address. + email = true, + + -- True if the End-User's e-mail address has been verified; otherwise false. + email_verified = true, + + -- End-User's gender. Values defined by this specification are female and male. + -- Other values MAY be used when neither of the defined values are applicable. + gender = true, + + -- End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] + -- YYYY-MM-DD format. + birthdate = true, + + -- String from zoneinfo [zoneinfo] time zone database representing the + -- End-User's time zone. + zoneinfo = true, + + -- End-User's locale, represented as a BCP47 [RFC5646] language tag. + locale = true, + + -- End-User's preferred telephone number. + phone_number = true, + + -- True if the End-User's phone number has been verified; otherwise false. + phone_number_verified = true, + + -- End-User's preferred postal address. + address = true, + + -- Time the End-User's information was last updated. + updated_at = true +} + +local function init_config(config) + local res = {} + + for header, field in pairs(config) do + if openid_fields[field] then + res[header] = field + else + ngx.log(ngx.WARN, string.format("Skipping HTTP Header '%s' in config since its value '%s' is not recognised as a valid OpenID Connect field.", header, field)) + end + end + + return res +end + +local new = _M.new +function _M.new(config) + local self = new() + self.config = init_config(config or {}) + return self +end + +function _M:access(context, host) + local jwt = context.jwt + if not jwt then + ngx.log(ngx.WARN, "Could not find any JWT token in the context !") + return + end + + for header, field in pairs(self.config) do + if jwt[field] then + ngx.req.set_header(header, jwt[field]) + else + ngx.log(ngx.INFO, "Skipping HTTP Header ", header, " since the matching JWT field ", field, ' is not in the OpenID Connect token') + end + end +end + +return _M