zmartzone / lua-resty-openidc

OpenID Connect Relying Party and OAuth 2.0 Resource Server implementation in Lua for NGINX / OpenResty
Apache License 2.0
963 stars 247 forks source link

Multiple public_keys? #512

Open renedupont opened 5 months ago

renedupont commented 5 months ago

Hello, I am using client credential grant flow and want to verify incoming tokens completely without connecting to my provider (Microsoft Entra, formerly known as Azure AD). Therefore I went to the JWKS URI and saw that they have two JWK entries with different x5c values and unfortunately Entra provides sometimes tokens signed with one OR the other key.

In lua-resty-openidc opts I can only set one public_key as far as I know. I found out that an access token has an x5t and kid field that can be used to identify the right public_key (e.g. by a x5t to x5c mapping). I am using bearer_jwt_verify, but to set opts.public_key to the right one, I would need to get the access token before calling bearer_jwt_verify, which would be rather unfortunate since this method does already do that here: https://github.com/zmartzone/lua-resty-openidc/blob/v1.7.6/lib/resty/openidc.lua#L1860 and I'd like to avoid doing it twice, especially as openidc_get_bearer_access_token is a local function and hence I wouldn't be able to use it and would need to copy it into my own code.

Currently I am doing this, but this is O(n) and I want to get back to O(1).

local openidc = require("resty.openidc")

local function read_file(path)
  local file = io.open(path, "r") -- r read mode
  if not file then return nil end
  local content = file:read "*a" -- *a or *all reads the whole file
  file:close()
  return content
end

local public_keys = {
  read_file("/etc/nginx/config/jwkX5c.crt"),
  read_file("/etc/nginx/config/jwkX5c2.crt"),
}

local entra_id_opts = {
  token_signing_alg_values_expected = { "RS256" },
  -- Available are: id_token, enc_id_token, user, access_token (includes refresh token).
  session_contents = {access_token=true},
}

local _M = {}

function _M.validate_access_token()
  local res, err
  for _, public_key in ipairs(public_keys) do
    entra_id_opts.public_key = public_key
    res, err = openidc.bearer_jwt_verify(entra_id_opts)
    if res and not err then
      break
    end
  end
  if err or not res then
    ngx.status = 403
    ngx.say(err and err or "no access_token provided")
    ngx.exit(ngx.HTTP_FORBIDDEN)
  end
end

return _M

Any idea how to deal with this? Or am I understanding something totally wrong? I am not sure how usual that is that a provider uses multiple JWK and returns different ones to the same client id.

Could this be a feature request that it is possible to provide this x5t to x5c mapping in opts and the verification considers this?

Environment
bodewig commented 3 weeks ago

If Entra provides a JWKS endpoint and properly announces it in the OpenID Connect discovery endpoint then you could make lua-resty-openidc use the discovery endpoint an pull in the JWKS itself - in which case it handles keystores with multiple keys properly. This currently is the only way to support multiple public keys - which at the same time would simplify your configuration, I guess.