Skip to content

Commit

Permalink
[oidc] expose OIDC as standalone policy
Browse files Browse the repository at this point in the history
  • Loading branch information
mikz committed Sep 27, 2018
1 parent 01112b5 commit 89d58ba
Show file tree
Hide file tree
Showing 9 changed files with 484 additions and 67 deletions.
2 changes: 1 addition & 1 deletion gateway/src/apicast/configuration/service.lua
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function _M:oauth()
local authentication = self.authentication_method or self.backend_version

if authentication == 'oidc' then
return oauth.oidc.new(self)
return oauth.oidc.new(self.oidc)
elseif authentication == 'oauth' then
return oauth.apicast.new(self)
else
Expand Down
116 changes: 77 additions & 39 deletions gateway/src/apicast/oauth/oidc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ local setmetatable = setmetatable
local ngx_now = ngx.now
local format = string.format
local type = type
local tostring = tostring
local assert = assert

local _M = {
cache_size = 10000,
Expand All @@ -28,20 +30,18 @@ local mt = {

local empty = {}

function _M.new(service)
local oidc = service.oidc or empty

local issuer = oidc.issuer or ""
function _M.new(oidc_config)
local oidc = oidc_config or empty
local issuer = oidc.issuer
local config = oidc.config or empty
local alg_values = config.id_token_signing_alg_values_supported or empty

local err
if #issuer == 0 or #alg_values == 0 then
if not issuer or #alg_values == 0 then
err = 'missing OIDC configuration'
end

return setmetatable({
service = service,
config = config,
issuer = issuer,
keys = oidc.keys or empty,
Expand All @@ -51,7 +51,7 @@ function _M.new(service)
jwt_claims = {
-- 1. The JWT MUST contain an "iss" (issuer) claim that contains a
-- unique identifier for the entity that issued the JWT.
iss = jwt_validators.equals_any_of({ issuer }),
iss = issuer and jwt_validators.equals_any_of({ issuer }) or jwt_validators.required(),

-- 2. The JWT MUST contain a "sub" (subject) claim identifying the
-- principal that is the subject of the JWT.
Expand All @@ -74,6 +74,9 @@ function _M.new(service)
-- 6. The JWT MAY contain an "iat" (issued at) claim that identifies
-- the time at which the JWT was issued.
iat = jwt_validators.opt_greater_than(0),

-- This is keycloak-specific. Its tokens have a 'typ' and we need to verify
typ = jwt_validators.opt_equals_any_of({ 'Bearer' }),
},
}, mt), err
end
Expand All @@ -92,64 +95,99 @@ end
-- Parses the token - in this case we assume it's a JWT token
-- Here we can extract authenticated user's claims or other information returned in the access_token
-- or id_token by RH SSO
local function parse_and_verify_token(self, jwt_token)
local function parse_and_verify_token(self, namespace, jwt_token)
local cache = self.cache

if not cache then
return nil, 'not initialized'
end
local cache_key = format('%s:%s', self.service.id, jwt_token)
local cache_key = format('%s:%s', namespace, jwt_token)

local jwt = self:parse(jwt_token, cache_key)

if jwt.verified then
return jwt
end

local _, err = self:verify(jwt, cache_key)

return jwt, err
end

function _M:parse_and_verify(access_token, cache_key)
local jwt_obj, err = parse_and_verify_token(self, assert(cache_key, 'missing cache key'), access_token)

if err then
if ngx.config.debug then
ngx.log(ngx.DEBUG, 'JWT object: ', require('inspect')(jwt_obj), ' err: ', err, ' reason: ', jwt_obj.reason)
end
return nil, jwt_obj and jwt_obj.reason or err
end

return jwt_obj
end

local jwt_mt = {
__tostring = function(jwt)
return jwt.token
end
}

local function load_jwt(token)
local jwt = JWT:load_jwt(tostring(token))

local jwt_obj = cache:get(cache_key)
jwt.token = token

if jwt_obj then
return setmetatable(jwt, jwt_mt)
end

function _M:parse(jwt, cache_key)
local cached = self.cache:get(cache_key or jwt)

if cached then
ngx.log(ngx.DEBUG, 'found JWT in cache for ', cache_key)
return jwt_obj
return cached
end

jwt_obj = JWT:load_jwt(jwt_token)
return load_jwt(jwt)
end

if not jwt_obj.valid then
ngx.log(ngx.WARN, jwt_obj.reason)
return jwt_obj, 'JWT not valid'
function _M:verify(jwt, cache_key)
if not jwt then
return false, 'JWT missing'
end

if not self.alg_whitelist[jwt_obj.header.alg] then
return jwt_obj, '[jwt] invalid alg'
if not jwt.valid then
ngx.log(ngx.WARN, jwt.reason)
return false, 'JWT not valid'
end
-- TODO: this should be able to use DER format instead of PEM
local pubkey = find_public_key(jwt_obj, self.keys)

-- This is keycloak-specific. Its tokens have a 'typ' and we need to verify
-- it's Bearer.
local claims = self.jwt_claims
if jwt_obj.payload and jwt_obj.payload.typ then
claims.typ = jwt_validators.equals('Bearer')
if not self.alg_whitelist[jwt.header.alg] then
return false, '[jwt] invalid alg'
end

jwt_obj = JWT:verify_jwt_obj(pubkey, jwt_obj, self.jwt_claims)
-- TODO: this should be able to use DER format instead of PEM
local pubkey = find_public_key(jwt, self.keys)

jwt = JWT:verify_jwt_obj(pubkey, jwt, self.jwt_claims)

if not jwt_obj.verified then
ngx.log(ngx.DEBUG, "[jwt] failed verification for token, reason: ", jwt_obj.reason)
return jwt_obj, "JWT not verified"
if not jwt.verified then
ngx.log(ngx.DEBUG, "[jwt] failed verification for token, reason: ", jwt.reason)
return false, "JWT not verified"
end

ngx.log(ngx.DEBUG, 'adding JWT to cache ', cache_key)
local ttl = timestamp_to_seconds_from_now(jwt_obj.payload.exp, self.clock)
cache:set(cache_key, jwt_obj, ttl)
local ttl = timestamp_to_seconds_from_now(jwt.payload.exp, self.clock)
self.cache:set(cache_key or tostring(jwt), jwt, ttl)

return jwt_obj
return true
end


function _M:transform_credentials(credentials)
local jwt_obj, err = parse_and_verify_token(self, credentials.access_token)
function _M:transform_credentials(credentials, cache_key)
local jwt_obj, err = self:parse_and_verify(credentials.access_token, cache_key or '<shared>')

if err then
if ngx.config.debug then
ngx.log(ngx.DEBUG, 'JWT object: ', require('inspect')(jwt_obj), ' err: ', err, ' reason: ', jwt_obj.reason)
end
return nil, nil, nil, jwt_obj and jwt_obj.reason or err
return nil, nil, nil, err
end

local payload = jwt_obj.payload
Expand Down
13 changes: 13 additions & 0 deletions gateway/src/apicast/policy/oidc_authentication/apicast-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "http://apicast.io/policy-v1/schema#manifest#",
"name": "oidc_authentication",
"summary": "OpenID Connect Authentication",
"description": [
"TODO: Write policy description"
],
"version": "builtin",
"configuration": {
"type": "object",
"properties": { }
}
}
1 change: 1 addition & 0 deletions gateway/src/apicast/policy/oidc_authentication/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return require('oidc_authentication')
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
-- This is a oidc_authentication description.
local lrucache = require('resty.lrucache')
local OIDC = require('apicast.oauth.oidc')
local oidc_discovery = require('resty.oidc.discovery')
local http_authorization = require('resty.http_authorization')
local resty_url = require('resty.url')
local policy = require('apicast.policy')
local _M = policy.new('oidc_authentication')

local tostring = tostring

_M.cache_size = 100

function _M.init()
_M.cache = lrucache.new(_M.cache_size)
end

local function valid_issuer_endpoint(endpoint)
return resty_url.parse(endpoint) and endpoint
end

local new = _M.new
--- Initialize a oidc_authentication
-- @tparam[opt] table config Policy configuration.
function _M.new(config)
local self = new(config)

self.issuer_endpoint = valid_issuer_endpoint(config and config.issuer_endpoint)
self.discovery = oidc_discovery.new(self.http_backend)

self.oidc = (config and config.oidc) or OIDC.new(self.discovery:call(self.issuer_endpoint))

self.required = config and config.required

return self
end

local function bearer_token()
return http_authorization.new(ngx.var.http_authorization).token
end

function _M:rewrite(context)
local access_token = bearer_token()

if access_token or self.required then
local jwt, err = self.oidc:parse(access_token)

if jwt then
context[self] = jwt
context.jwt = jwt
else
ngx.log(ngx.WARN, 'failed to parse access token ', access_token, ' err: ', err)
end
end
end

local function exit_status(status)
ngx.status = status
-- TODO: implement content negotiation to generate proper content with an error
return ngx.exit(status)
end

local function challenge_response()
ngx.header.www_authenticate = 'Bearer'

return exit_status(ngx.HTTP_UNAUTHORIZED)
end

function _M:access(context)
local jwt = context[self]

if not jwt or not jwt.token then
if self.required then
return challenge_response()
else
return
end
end

local ok, err = self.oidc:verify(jwt)

if not ok then
ngx.log(ngx.INFO, 'JWT verification error: ', err, ' token: ', tostring(jwt))

return exit_status(ngx.HTTP_FORBIDDEN)
end

return ok
end

return _M
2 changes: 1 addition & 1 deletion gateway/src/apicast/proxy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ function _M:rewrite(service, context)

if self.oauth then
local jwt_payload
credentials, ttl, jwt_payload, err = self.oauth:transform_credentials(credentials)
credentials, ttl, jwt_payload, err = self.oauth:transform_credentials(credentials, service.id)

if err then
ngx.log(ngx.DEBUG, 'oauth failed with ', err)
Expand Down
Loading

0 comments on commit 89d58ba

Please sign in to comment.