integrate Azure AD to protect dashboard
This commit is contained in:
Zhiyuan Zheng 2017-08-10 18:25:46 +08:00 коммит произвёл GitHub
Родитель 72d058a49d
Коммит 9c5d4b5511
12 изменённых файлов: 1263 добавлений и 29 удалений

Просмотреть файл

@ -87,6 +87,38 @@
"metadata": {
"description": "Directory in archive."
}
},
"authenticationMode": {
"type": "string",
"defaultValue": "BasicAuth",
"allowedValues": [
"BasicAuth",
"AzureAD"
],
"metadata": {
"description": "User authentication mode."
}
},
"azureAdClientId": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD client ID(Application ID)"
}
},
"azureAdClientSecret": {
"type": "securestring",
"defaultValue": "",
"metadata": {
"description": "Azure AD client secret/key"
}
},
"tenant": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD tenant(e.g. contoso.onmicrosoft.com)"
}
}
},
"variables": {
@ -105,7 +137,7 @@
"publicIPAddressName": "controllerip",
"publicIPAddressType": "Dynamic",
"vmName": "controllervm",
"vmSize": "Standard_A1",
"vmSize": "Standard_D1_v2",
"virtualNetworkName": "controller-vnet",
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
@ -185,7 +217,7 @@
]
},
"protectedSettings": {
"commandToExecute": "[concat('bash run.sh -d ', variables('mastersEndpointDNSNamePrefix'), ' -l ', parameters('location'), ' -u ', parameters('linuxAdminUsername'), ' -p ', parameters('adminPassword'), ' -k ', variables('basedPrivateKey'), ' -a ', variables('registryName'), ' -b ', listCredentials(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), '2017-03-01').passwords[0].value, ' -s ', variables('dataStorageAccountName'), ' -c ', parameters('storageAccountSku'), ' -e ', parameters('archiveUrl'), ' -f ', parameters('directoryName'))]"
"commandToExecute": "[concat('bash run.sh -d ', variables('mastersEndpointDNSNamePrefix'), ' -l ', parameters('location'), ' -u ', parameters('linuxAdminUsername'), ' -p ', parameters('adminPassword'), ' -k ', variables('basedPrivateKey'), ' -a ', variables('registryName'), ' -b ', listCredentials(resourceId('Microsoft.ContainerRegistry/registries', variables('registryName')), '2017-03-01').passwords[0].value, ' -s ', variables('dataStorageAccountName'), ' -c ', parameters('storageAccountSku'), ' -e ', parameters('archiveUrl'), ' -f ', parameters('directoryName'), ' -g ', parameters('authenticationMode'), ' -h ', parameters('azureAdClientId'), ' -i ', parameters('azureAdClientSecret'), ' -t ', parameters('tenant'))]"
}
}
}

Просмотреть файл

@ -87,6 +87,38 @@
"metadata": {
"description": "Directory in archive."
}
},
"authenticationMode": {
"type": "string",
"defaultValue": "BasicAuth",
"allowedValues": [
"BasicAuth",
"AzureAD"
],
"metadata": {
"description": "User authentication mode."
}
},
"azureAdClientId": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD client ID(Application ID)"
}
},
"azureAdClientSecret": {
"type": "securestring",
"defaultValue": "",
"metadata": {
"description": "Azure AD client secret/key"
}
},
"tenant": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD tenant(e.g. contoso.onmicrosoft.com)"
}
}
},
"variables": {
@ -105,7 +137,7 @@
"publicIPAddressName": "controllerip",
"publicIPAddressType": "Dynamic",
"vmName": "controllervm",
"vmSize": "Standard_A1",
"vmSize": "Standard_D1_v2",
"virtualNetworkName": "controller-vnet",
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
@ -185,7 +217,7 @@
]
},
"protectedSettings": {
"commandToExecute": "[concat('bash run.sh -d ', variables('mastersEndpointDNSNamePrefix'), ' -l ', parameters('location'), ' -u ', parameters('linuxAdminUsername'), ' -p ', parameters('adminPassword'), ' -k ', variables('basedPrivateKey'), ' -r ', parameters('registryUrl'), ' -s ', variables('dataStorageAccountName'), ' -c ', parameters('storageAccountSku'), ' -e ', parameters('archiveUrl'), ' -f ', parameters('directoryName'))]"
"commandToExecute": "[concat('bash run.sh -d ', variables('mastersEndpointDNSNamePrefix'), ' -l ', parameters('location'), ' -u ', parameters('linuxAdminUsername'), ' -p ', parameters('adminPassword'), ' -k ', variables('basedPrivateKey'), ' -r ', parameters('registryUrl'), ' -s ', variables('dataStorageAccountName'), ' -c ', parameters('storageAccountSku'), ' -e ', parameters('archiveUrl'), ' -f ', parameters('directoryName'), ' -g ', parameters('authenticationMode'), ' -h ', parameters('azureAdClientId'), ' -i ', parameters('azureAdClientSecret'), ' -t ', parameters('tenant'))]"
}
}
}

Просмотреть файл

@ -87,6 +87,38 @@
"metadata": {
"description": "Directory in archive."
}
},
"authenticationMode": {
"type": "string",
"defaultValue": "BasicAuth",
"allowedValues": [
"BasicAuth",
"AzureAD"
],
"metadata": {
"description": "User authentication mode."
}
},
"azureAdClientId": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD client ID(Application ID)"
}
},
"azureAdClientSecret": {
"type": "securestring",
"defaultValue": "",
"metadata": {
"description": "Azure AD client secret/key"
}
},
"tenant": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD tenant(e.g. contoso.onmicrosoft.com)"
}
}
},
"variables": {
@ -284,6 +316,18 @@
},
"directoryName": {
"value": "[parameters('directoryName')]"
},
"authenticationMode": {
"value": "[parameters('authenticationMode')]"
},
"azureAdClientId": {
"value": "[parameters('azureAdClientId')]"
},
"azureAdClientSecret": {
"value": "[parameters('azureAdClientSecret')]"
},
"tenant": {
"value": "[parameters('tenant')]"
}
}
}
@ -299,4 +343,4 @@
"value": "[concat('ssh ', variables('adminUsername'), '@', reference(variables('publicIPAddressName')).dnsSettings.fqdn)]"
}
}
}
}

Просмотреть файл

@ -97,6 +97,59 @@
}
]
}
},
{
"name": "authenticationMode",
"type": "Microsoft.Common.OptionsGroup",
"label": "Authentication Mode",
"defaultValue": "BasicAuth",
"toolTip": "Basic authentication for Kubernetes dashboard and Kibana",
"constraints": {
"allowedValues": [
{
"label": "Basic Authentication",
"value": "BasicAuth"
},
{
"label": "Azure Active Directory",
"value": "AzureAD"
}
]
},
"visible": true
},
{
"name": "azureAdClientId",
"type": "Microsoft.Common.TextBox",
"label": "Azure AD client ID",
"defaultValue": "",
"toolTip": "Azure AD client ID(Application ID)",
"constraints": {
"required": false
},
"visible": "[equals('AzureAD', steps('commonSettings').authenticationMode)]"
},
{
"name": "azureAdClientSecret",
"type": "Microsoft.Common.TextBox",
"label": "Azure AD client secret",
"defaultValue": "",
"toolTip": "Azure AD client secret/key",
"constraints": {
"required": false
},
"visible": "[equals('AzureAD', steps('commonSettings').authenticationMode)]"
},
{
"name": "tenant",
"type": "Microsoft.Common.TextBox",
"label": "Azure AD tenant",
"defaultValue": "",
"toolTip": "Azure AD tenant(e.g. contoso.onmicrosoft.com)",
"constraints": {
"required": false
},
"visible": "[equals('AzureAD', steps('commonSettings').authenticationMode)]"
}
]
},
@ -683,6 +736,10 @@
"dnsNamePrefix": "[steps('commonSettings').dnsNamePrefix]",
"registryUrl": "[steps('commonSettings').registryUrl]",
"storageAccountSku": "[steps('commonSettings').storageAccountSku]",
"authenticationMode": "[steps('commonSettings').authenticationMode]",
"azureAdClientId": "[steps('commonSettings').azureAdClientId]",
"azureAdClientSecret": "[steps('commonSettings').azureAdClientSecret]",
"tenant": "[steps('commonSettings').tenant]",
"agentCount": "[steps('k8sSettings').agentCount]",
"agentVMSize": "[steps('k8sSettings').agentVMSize]",
"linuxAdminUsername": "[basics('userName')]",
@ -698,4 +755,4 @@
"directoryName": "elk-acs-kubernetes-rc/"
}
}
}
}

Просмотреть файл

@ -187,6 +187,38 @@
"metadata": {
"description": "Directory in archive."
}
},
"authenticationMode": {
"type": "string",
"defaultValue": "BasicAuth",
"allowedValues": [
"BasicAuth",
"AzureAD"
],
"metadata": {
"description": "User authentication mode."
}
},
"azureAdClientId": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD client ID(Application ID)"
}
},
"azureAdClientSecret": {
"type": "securestring",
"defaultValue": "",
"metadata": {
"description": "Azure AD client secret/key"
}
},
"tenant": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Azure AD tenant(e.g. contoso.onmicrosoft.com)"
}
}
},
"variables": {
@ -355,6 +387,18 @@
},
"directoryName": {
"value": "[parameters('directoryName')]"
},
"authenticationMode": {
"value": "[parameters('authenticationMode')]"
},
"azureAdClientId": {
"value": "[parameters('azureAdClientId')]"
},
"azureAdClientSecret": {
"value": "[parameters('azureAdClientSecret')]"
},
"tenant": {
"value": "[parameters('tenant')]"
}
}
}
@ -370,4 +414,4 @@
"value": "[reference('controllerNode').outputs.sshCommand.value]"
}
}
}
}

Просмотреть файл

@ -46,6 +46,18 @@
},
"directoryName": {
"value": "elk-acs-kubernetes-rc/"
},
"authenticationMode": {
"value": "BasicAuth"
},
"azureAdClientId": {
"value": "<azure-ad-client-id>"
},
"azureAdClientSecret": {
"value": "<azure-ad-client-secret>"
},
"tenant": {
"value": "<tenant-name>"
}
}
}

Просмотреть файл

@ -34,4 +34,10 @@ This repository contains tools and helm charts to help deploy the [ELK stack](ht
"value": "acs-dns-abcdefcontrol.westus.cloudapp.azure.com"
},
...
```
```
## License
This project is under MIT license.
```config/openidc.lua``` is derived from [https://github.com/pingidentity/lua-resty-openidc](https://github.com/pingidentity/lua-resty-openidc) with some modifications to satisfy requirements and this file (```config/openidc.lua```) is under Apache 2.0 license.

19
config/nginx-basic.conf Normal file
Просмотреть файл

@ -0,0 +1,19 @@
events {
worker_connections 1024;
}
http {
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass http://localhost:8080;
auth_basic "Restrict Access";
auth_basic_user_file /usr/local/openresty/nginx/conf/.htpasswd;
}
}
}

91
config/nginx-openid.conf Normal file
Просмотреть файл

@ -0,0 +1,91 @@
events {
worker_connections 1024;
}
http {
lua_package_path '~/lua/?.lua;;';
resolver 8.8.8.8;
lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 5;
# cache for discovery metadata documents
lua_shared_dict discovery 1m;
# NB: if you have "lua_code_cache off;", use:
# set $session_secret xxxxxxxxxxxxxxxxxxx;
# see: https://github.com/bungle/lua-resty-session#notes-about-turning-lua-code-cache-off
server {
listen 80;
location / {
access_by_lua '
local opts = {
-- the full redirect URI must be protected by this script and becomes:
-- ngx.var.scheme.."://"..ngx.var.http_host..opts.redirect_uri_path
-- unless the scheme is overridden using opts.redirect_uri_scheme or an X-Forwarded-Proto header in the incoming request
redirect_uri_path = "/callback",
discovery = "https://login.microsoftonline.com/${TENANT}/.well-known/openid-configuration",
client_id = "${CLIENT_ID}",
client_secret = "${CLIENT_SECRET}",
-- default iat_slack is 120 which is insufficient.
iat_slack = 600,
-- if graph API will be called, uncomment next line.
-- authorization_params = { resource="https://graph.windows.net" },
-- scope = "openid email profile",
-- Refresh the user id_token after 900 seconds without requiring re-authentication
-- refresh_session_interval = 900,
-- redirect_uri_scheme = "https",
-- logout_path = "/logout",
-- redirect_after_logout_uri = "/",
-- redirect_after_logout_with_id_token_hint = true,
-- token_endpoint_auth_method = ["client_secret_basic"|"client_secret_post"],
-- ssl_verify = "no"
-- access_token_expires_in = 3600
-- Default lifetime in seconds of the access_token if no expires_in attribute is present in the token
-- endpoint response.
-- This plugin will silently renew the access_token once it is expired if refreshToken scope is present.
-- access_token_expires_leeway = 0
-- Expiration leeway for access_token renewal.
-- If this is set, renewal will happen access_token_expires_leeway seconds before the token expiration.
-- This avoids errors in case the access_token just expires when arriving to the OAuth Resoource Server.
-- force_reauthorize = false
-- when force_reauthorize is set to true the authorization flow will be executed even if a token has been cached already
}
-- 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
-- at this point res is a Lua table with 3 keys:
-- id_token : a Lua table with the claims from the id_token (required)
-- access_token: the access token (optional)
-- user : a Lua table with the claims returned from the user info endpoint (optional)
--if res.id_token.hd ~= "pingidentity.com" then
-- ngx.exit(ngx.HTTP_FORBIDDEN)
--end
--if res.user.email ~= "hans.zandbelt@zmartzone.eu" then
-- ngx.exit(ngx.HTTP_FORBIDDEN)
--end
-- set headers with user info: this will overwrite any existing headers
-- but also scrub(!) them in case no value is provided in the token
ngx.req.set_header("X-USER", res.id_token.sub)
';
proxy_pass http://localhost:8080;
}
}
}

Просмотреть файл

@ -1,12 +0,0 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass http://localhost:8080;
auth_basic "Restrict Access";
auth_basic_user_file /etc/nginx/.htpasswd;
}
}

872
config/openidc.lua Normal file
Просмотреть файл

@ -0,0 +1,872 @@
--[[
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.4.0"
}
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
if not ngx.var.http_host then
-- possibly HTTP 1.0 and no Host header
ngx.exit(ngx.HTTP_BAD_REQUEST)
end
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
ngx.redirect(session.data.original_url)
return nil, nil, 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, "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
openidc_cache_set("discovery", url, cjson.encode(json), 24 * 60 * 60)
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, "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)
local session_token = session.data.enc_id_token
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 and opts.redirect_after_logout_with_id_token_hint then
return ngx.redirect(opts.redirect_after_logout_uri.."&id_token_hint="..session_token)
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)
-- retrieve token endpoint URL from discovery endpoint if necessary
if type(opts.discovery) == "string" then
opts.discovery, err = openidc_discover(opts.discovery, opts.ssl_verify)
if err then
return nil, err
end
end
-- set the authentication method for the token endpoint
opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts)
-- 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)
end
-- see if this is a request to logout
if path == (opts.logout_path and opts.logout_path or "/logout") then
openidc_logout(opts, session)
return nil, nil, target_url, 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 or opts.force_reauthorize then
if unauth_action == "pass" then
return
nil,
err,
target_url,
session
end
openidc_authorize(opts, session, target_url)
return nil, nil, 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"
openidc_authorize(opts, session, target_url)
return nil, nil, 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 "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.introspection_expiry_claim or "exp"
if json.active or json[expiry_claim] then
local ttl = json[expiry_claim]
if expiry_claim == "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2
ttl = ttl - ngx.time()
end
ngx.log(ngx.DEBUG, "cache token ttl: "..ttl)
openidc_cache_set("introspection", access_token, cjson.encode(json), ttl)
else
err = "invalid token"
end
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
local slack=opts.iat_slack and opts.iat_slack or 120
-- check the token expiry
if json then
if json.exp and json.exp + slack < 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

Просмотреть файл

@ -4,7 +4,7 @@ set -e
echo $@
while getopts ':d:l:u:p:k:r:a:b:s:c:e:f:' arg
while getopts ':d:l:u:p:k:r:a:b:s:c:e:f:g:h:i:t:' arg
do
case ${arg} in
d) masterDns=${OPTARG};;
@ -19,6 +19,10 @@ do
c) storageAccountSku=${OPTARG};;
e) repositoryUrl=${OPTARG};;
f) directoryName=${OPTARG};;
g) authenticationMode=${OPTARG};; # "AzureAD" or "BasicAuth"
h) clientId=${OPTARG};;
i) clientSecret=${OPTARG};;
t) tenant=${OPTARG};;
esac
done
@ -64,19 +68,41 @@ if [ -z ${repositoryUrl} ]; then
exit 1
fi
if [ -z ${authenticationMode} ]; then
authenticationMode = 'BasicAuth'
fi
if [ "${authenticationMode}" = "AzureAD" ]; then
if [ -z ${clientId} ]; then
echo 'Client ID is required in Azure AD mode' >&2
exit 1
fi
if [ -z ${clientSecret} ]; then
echo 'Client secret is required in Azure AD mode' >&2
exit 1
fi
if [ -z ${tenant} ]; then
echo 'Tenant is required in Azure AD mode' >&2
exit 1
fi
fi
privateKeyFile='private_key'
masterUrl=${masterDns}.${resourceLocation}.cloudapp.azure.com
export KUBECONFIG=/root/.kube/config
export HOME=/root
# prerequisites, e.g. docker, nginx
# prerequisites, e.g. docker, openresty
sudo apt-get -y install software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
sudo apt-get update
apt-cache policy docker-ce
sudo apt-get install -y unzip docker-ce nginx apache2-utils
sudo apt-get install -y unzip docker-ce openresty apache2-utils
# install kubectl
cd /tmp
@ -98,7 +124,7 @@ helm init
# make sure helm installed
until [ $(kubectl get pods -n kube-system -l app=helm,name=tiller -o jsonpath="{.items[0].status.containerStatuses[0].ready}") = "true" ]; do
sleep 2
sleep 2
done
# download templates
@ -106,11 +132,22 @@ curl -L ${repositoryUrl} -o template.zip
unzip -o template.zip -d template
# expose kubectl proxy
cd template/${directoryName}
echo ${masterPassword} | htpasswd -c -i /etc/nginx/.htpasswd ${masterUsername}
cp config/nginx-site.conf /etc/nginx/sites-available/default
nohup kubectl proxy --port=8080 &
systemctl reload nginx
cd template/${directoryName}
if [ "${authenticationMode}" = "BasicAuth" ]; then
echo ${masterPassword} | htpasswd -c -i /usr/local/openresty/nginx/conf/.htpasswd ${masterUsername}
cp config/nginx-basic.conf /usr/local/openresty/nginx/conf/nginx.conf
systemctl reload openresty
else
opm get pintsized/lua-resty-http bungle/lua-resty-session
cp config/openidc.lua /usr/local/openresty/lualib/resty/openidc.lua
export TENANT=${tenant}
export CLIENT_ID=${clientId}
export CLIENT_SECRET=${clientSecret}
cat config/nginx-openid.conf | envsubst > /usr/local/openresty/nginx/conf/nginx.conf
systemctl reload openresty
fi
# push image
cd docker