Use OpenIDC for authentication fixes #58
This commit is contained in:
Родитель
80483bb2b4
Коммит
dd1db67b0b
|
@ -5,3 +5,5 @@ DB_NAME=postgres
|
|||
DB_USER=postgres
|
||||
DB_PASS=postgres
|
||||
DB_HOST=db
|
||||
OPENIDC_CLIENT_ID=
|
||||
OPENIDC_CLIENT_SECRET=
|
||||
|
|
2
Makefile
2
Makefile
|
@ -23,7 +23,7 @@ makemigrations: compose_build
|
|||
docker-compose run app python manage.py makemigrations
|
||||
|
||||
migrate: compose_build
|
||||
docker-compose run app python manage.py migrate
|
||||
docker-compose run app sh -c "/app/bin/wait-for-it.sh db:5432 -- python manage.py migrate"
|
||||
|
||||
createuser: compose_build
|
||||
docker-compose run app python manage.py createsuperuser
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
from django.core.urlresolvers import resolve, Resolver404
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
class OpenIDCAuthMiddleware(object):
|
||||
"""
|
||||
An authentication middleware that depends on a header being set in the
|
||||
request. This header will be populated by nginx configured to authenticate
|
||||
with OpenIDC.
|
||||
|
||||
We will automatically create a user object and attach it to the
|
||||
experimenters group.
|
||||
"""
|
||||
|
||||
def get_experimenter_group(self):
|
||||
experimenter_group, created = Group.objects.get_or_create(
|
||||
name='Experimenters')
|
||||
|
||||
if created:
|
||||
apps = ('projects', 'experiments')
|
||||
content_types = ContentType.objects.filter(app_label__in=apps)
|
||||
permissions = Permission.objects.filter(
|
||||
content_type__in=content_types)
|
||||
experimenter_group.permissions.add(*permissions)
|
||||
|
||||
return experimenter_group
|
||||
|
||||
def process_request(self, request):
|
||||
try:
|
||||
resolved = resolve(request.path)
|
||||
if resolved.url_name in settings.OPENIDC_AUTH_WHITELIST:
|
||||
# If the requested path is in our auth whitelist,
|
||||
# skip authentication entirely
|
||||
return
|
||||
except Resolver404:
|
||||
pass
|
||||
|
||||
openidc_email = request.META.get(settings.OPENIDC_EMAIL_HEADER, None)
|
||||
|
||||
if openidc_email is None:
|
||||
# If a user has bypassed the OpenIDC flow entirely and no header
|
||||
# is set then we reject the request entirely
|
||||
return HttpResponse(
|
||||
'Please login using OpenID Connect', status=401)
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=openidc_email)
|
||||
except User.DoesNotExist:
|
||||
user = User(username=openidc_email, email=openidc_email)
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
||||
user.groups.add(self.get_experimenter_group())
|
||||
|
||||
request.user = user
|
|
@ -0,0 +1,95 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.core.urlresolvers import Resolver404
|
||||
from django.test import TestCase
|
||||
|
||||
import mock
|
||||
|
||||
from experimenter.openidc.middleware import OpenIDCAuthMiddleware
|
||||
|
||||
|
||||
class OpenIDCAuthMiddlewareTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = OpenIDCAuthMiddleware()
|
||||
|
||||
mock_resolve_patcher = mock.patch(
|
||||
'experimenter.openidc.middleware.resolve')
|
||||
self.mock_resolve = mock_resolve_patcher.start()
|
||||
self.addCleanup(mock_resolve_patcher.stop)
|
||||
|
||||
def test_get_group_creates_group_with_project_experiment_permissions(self):
|
||||
self.assertFalse(Group.objects.all().exists())
|
||||
group = self.middleware.get_experimenter_group()
|
||||
self.assertTrue(Group.objects.all().exists())
|
||||
self.assertEqual(group.permissions.all().count(), 9)
|
||||
self.assertEqual(
|
||||
set([
|
||||
permission.content_type.app_label for
|
||||
permission in group.permissions.all()
|
||||
]),
|
||||
set(['projects', 'experiments']),
|
||||
)
|
||||
|
||||
def test_get_group_only_creates_one_group(self):
|
||||
self.assertEqual(Group.objects.all().count(), 0)
|
||||
self.middleware.get_experimenter_group()
|
||||
self.assertEqual(Group.objects.all().count(), 1)
|
||||
self.middleware.get_experimenter_group()
|
||||
self.assertEqual(Group.objects.all().count(), 1)
|
||||
|
||||
def test_whitelisted_url_is_not_authed(self):
|
||||
request = mock.Mock()
|
||||
request.path = '/whitelisted-view/'
|
||||
whitelisted_view_name = 'whitelisted-view'
|
||||
|
||||
with self.settings(OPENIDC_AUTH_WHITELIST=[whitelisted_view_name]):
|
||||
mock_view = mock.Mock()
|
||||
mock_view.url_name = whitelisted_view_name
|
||||
self.mock_resolve.return_value = mock_view
|
||||
|
||||
response = self.middleware.process_request(request)
|
||||
self.assertEqual(response, None)
|
||||
|
||||
def test_404_path_forces_authentication(self):
|
||||
request = mock.Mock()
|
||||
request.META = {
|
||||
}
|
||||
|
||||
self.mock_resolve.side_effect = Resolver404
|
||||
|
||||
response = self.middleware.process_request(request)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_request_missing_headers_raises_401(self):
|
||||
request = mock.Mock()
|
||||
request.META = {
|
||||
}
|
||||
|
||||
with self.settings(OPENIDC_AUTH_WHITELIST=[]):
|
||||
response = self.middleware.process_request(request)
|
||||
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_user_created_with_correct_email_from_header(self):
|
||||
user_email = 'user@example.com'
|
||||
|
||||
request = mock.Mock()
|
||||
request.META = {
|
||||
settings.OPENIDC_EMAIL_HEADER: user_email,
|
||||
}
|
||||
|
||||
self.assertEqual(User.objects.all().count(), 0)
|
||||
|
||||
with self.settings(OPENIDC_AUTH_WHITELIST=[]):
|
||||
response = self.middleware.process_request(request)
|
||||
|
||||
self.assertEqual(response, None)
|
||||
self.assertEqual(User.objects.all().count(), 1)
|
||||
|
||||
self.assertEqual(request.user.email, user_email)
|
||||
self.assertTrue(request.user.is_staff)
|
||||
self.assertTrue(
|
||||
self.middleware.get_experimenter_group()
|
||||
in request.user.groups.all()
|
||||
)
|
|
@ -45,8 +45,9 @@ INSTALLED_APPS = [
|
|||
'raven.contrib.django.raven_compat',
|
||||
'rest_framework',
|
||||
|
||||
'experimenter.projects',
|
||||
'experimenter.openidc',
|
||||
'experimenter.experiments',
|
||||
'experimenter.projects',
|
||||
]
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
|
@ -54,12 +55,12 @@ MIDDLEWARE_CLASSES = [
|
|||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
'dockerflow.django.middleware.DockerflowMiddleware',
|
||||
|
||||
'experimenter.openidc.middleware.OpenIDCAuthMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'experimenter.urls'
|
||||
|
@ -118,6 +119,11 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
OPENIDC_EMAIL_HEADER = 'HTTP_X_FORWARDED_USER'
|
||||
OPENIDC_AUTH_WHITELIST = (
|
||||
'experiments-list',
|
||||
)
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.9/topics/i18n/
|
||||
|
|
|
@ -17,6 +17,7 @@ services:
|
|||
|
||||
nginx:
|
||||
build: ./nginx
|
||||
env_file: .env
|
||||
links:
|
||||
- app
|
||||
ports:
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
FROM nginx:1.9.9
|
||||
FROM openresty/openresty:trusty
|
||||
|
||||
RUN rm /etc/nginx/nginx.conf
|
||||
COPY nginx.conf /etc/nginx/
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y libssl-dev git-core
|
||||
RUN luarocks install lua-resty-session
|
||||
RUN luarocks install lua-resty-openidc
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx-site.conf /etc/nginx/conf.d/
|
||||
RUN rm /usr/local/openresty/nginx/conf/nginx.conf
|
||||
COPY nginx.conf /usr/local/openresty/nginx/conf/
|
||||
|
||||
COPY openidc.lua /usr/local/openresty/luajit/share/lua/5.1/resty/openidc.lua
|
||||
|
||||
COPY openidc_layer.lua /usr/local/openresty/nginx/conf/
|
||||
COPY login_conf.lua /usr/local/openresty/nginx/conf/
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
local cjson = require("cjson")
|
||||
|
||||
-- configs
|
||||
local opts = {
|
||||
client_id = ngx.var.oidc_client_id,
|
||||
client_secret = ngx.var.oidc_client_secret,
|
||||
discovery = ngx.var.oidc_discovery or "https://auth.mozilla.auth0.com/.well-known/openid-configuration",
|
||||
logout_path = ngx.var.oidc_logout_path,
|
||||
redirect_uri_path = ngx.var.oidc_redirect_uri_path,
|
||||
redirect_uri_scheme = "https",
|
||||
scope = ngx.var.oidc_scope or "openid email profile",
|
||||
token_endpoint_auth_method = "client_secret_post",
|
||||
}
|
||||
|
||||
if ngx.var.oidc_hd then
|
||||
opts.authorization_params = {hd=ngx.var.oidc_hd}
|
||||
end
|
||||
|
||||
-- call authenticate for OpenID Connect user authentication
|
||||
local res, err = require("resty.openidc").authenticate(opts)
|
||||
|
||||
if err then
|
||||
ngx.status = 500
|
||||
ngx.say(err)
|
||||
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
|
||||
end
|
||||
|
||||
-- require hd matches if specified
|
||||
if ngx.var.oidc_hd and res.id_token.hd ~= ngx.var.oidc_hd then
|
||||
ngx.exit(ngx.HTTP_FORBIDDEN)
|
||||
end
|
||||
|
||||
-- if at least one email or group is specified, require user is in one of them
|
||||
local validate_user, valid_user
|
||||
|
||||
for _, email in (ngx.var.oidc_emails or ""):gmatch("([^,]+),?") do
|
||||
validate_user = true
|
||||
if res.user.email == email then
|
||||
valid_user = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
for _, group in (ngx.var.oidc_groups or ""):gmatch("([^,]+),?") do
|
||||
validate_user = true
|
||||
for _, usergroup in res.user.groups do
|
||||
if usergroup == group then
|
||||
valid_user = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if validate_user and not valid_user then
|
||||
ngx.exit(ngx.HTTP_FORBIDDEN)
|
||||
end
|
||||
|
||||
-- delete OIDC* headers from the request
|
||||
for key, _ in pairs(ngx.req.get_headers()) do
|
||||
if key:sub(1,4):upper() == "OIDC" then
|
||||
ngx.req.clear_header(key)
|
||||
end
|
||||
end
|
||||
|
||||
-- set headers with user info
|
||||
ngx.req.set_header("REMOTE-USER", res.id_token.user_id)
|
||||
ngx.req.set_header("OIDC-CLAIM-ACCESS-TOKEN", res.access_token)
|
||||
|
||||
local function build_headers(t, name)
|
||||
for k,v in pairs(t) do
|
||||
k = k:gsub("_", "-")
|
||||
-- unpack tables
|
||||
if type(v) == "table" then
|
||||
local j = cjson.encode(v)
|
||||
ngx.req.set_header("OIDC-CLAIM-"..name..k, j)
|
||||
else
|
||||
ngx.req.set_header("OIDC-CLAIM-"..name..k, tostring(v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
build_headers(res.id_token, "ID-TOKEN-")
|
||||
build_headers(res.user, "USER-PROFILE-")
|
|
@ -0,0 +1,13 @@
|
|||
-- lua-resty-openidc options
|
||||
opts = {
|
||||
redirect_uri_path = "/openid/callback/login/",
|
||||
discovery = "https://auth.mozilla.auth0.com/.well-known/openid-configuration",
|
||||
client_id = os.getenv("OPENIDC_CLIENT_ID"),
|
||||
client_secret = os.getenv("OPENIDC_CLIENT_SECRET"),
|
||||
scope = "openid email profile",
|
||||
iat_slack = 600,
|
||||
redirect_uri_scheme = "http",
|
||||
logout_path = "/logout",
|
||||
redirect_after_logout_uri = "/",
|
||||
refresh_session_interval = 900
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:7001/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
|
@ -1,24 +1,26 @@
|
|||
user nginx;
|
||||
env OPENIDC_CLIENT_ID;
|
||||
env OPENIDC_CLIENT_SECRET;
|
||||
env SECRET_KEY;
|
||||
|
||||
worker_processes 4;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
error_log /usr/local/openresty/nginx/logs/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 19000;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
include /usr/local/openresty/nginx/conf/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
access_log /usr/local/openresty/nginx/logs/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
@ -27,5 +29,35 @@ http {
|
|||
|
||||
#gzip on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
resolver 8.8.8.8;
|
||||
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||
lua_ssl_verify_depth 5;
|
||||
lua_shared_dict discovery 1m;
|
||||
lua_shared_dict introspection 15m;
|
||||
lua_shared_dict sessions 10m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
set $session_storage shm;
|
||||
set $session_cookie_persistent on;
|
||||
set $session_cookie_path "/";
|
||||
set $session_check_ssi off;
|
||||
set_by_lua $session_secret 'return os.getenv("SECRET_KEY")';
|
||||
set $config_loader "/usr/local/openresty/nginx/conf/login_conf.lua";
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://app:7001/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location / {
|
||||
access_by_lua_file '/usr/local/openresty/nginx/conf/openidc_layer.lua';
|
||||
proxy_pass http://app:7001/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,849 @@
|
|||
--[[
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
***************************************************************************
|
||||
Copyright (C) 2015-2017 Ping Identity Corporation
|
||||
All rights reserved.
|
||||
|
||||
For further information please contact:
|
||||
|
||||
Ping Identity Corporation
|
||||
1099 18th St Suite 2950
|
||||
Denver, CO 80202
|
||||
303.468.2900
|
||||
http://www.pingidentity.com
|
||||
|
||||
DISCLAIMER OF WARRANTIES:
|
||||
|
||||
THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
|
||||
ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
|
||||
WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY
|
||||
WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
|
||||
USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
|
||||
YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
|
||||
WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@Author: Hans Zandbelt - hans.zandbelt@zmartzone.eu
|
||||
--]]
|
||||
|
||||
local require = require
|
||||
local cjson = require "cjson"
|
||||
local http = require "resty.http"
|
||||
local string = string
|
||||
local ipairs = ipairs
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local ngx = ngx
|
||||
|
||||
local openidc = {
|
||||
_VERSION = "1.3.1"
|
||||
}
|
||||
openidc.__index = openidc
|
||||
|
||||
-- set value in server-wide cache if available
|
||||
local function openidc_cache_set(type, key, value, exp)
|
||||
local dict = ngx.shared[type]
|
||||
if dict then
|
||||
local success, err, forcible = dict:set(key, value, exp)
|
||||
ngx.log(ngx.DEBUG, "cache set: success=", success, " err=", err, " forcible=", forcible)
|
||||
end
|
||||
end
|
||||
|
||||
-- retrieve value from server-wide cache if available
|
||||
local function openidc_cache_get(type, key)
|
||||
local dict = ngx.shared[type]
|
||||
local value
|
||||
local flags
|
||||
if dict then
|
||||
value, flags = dict:get(key)
|
||||
if value then ngx.log(ngx.DEBUG, "cache hit: type=", type, " key=", key) end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
-- validate the contents of and id_token
|
||||
local function openidc_validate_id_token(opts, id_token, nonce)
|
||||
|
||||
-- check issuer
|
||||
if opts.discovery.issuer ~= id_token.iss then
|
||||
ngx.log(ngx.ERR, "issuer \"", id_token.iss, " in id_token is not equal to the issuer from the discovery document \"", opts.discovery.issuer, "\"")
|
||||
return false
|
||||
end
|
||||
|
||||
-- check nonce
|
||||
if nonce and nonce ~= id_token.nonce then
|
||||
ngx.log(ngx.ERR, "nonce \"", id_token.nonce, " in id_token is not equal to the nonce that was sent in the request \"", nonce, "\"")
|
||||
return false
|
||||
end
|
||||
|
||||
-- check issued-at timestamp
|
||||
if not id_token.iat then
|
||||
ngx.log(ngx.ERR, "no \"iat\" claim found in id_token")
|
||||
return false
|
||||
end
|
||||
|
||||
local slack=opts.iat_slack and opts.iat_slack or 120
|
||||
if id_token.iat < (ngx.time() - slack) then
|
||||
ngx.log(ngx.ERR, "token is not valid yet: id_token.iat=", id_token.iat, ", ngx.time()=", ngx.time())
|
||||
return false
|
||||
end
|
||||
|
||||
-- check expiry timestamp
|
||||
if id_token.exp < ngx.time() then
|
||||
ngx.log(ngx.ERR, "token expired: id_token.exp=", id_token.exp, ", ngx.time()=", ngx.time())
|
||||
return false
|
||||
end
|
||||
|
||||
-- check audience (array or string)
|
||||
if (type(id_token.aud) == "table") then
|
||||
for key, value in pairs(id_token.aud) do
|
||||
if value == opts.client_id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
ngx.log(ngx.ERR, "no match found token audience array: client_id=", opts.client_id )
|
||||
return false
|
||||
elseif (type(id_token.aud) == "string") then
|
||||
if id_token.aud ~= opts.client_id then
|
||||
ngx.log(ngx.ERR, "token audience does not match: id_token.aud=", id_token.aud, ", client_id=", opts.client_id )
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- assemble the redirect_uri
|
||||
local function openidc_get_redirect_uri(opts)
|
||||
local scheme = opts.redirect_uri_scheme or ngx.req.get_headers()['X-Forwarded-Proto'] or ngx.var.scheme
|
||||
return scheme.."://"..ngx.var.http_host..opts.redirect_uri_path
|
||||
end
|
||||
|
||||
-- perform base64url decoding
|
||||
local function openidc_base64_url_decode(input)
|
||||
local reminder = #input % 4
|
||||
if reminder > 0 then
|
||||
local padlen = 4 - reminder
|
||||
input = input .. string.rep('=', padlen)
|
||||
end
|
||||
input = input:gsub('-','+'):gsub('_','/')
|
||||
return ngx.decode_base64(input)
|
||||
end
|
||||
|
||||
-- perform base64url encoding
|
||||
local function openidc_base64_url_encode(input)
|
||||
input = ngx.encode_base64(input)
|
||||
return input:gsub('+','-'):gsub('/','_'):gsub('=','')
|
||||
end
|
||||
|
||||
-- send the browser of to the OP's authorization endpoint
|
||||
local function openidc_authorize(opts, session, target_url)
|
||||
local resty_random = require "resty.random"
|
||||
local resty_string = require "resty.string"
|
||||
|
||||
-- generate state and nonce
|
||||
local state = resty_string.to_hex(resty_random.bytes(16))
|
||||
local nonce = resty_string.to_hex(resty_random.bytes(16))
|
||||
|
||||
-- assemble the parameters to the authentication request
|
||||
local params = {
|
||||
client_id=opts.client_id,
|
||||
response_type="code",
|
||||
scope=opts.scope and opts.scope or "openid email profile",
|
||||
redirect_uri=openidc_get_redirect_uri(opts),
|
||||
state=state,
|
||||
nonce=nonce,
|
||||
prompt=opts.prompt and opts.prompt or ""
|
||||
}
|
||||
|
||||
-- merge any provided extra parameters
|
||||
if opts.authorization_params then
|
||||
for k,v in pairs(opts.authorization_params) do params[k] = v end
|
||||
end
|
||||
|
||||
-- store state in the session
|
||||
session:start()
|
||||
session.data.original_url = target_url
|
||||
session.data.state = state
|
||||
session.data.nonce = nonce
|
||||
session.data.last_authenticated = ngx.time()
|
||||
session:save()
|
||||
|
||||
-- redirect to the /authorization endpoint
|
||||
return ngx.redirect(opts.discovery.authorization_endpoint.."?"..ngx.encode_args(params))
|
||||
end
|
||||
|
||||
-- parse the JSON result from a call to the OP
|
||||
local function openidc_parse_json_response(response)
|
||||
|
||||
local err
|
||||
local res
|
||||
|
||||
-- check the response from the OP
|
||||
if response.status ~= 200 then
|
||||
err = "response indicates failure, status="..response.status..", body="..response.body
|
||||
else
|
||||
-- decode the response and extract the JSON object
|
||||
res = cjson.decode(response.body)
|
||||
|
||||
if not res then
|
||||
err = "JSON decoding failed"
|
||||
end
|
||||
end
|
||||
|
||||
return res, err
|
||||
end
|
||||
|
||||
-- make a call to the token endpoint
|
||||
local function openidc_call_token_endpoint(opts, endpoint, body, auth)
|
||||
|
||||
local headers = {
|
||||
["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
if auth then
|
||||
if auth == "client_secret_basic" then
|
||||
headers.Authorization = "Basic "..ngx.encode_base64( opts.client_id..":"..opts.client_secret)
|
||||
ngx.log(ngx.DEBUG,"client_secret_basic: authorization header '"..headers.Authorization.."'")
|
||||
end
|
||||
if auth == "client_secret_post" then
|
||||
body.client_id=opts.client_id
|
||||
body.client_secret=opts.client_secret
|
||||
ngx.log(ngx.DEBUG, "client_secret_post: client_id and client_secret being sent in POST body")
|
||||
end
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "request body for token endpoint call: ", ngx.encode_args(body))
|
||||
|
||||
local httpc = http.new()
|
||||
local res, err = httpc:request_uri(endpoint, {
|
||||
method = "POST",
|
||||
body = ngx.encode_args(body),
|
||||
headers = headers,
|
||||
ssl_verify = (opts.ssl_verify ~= "no")
|
||||
})
|
||||
if not res then
|
||||
err = "accessing token endpoint ("..endpoint..") failed: "..err
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "token endpoint response: ", res.body)
|
||||
|
||||
return openidc_parse_json_response(res);
|
||||
end
|
||||
|
||||
-- make a call to the userinfo endpoint
|
||||
local function openidc_call_userinfo_endpoint(opts, access_token)
|
||||
if not opts.discovery.userinfo_endpoint then
|
||||
ngx.log(ngx.DEBUG, "no userinfo endpoint supplied")
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local httpc = http.new()
|
||||
local res, err = httpc:request_uri(opts.discovery.userinfo_endpoint, {
|
||||
headers = {
|
||||
["Authorization"] = "Bearer "..access_token,
|
||||
}
|
||||
})
|
||||
if not res then
|
||||
err = "accessing userinfo endpoint ("..opts.discovery.userinfo_endpoint..") failed: "..err
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "userinfo response: ", res.body)
|
||||
|
||||
-- parse the response from the user info endpoint
|
||||
return openidc_parse_json_response(res)
|
||||
end
|
||||
|
||||
-- computes access_token expires_in value (in seconds)
|
||||
local function openidc_access_token_expires_in(opts, expires_in)
|
||||
return (expires_in or opts.access_token_expires_in or 3600) - 1 - (opts.access_token_expires_leeway or 0)
|
||||
end
|
||||
|
||||
-- handle a "code" authorization response from the OP
|
||||
local function openidc_authorization_response(opts, session)
|
||||
local args = ngx.req.get_uri_args()
|
||||
local err
|
||||
|
||||
if not args.code or not args.state then
|
||||
err = "unhandled request to the redirect_uri: "..ngx.var.request_uri
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- check that the state returned in the response against the session; prevents CSRF
|
||||
if args.state ~= session.data.state then
|
||||
err = "state from argument: "..(args.state and args.state or "nil").." does not match state restored from session: "..(session.data.state and session.data.state or "nil")
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- check the iss if returned from the OP
|
||||
if args.iss and args.iss ~= opts.discovery.issuer then
|
||||
err = "iss from argument: "..args.iss.." does not match expected issuer: "..opts.discovery.issuer
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- check the client_id if returned from the OP
|
||||
if args.client_id and args.client_id ~= opts.client_id then
|
||||
err = "client_id from argument: "..args.client_id.." does not match expected client_id: "..opts.client_id
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- assemble the parameters to the token endpoint
|
||||
local body = {
|
||||
grant_type="authorization_code",
|
||||
code=args.code,
|
||||
redirect_uri=openidc_get_redirect_uri(opts),
|
||||
state = session.data.state
|
||||
}
|
||||
|
||||
local current_time = ngx.time()
|
||||
-- make the call to the token endpoint
|
||||
local json, err = openidc_call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
|
||||
if err then
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- process the token endpoint response with the id_token and access_token
|
||||
local enc_hdr, enc_pay, enc_sign = string.match(json.id_token, '^(.+)%.(.+)%.(.+)$')
|
||||
local jwt = openidc_base64_url_decode(enc_pay)
|
||||
local id_token = cjson.decode(jwt)
|
||||
|
||||
-- validate the id_token contents
|
||||
if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then
|
||||
err = "id_token validation failed"
|
||||
return nil, err, session.data.original_url, session
|
||||
end
|
||||
|
||||
-- call the user info endpoint
|
||||
-- TODO: should this error be checked?
|
||||
local user, err = openidc_call_userinfo_endpoint(opts, json.access_token)
|
||||
|
||||
session:start()
|
||||
session.data.user = user
|
||||
session.data.id_token = id_token
|
||||
session.data.enc_id_token = json.id_token
|
||||
session.data.access_token = json.access_token
|
||||
session.data.access_token_expiration = current_time
|
||||
+ openidc_access_token_expires_in(opts, json.expires_in)
|
||||
if json.refresh_token ~= nil then
|
||||
session.data.refresh_token = json.refresh_token
|
||||
end
|
||||
|
||||
-- save the session with the obtained id_token
|
||||
session:save()
|
||||
|
||||
-- redirect to the URL that was accessed originally
|
||||
return ngx.redirect(session.data.original_url), session
|
||||
|
||||
end
|
||||
|
||||
-- get the Discovery metadata from the specified URL
|
||||
local function openidc_discover(url, ssl_verify)
|
||||
ngx.log(ngx.DEBUG, "In openidc_discover - URL is "..url)
|
||||
|
||||
local json, err
|
||||
local v = openidc_cache_get("discovery", url)
|
||||
if not v then
|
||||
|
||||
ngx.log(ngx.DEBUG, "Discovery data not in cache. Making call to discovery endpoint")
|
||||
-- make the call to the discovery endpoint
|
||||
local httpc = http.new()
|
||||
local res, error = httpc:request_uri(url, {
|
||||
ssl_verify = (ssl_verify ~= "no")
|
||||
})
|
||||
if not res then
|
||||
err = "accessing discovery url ("..url..") failed: "..error
|
||||
ngx.log(ngx.ERR, err)
|
||||
else
|
||||
ngx.log(ngx.DEBUG, "Response data: "..res.body)
|
||||
json, err = openidc_parse_json_response(res)
|
||||
if json then
|
||||
if string.sub(url, 1, string.len(json['issuer'])) == json['issuer'] then
|
||||
openidc_cache_set("discovery", url, cjson.encode(json), 24 * 60 * 60)
|
||||
else
|
||||
err = "issuer field in Discovery data does not match URL"
|
||||
json = nil
|
||||
end
|
||||
else
|
||||
err = "could not decode JSON from Discovery data"
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
json = cjson.decode(v)
|
||||
end
|
||||
|
||||
return json, err
|
||||
end
|
||||
|
||||
local function openidc_jwks(url, ssl_verify)
|
||||
ngx.log(ngx.DEBUG, "In openidc_jwks - URL is "..url)
|
||||
|
||||
local json, err
|
||||
local v = openidc_cache_get("jwks", url)
|
||||
if not v then
|
||||
|
||||
ngx.log(ngx.DEBUG, "JWKS data not in cache. Making call to jwks endpoint")
|
||||
-- make the call to the jwks endpoint
|
||||
local httpc = http.new()
|
||||
local res, error = httpc:request_uri(url, {
|
||||
ssl_verify = (ssl_verify ~= "no")
|
||||
})
|
||||
if not res then
|
||||
err = "accessing jwks url ("..url..") failed: "..error
|
||||
ngx.log(ngx.ERR, err)
|
||||
else
|
||||
ngx.log(ngx.DEBUG, "Response data: "..res.body)
|
||||
json, err = openidc_parse_json_response(res)
|
||||
if json then
|
||||
openidc_cache_set("jwks", url, cjson.encode(json), 24 * 60 * 60)
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
json = cjson.decode(v)
|
||||
end
|
||||
|
||||
return json, err
|
||||
end
|
||||
|
||||
local function split_by_chunk(text, chunkSize)
|
||||
local s = {}
|
||||
for i=1, #text, chunkSize do
|
||||
s[#s+1] = text:sub(i,i+chunkSize - 1)
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
local function get_jwk (keys, kid)
|
||||
for _, value in pairs(keys) do
|
||||
if value.kid == kid then
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function pem_from_jwk (opts, kid)
|
||||
local cache_id = opts.discovery.jwks_uri .. '#' .. kid
|
||||
local v = openidc_cache_get("jwks", cache_id)
|
||||
|
||||
if v then
|
||||
return v
|
||||
end
|
||||
|
||||
local jwks, err = openidc_jwks(opts.discovery.jwks_uri, opts.ssl_verify)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local x5c = get_jwk(jwks.keys, kid).x5c
|
||||
-- TODO check x5c length
|
||||
local chunks = split_by_chunk(ngx.encode_base64(openidc_base64_url_decode(x5c[1])), 64)
|
||||
local pem = "-----BEGIN CERTIFICATE-----\n" .. table.concat(chunks, "\n") .. "\n-----END CERTIFICATE-----"
|
||||
openidc_cache_set("jwks", cache_id, pem, 24 * 60 * 60)
|
||||
return pem
|
||||
end
|
||||
|
||||
local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" ..
|
||||
"\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" ..
|
||||
"\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" ..
|
||||
"\002\007\001\002\154\028\049\113\000\000\000\000\073\069\078\068" ..
|
||||
"\174\066\096\130"
|
||||
|
||||
-- handle logout
|
||||
local function openidc_logout(opts, session)
|
||||
session:destroy()
|
||||
local headers = ngx.req.get_headers()
|
||||
local header = headers['Accept']
|
||||
if header and header:find("image/png") then
|
||||
ngx.header["Cache-Control"] = "no-cache, no-store"
|
||||
ngx.header["Pragma"] = "no-cache"
|
||||
ngx.header["P3P"] = "CAO PSA OUR"
|
||||
ngx.header["Expires"] = "0"
|
||||
ngx.header["X-Frame-Options"] = "DENY"
|
||||
ngx.header.content_type = "image/png"
|
||||
ngx.print(openidc_transparent_pixel)
|
||||
ngx.exit(ngx.OK)
|
||||
return
|
||||
elseif opts.redirect_after_logout_uri then
|
||||
return ngx.redirect(opts.redirect_after_logout_uri)
|
||||
elseif opts.discovery.end_session_endpoint then
|
||||
return ngx.redirect(opts.discovery.end_session_endpoint)
|
||||
elseif opts.discovery.ping_end_session_endpoint then
|
||||
return ngx.redirect(opts.discovery.ping_end_session_endpoint)
|
||||
end
|
||||
|
||||
ngx.header.content_type = "text/html"
|
||||
ngx.say("<html><body>Logged Out</body></html>")
|
||||
ngx.exit(ngx.OK)
|
||||
end
|
||||
|
||||
-- get the token endpoint authentication method
|
||||
local function openidc_get_token_auth_method(opts)
|
||||
|
||||
local result
|
||||
if opts.discovery.token_endpoint_auth_methods_supported ~= nil then
|
||||
-- if set check to make sure the discovery data includes the selected client auth method
|
||||
if opts.token_endpoint_auth_method ~= nil then
|
||||
for index, value in ipairs (opts.discovery.token_endpoint_auth_methods_supported) do
|
||||
ngx.log(ngx.DEBUG, index.." => "..value)
|
||||
if value == opts.token_endpoint_auth_method then
|
||||
ngx.log(ngx.DEBUG, "configured value for token_endpoint_auth_method ("..opts.token_endpoint_auth_method..") found in token_endpoint_auth_methods_supported in metadata")
|
||||
result = opts.token_endpoint_auth_method
|
||||
break
|
||||
end
|
||||
end
|
||||
if result == nil then
|
||||
ngx.log(ngx.ERR, "configured value for token_endpoint_auth_method ("..opts.token_endpoint_auth_method..") NOT found in token_endpoint_auth_methods_supported in metadata")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
result = opts.discovery.token_endpoint_auth_methods_supported[1]
|
||||
ngx.log(ngx.DEBUG, "no configuration setting for option so select the first method specified by the OP: "..result)
|
||||
end
|
||||
else
|
||||
result = opts.token_endpoint_auth_method
|
||||
end
|
||||
|
||||
-- set a sane default if auto-configuration failed
|
||||
if result == nil then
|
||||
result = "client_secret_basic"
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "token_endpoint_auth_method result set to "..result)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- returns a valid access_token (eventually refreshing the token)
|
||||
local function openidc_access_token(opts, session)
|
||||
|
||||
local err
|
||||
|
||||
if session.data.access_token == nil then
|
||||
return nil, err
|
||||
end
|
||||
local current_time = ngx.time()
|
||||
if current_time < session.data.access_token_expiration then
|
||||
return session.data.access_token, err
|
||||
end
|
||||
if session.data.refresh_token == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "refreshing expired access_token: ", session.data.access_token, " with: ", session.data.refresh_token)
|
||||
-- assemble the parameters to the token endpoint
|
||||
local body = {
|
||||
grant_type="refresh_token",
|
||||
refresh_token=session.data.refresh_token,
|
||||
scope=opts.scope and opts.scope or "openid email profile"
|
||||
}
|
||||
|
||||
local json, err = openidc_call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
ngx.log(ngx.DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token)
|
||||
|
||||
session:start()
|
||||
session.data.access_token = json.access_token
|
||||
session.data.access_token_expiration = current_time + openidc_access_token_expires_in(opts, json.expires_in)
|
||||
if json.refresh_token ~= nil then
|
||||
session.data.refresh_token = json.refresh_token
|
||||
end
|
||||
|
||||
-- save the session with the new access_token and optionally the new refresh_token
|
||||
session:save()
|
||||
|
||||
return session.data.access_token, err
|
||||
|
||||
end
|
||||
|
||||
-- main routine for OpenID Connect user authentication
|
||||
function openidc.authenticate(opts, target_url, unauth_action, session_opts)
|
||||
|
||||
local err
|
||||
|
||||
local session = require("resty.session").open(session_opts)
|
||||
|
||||
local target_url = target_url or ngx.var.request_uri
|
||||
|
||||
local access_token
|
||||
|
||||
if type(opts.discovery) == "string" then
|
||||
--if session.data.discovery then
|
||||
-- opts.discovery = session.data.discovery
|
||||
--else
|
||||
-- session.data.discovery = opts.discovery
|
||||
--end
|
||||
opts.discovery, err = openidc_discover(opts.discovery, opts.ssl_verify)
|
||||
if err then
|
||||
return nil, err, target_url, session
|
||||
end
|
||||
end
|
||||
|
||||
-- set the authentication method for the token endpoint
|
||||
opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts)
|
||||
|
||||
-- see if this is a request to the redirect_uri i.e. an authorization response
|
||||
local path = target_url:match("(.-)%?") or target_url
|
||||
if path == opts.redirect_uri_path then
|
||||
if not session.present then
|
||||
err = "request to the redirect_uri_path but there's no session state found"
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err, target_url, session
|
||||
end
|
||||
return openidc_authorization_response(opts, session), session
|
||||
end
|
||||
|
||||
-- see if this is a request to logout
|
||||
if path == (opts.logout_path and opts.logout_path or "/logout") then
|
||||
return openidc_logout(opts, session), session
|
||||
end
|
||||
|
||||
-- if we have no id_token then redirect to the OP for authentication
|
||||
if not session.present or not session.data.id_token then
|
||||
if unauth_action == "pass" then
|
||||
return
|
||||
nil,
|
||||
err,
|
||||
target_url,
|
||||
session
|
||||
end
|
||||
return openidc_authorize(opts, session, target_url), session
|
||||
end
|
||||
|
||||
-- silently reauthenticate if necessary (mainly used for session refresh/getting updated id_token data)
|
||||
if opts.refresh_session_interval ~= nil then
|
||||
if session.data.last_authenticated == nil or (session.data.last_authenticated+opts.refresh_session_interval) < ngx.time() then
|
||||
opts.prompt = "none"
|
||||
return openidc_authorize(opts, session, target_url), session
|
||||
end
|
||||
end
|
||||
|
||||
-- refresh access_token if necessary
|
||||
access_token, err = openidc_access_token(opts, session)
|
||||
if err then
|
||||
return nil, err, target_url, session
|
||||
end
|
||||
|
||||
-- log id_token contents
|
||||
ngx.log(ngx.DEBUG, "id_token=", cjson.encode(session.data.id_token))
|
||||
|
||||
-- return the id_token to the caller Lua script for access control purposes
|
||||
return
|
||||
{
|
||||
id_token=session.data.id_token,
|
||||
access_token=access_token,
|
||||
user=session.data.user
|
||||
},
|
||||
err,
|
||||
target_url,
|
||||
session
|
||||
end
|
||||
|
||||
-- get a valid access_token (eventually refreshing the token), or nil if there's no valid access_token
|
||||
function openidc.access_token(opts, session_opts)
|
||||
|
||||
local session = require("resty.session").open(session_opts)
|
||||
|
||||
return openidc_access_token(opts, session)
|
||||
|
||||
end
|
||||
|
||||
-- get an OAuth 2.0 bearer access token from the HTTP request
|
||||
local function openidc_get_bearer_access_token(opts)
|
||||
|
||||
local err
|
||||
|
||||
-- get the access token from the Authorization header
|
||||
local headers = ngx.req.get_headers()
|
||||
local header = headers['Authorization']
|
||||
|
||||
if header == nil or header:find(" ") == nil then
|
||||
err = "no Authorization header found"
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local divider = header:find(' ')
|
||||
if string.lower(header:sub(0, divider-1)) ~= string.lower("Bearer") then
|
||||
err = "no Bearer authorization header value found"
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local access_token = header:sub(divider+1)
|
||||
if access_token == nil then
|
||||
err = "no Bearer access token value found"
|
||||
ngx.log(ngx.ERR, err)
|
||||
return nil, err
|
||||
end
|
||||
|
||||
return access_token, err
|
||||
end
|
||||
|
||||
-- main routine for OAuth 2.0 token introspection
|
||||
function openidc.introspect(opts)
|
||||
|
||||
-- get the access token from the request
|
||||
local access_token, err = openidc_get_bearer_access_token(opts)
|
||||
if access_token == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- see if we've previously cached the introspection result for this access token
|
||||
local json
|
||||
local v = openidc_cache_get("introspection", access_token)
|
||||
if not v then
|
||||
|
||||
-- assemble the parameters to the introspection (token) endpoint
|
||||
local token_param_name = opts.introspection_token_param_name and opts.introspection_token_param_name or "access_token"
|
||||
|
||||
local body = {}
|
||||
|
||||
body[token_param_name]= access_token
|
||||
|
||||
if opts.client_id then
|
||||
body.client_id=opts.client_id
|
||||
end
|
||||
if opts.client_secret then
|
||||
body.client_secret=opts.client_secret
|
||||
end
|
||||
|
||||
-- merge any provided extra parameters
|
||||
if opts.introspection_params then
|
||||
for k,v in pairs(opts.introspection_params) do body[k] = v end
|
||||
end
|
||||
|
||||
-- call the introspection endpoint
|
||||
json, err = openidc_call_token_endpoint(opts, opts.introspection_endpoint, body, nil)
|
||||
|
||||
-- cache the results
|
||||
if json then
|
||||
local expiry_claim = opts.expiry_claim or "expires_in"
|
||||
local ttl = json[expiry_claim]
|
||||
if expiry_claim ~= "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2
|
||||
ttl = ttl - ngx.time()
|
||||
end
|
||||
openidc_cache_set("introspection", access_token, cjson.encode(json), ttl)
|
||||
end
|
||||
|
||||
else
|
||||
json = cjson.decode(v)
|
||||
end
|
||||
|
||||
return json, err
|
||||
end
|
||||
|
||||
-- main routine for OAuth 2.0 JWT token validation
|
||||
-- optional args are claim specs, see jwt-validators in resty.jwt
|
||||
function openidc.jwt_verify(access_token, opts, ...)
|
||||
local err
|
||||
local json
|
||||
|
||||
-- see if we've previously cached the validation result for this access token
|
||||
local v = openidc_cache_get("introspection", access_token)
|
||||
if not v then
|
||||
|
||||
-- do the verification first time
|
||||
local jwt = require "resty.jwt"
|
||||
|
||||
-- No secret given try getting it from the jwks endpoint
|
||||
if not opts.secret and opts.discovery then
|
||||
ngx.log(ngx.DEBUG, "bearer_jwt_verify using discovery.")
|
||||
if type(opts.discovery) == "string" then
|
||||
opts.discovery, err = openidc_discover(opts.discovery, opts.ssl_verify)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
-- We decode the token twice, could be saved
|
||||
local jwt_obj = jwt:load_jwt(access_token, nil)
|
||||
|
||||
if not jwt_obj.valid then
|
||||
return nil, "invalid jwt"
|
||||
end
|
||||
|
||||
opts.secret, err = pem_from_jwk(opts, jwt_obj.header.kid)
|
||||
|
||||
if opts.secret == nil then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
json = jwt:verify(opts.secret, access_token, ...)
|
||||
|
||||
ngx.log(ngx.DEBUG, "jwt: ", cjson.encode(json))
|
||||
|
||||
-- cache the results
|
||||
if json and json.valid == true and json.verified == true then
|
||||
json = json.payload
|
||||
openidc_cache_set("introspection", access_token, cjson.encode(json), json.exp - ngx.time())
|
||||
else
|
||||
err = "invalid token: ".. json.reason
|
||||
end
|
||||
|
||||
else
|
||||
-- decode from the cache
|
||||
json = cjson.decode(v)
|
||||
end
|
||||
|
||||
-- check the token expiry
|
||||
if json then
|
||||
if json.exp and json.exp < ngx.time() then
|
||||
ngx.log(ngx.ERR, "token expired: json.exp=", json.exp, ", ngx.time()=", ngx.time())
|
||||
err = "JWT expired"
|
||||
end
|
||||
end
|
||||
|
||||
return json, err
|
||||
end
|
||||
|
||||
function openidc.bearer_jwt_verify(opts, ...)
|
||||
local err
|
||||
local json
|
||||
|
||||
-- get the access token from the request
|
||||
local access_token, err = openidc_get_bearer_access_token(opts)
|
||||
if access_token == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
ngx.log(ngx.DEBUG, "access_token: ", access_token)
|
||||
|
||||
json, err = openidc.jwt_verify(access_token, opts, ...)
|
||||
return json, err, access_token
|
||||
end
|
||||
|
||||
return openidc
|
|
@ -0,0 +1,60 @@
|
|||
-- Lua reference for nginx: https://github.com/openresty/lua-nginx-module
|
||||
-- Lua reference for openidc: https://github.com/pingidentity/lua-resty-openidc
|
||||
local oidc = require("resty.openidc")
|
||||
local cjson = require( "cjson" )
|
||||
|
||||
-- Load config
|
||||
local f, e = loadfile(ngx.var.config_loader)
|
||||
if f == nil then
|
||||
ngx.log(ngx.ERR, "can't initialize loadfile: "..e)
|
||||
end
|
||||
ok, e = pcall(f)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "can't load configuration: "..e)
|
||||
end
|
||||
|
||||
-- Authenticate with lua-resty-openidc if necessary (this will return quickly if no authentication is necessary)
|
||||
local res, err, url, session = oidc.authenticate(opts)
|
||||
|
||||
-- Check if authentication succeeded, otherwise kick the user out
|
||||
if err then
|
||||
if session ~= nil then
|
||||
session:destroy()
|
||||
end
|
||||
ngx.redirect(opts.logout_path)
|
||||
else
|
||||
ngx.log(ngx.ERR, "no error was returned but session is not set. Are you using lua-resty-openidc>=1.3.2?")
|
||||
end
|
||||
|
||||
-- Access control: only allow specific users in (this is optional, without it all authenticated users are allowed in)
|
||||
-- (TODO: add example)
|
||||
|
||||
-- Set headers with user info and OIDC claims for the underlaying web application to use (this is optional)
|
||||
-- These header names are voluntarily similar to Apaches mod_auth_openidc, but may of course be modified
|
||||
ngx.req.set_header("REMOTE_USER", session.data.user.email)
|
||||
ngx.req.set_header("X-Forwarded-User", session.data.user.email)
|
||||
ngx.req.set_header("OIDC_CLAIM_ACCESS_TOKEN", session.data.access_token)
|
||||
ngx.req.set_header("OIDC_CLAIM_ID_TOKEN", session.data.enc_id_token)
|
||||
ngx.req.set_header("via",session.data.user.email)
|
||||
|
||||
local function build_headers(t, name)
|
||||
for k,v in pairs(t) do
|
||||
-- unpack tables
|
||||
if type(v) == "table" then
|
||||
local j = cjson.encode(v)
|
||||
ngx.req.set_header("OIDC_CLAIM_"..name..k, j)
|
||||
else
|
||||
ngx.req.set_header("OIDC_CLAIM_"..name..k, tostring(v))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
build_headers(session.data.id_token, "ID_TOKEN_")
|
||||
build_headers(session.data.user, "USER_PROFILE_")
|
||||
|
||||
-- Flat groups, useful for some RP's that won't read JSON
|
||||
local gprs = ""
|
||||
for k,v in pairs(session.data.user.groups) do
|
||||
grps = grps and grps.."|"..v or v
|
||||
end
|
||||
ngx.req.set_header("X-Forwarded-Groups", grps)
|
Загрузка…
Ссылка в новой задаче