Skip to content
This repository has been archived by the owner on Apr 7, 2020. It is now read-only.

Commit

Permalink
Adding support for Google Suite Directory Groups
Browse files Browse the repository at this point in the history
Google Suite Directory Groups can be fetched for the user.
A white list of group emails can be defined grant access.
The list of groups can be passed to an NGINX variable which can be
passed to the applications for fine grained access control.
A service account with Google Suite Domain-Wide Delegation of Authority
is required to access Google Directory API.

Added the possibility to extract also user email and name from the Oauth
profile data and pass them on as NGINX variables.
  • Loading branch information
TiagoTT committed Apr 16, 2018
1 parent 2eead8a commit 6a6edbc
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 13 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ variables are:
returned from Google (portion left of '@' in email).
- **$ngo_email_as_user** If set and `$ngo_user` is defined, username
returned will be full email address.
- **$ngo_email** Optional, boolean. If set to true, it will be populated with
the OAuth email returned from Google.
- **$ngo_name** Optional, boolean. If set to true, it will be populated with
the OAuth name returned from Google.
- **$ngo_groups** Optional, boolean. If set to true, it will be populated with
the Google Directory Groups of which the user is a member, within the Google
Suite domain defined in variable **$ngo_groups_domain**.
Requires the definition of **$ngo_service_account_json_file**,
**$ngo_organization_admin_email** and **$ngo_groups_domain**.
- **$ngo_allowed_groups** Optional, space separated list of email addresses of
Google Directory Groups. If set, will be used for access control, so that
only members of the defined groups will be authorized.
Requires the definition of **$ngo_service_account_json_file**,
**$ngo_organization_admin_email** and **$ngo_groups_domain**.
- **$ngo_service_account_json_file** Optional, path to JSON credentials file of the Google Service Account which has been granted domain-wide-delegation of the Google Suite Domain.
Please follow the [official documentation](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to setup Google Suite Domain-Wide Delegation of Authority and define at least the following scopes:
```
https://www.googleapis.com/auth/admin.directory.user.readonly
https://www.googleapis.com/auth/admin.directory.group.readonly
https://www.googleapis.com/auth/admin.directory.group.member.readonly
```
- **$ngo_organization_admin_email** Optional, the email of a Google Suite
administrator account. The Service Account will impersonate this user to
access Google Directory API.
- **ngo_groups_domain** Optional, the domain of the Google Suite account where
groups are fetched from.

## Available endpoints

Expand All @@ -80,6 +106,8 @@ Endpoint that reports your OAuth token in a JSON object:
```json
{
"email": "[email protected]",
"name": "Foo Name",
"groups": "[email protected] [email protected]",
"token": "abc..xyz",
"expires": 1445455680
}
Expand All @@ -91,6 +119,8 @@ Endpoint that reports your OAuth token in text format:

```
email: [email protected]
name: Foo Name
groups: [email protected] [email protected]
token: abc..xyz
expires: 1445455680
```
Expand All @@ -100,7 +130,7 @@ expires: 1445455680
Endpoint that reports your OAuth token as `curl` arguments for header auth:

```
-H "OauthEmail: [email protected]" -H "OauthAccessToken: abc..xyz" -H "OauthExpires: 1445455680"
-H "OauthEmail: [email protected]" -H "OauthName: Foo Name" -H "OauthGroups: [email protected] [email protected]" -H "OauthAccessToken: abc..xyz" -H "OauthExpires: 1445455680"
```

You can add it to your `curl` command to make it work with OAuth.
Expand Down
156 changes: 144 additions & 12 deletions access.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ local blacklist = ngx.var.ngo_blacklist or ""
local secure_cookies = ngx.var.ngo_secure_cookies == "true" or false
local http_only_cookies = ngx.var.ngo_http_only_cookies == "true" or false
local set_user = ngx.var.ngo_user or false
local set_email = ngx.var.ngo_email or false
local set_name = ngx.var.ngo_name or false
local email_as_user = ngx.var.ngo_email_as_user == "true" or false
local sa_json_file = ngx.var.ngo_service_account_json_file or false
local org_admin_email = ngx.var.ngo_organization_admin_email or false
local set_groups = ngx.var.ngo_groups or false
local allowed_groups = ngx.var.ngo_allowed_groups or ""
local groups_domain = ngx.var.ngo_groups_domain or false

if whitelist:len() == 0 then
whitelist = nil
Expand All @@ -34,26 +41,32 @@ if blacklist:len() == 0 then
blacklist = nil
end

local function handle_token_uris(email, token, expires)
if allowed_groups:len() == 0 then
allowed_groups = nil
end

local function handle_token_uris(email, name, groups, token, expires)
if uri == "/_token.json" then
ngx.header["Content-type"] = "application/json"
ngx.header["Content-type"] = "application/json; charset=utf-8"
ngx.say(json.encode({
email = email,
name = name,
groups = groups,
token = token,
expires = expires,
}))
ngx.exit(ngx.OK)
end

if uri == "/_token.txt" then
ngx.header["Content-type"] = "text/plain"
ngx.say("email: " .. email .. "\n" .. "token: " .. token .. "\n" .. "expires: " .. expires .. "\n")
ngx.header["Content-type"] = "text/plain; charset=utf-8"
ngx.say("email: " .. email .. "\n" .. "name: " .. name .. "\n" .. "groups: " .. groups .. "\n" .. "token: " .. token .. "\n" .. "expires: " .. expires .. "\n")
ngx.exit(ngx.OK)
end

if uri == "/_token.curl" then
ngx.header["Content-type"] = "text/plain"
ngx.say("-H \"OauthEmail: " .. email .. "\" -H \"OauthAccessToken: " .. token .. "\" -H \"OauthExpires: " .. expires .. "\"\n")
ngx.header["Content-type"] = "text/plain; charset=utf-8"
ngx.say("-H \"OauthEmail: " .. email .. "\" -H \"OauthName: " .. name .. "\" -H \"OauthGroups: " .. groups .. "\" -H \"OauthAccessToken: " .. token .. "\" -H \"OauthExpires: " .. expires .. "\"\n")
ngx.exit(ngx.OK)
end
end
Expand All @@ -73,7 +86,8 @@ local function check_domain(email, whitelist_failed)
end
end

local function on_auth(email, token, expires)

local function on_auth(email, name, groups, token, expires)
if blacklist then
-- blacklisted user is always rejected
if string.find(" " .. blacklist .. " ", " " .. email .. " ", 1, true) then
Expand All @@ -92,6 +106,19 @@ local function on_auth(email, token, expires)
check_domain(email, false)
end

if allowed_groups then
local allow_group = false
for group in groups:gmatch("%S+") do
if string.find(" " .. allowed_groups .. " ", " " .. group .. " ", 1, true) then
allow_group = true
break
end
end
if not allow_group then
ngx.log(ngx.ERR, "none of the user groups (" .. groups .. ") are present in allowed_groups (" .. allowed_groups .. ")")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
end

if set_user then
if email_as_user then
Expand All @@ -101,7 +128,19 @@ local function on_auth(email, token, expires)
end
end

handle_token_uris(email, token, expires)
if set_email then
ngx.var.ngo_email = email
end

if set_name then
ngx.var.ngo_name = name
end

if set_groups then
ngx.var.ngo_groups = groups
end

handle_token_uris(email, name, groups, token, expires)
end

local function request_access_token(code)
Expand Down Expand Up @@ -134,6 +173,79 @@ local function request_access_token(code)
return json.decode(res.body)
end

local function base64url(text)
return ngx.encode_base64(text):gsub("+", "-"):gsub("/", "_")
end

local function request_groups(email)
if not (sa_json_file and org_admin_email and groups_domain) then
return ""
end

local digest = require("openssl.digest")
local pkey = require("openssl.pkey")

local json_file = io.open(sa_json_file, "r")
if json_file then
service_account = json.decode(json_file:read("*a"))
io.close(json_file)
else
ngx.log(ngx.ERR, "failed to open service account JSON file: " .. sa_json_file)
return ""
end

local now = os.time()
local header = '{"alg":"RS256","typ":"JWT"}'
local claims = '{"iss":"' .. service_account["client_email"] .. '","sub":"' .. org_admin_email .. '", "scope":"https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly https://www.googleapis.com/auth/admin.directory.group.member.readonly", "aud":"https://www.googleapis.com/oauth2/v4/token","exp":' .. now + 300 .. ', "iat":' .. now .. '}'
local signature = pkey.new(service_account["private_key"]):sign(digest.new("sha256"):update(base64url(header) .. "." .. base64url(claims)))
local assertion = base64url(header) .. "." .. base64url(claims) .. "." .. base64url(signature)

local request = http.new()
request:set_timeout(7000)
local res, err = request:request_uri("https://www.googleapis.com/oauth2/v4/token", {
method = "POST",
body = ngx.encode_args({
assertion = assertion,
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
}),
headers = {
["Content-type"] = "application/x-www-form-urlencoded"
},
ssl_verify = true,
})
if not res then
return nil, (err or "domain auth token request failed: " .. (err or "unknown reason"))
end
if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://www.googleapis.com/oauth2/v4/token: " .. res.body
end
access_token = json.decode(res.body)['access_token']

local request = http.new()
request:set_timeout(7000)
local res, err = request:request_uri("https://www.googleapis.com/admin/directory/v1/groups?domain=" .. groups_domain .. "&userKey=" .. email, {
headers = {
["Authorization"] = "Bearer " .. access_token,
},
ssl_verify = true,
})
if not res then
return nil, "auth info request failed: " .. (err or "unknown reason")
end

if res.status ~= 200 then
return nil, "received " .. res.status .. " from https://www.googleapis.com/admin/directory/v1/groups?domain=" .. groups_domain .. "&userKey=" .. email
end

local user_groups = json.decode(res.body)
local groups = ""
for i, group in ipairs(user_groups["groups"]) do
groups = groups .. " " .. group["email"]
end

return groups
end

local function request_profile(token)
local request = http.new()

Expand Down Expand Up @@ -161,6 +273,8 @@ local function is_authorized()

local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
local email = ngx.unescape_uri(ngx.var.cookie_OauthEmail or "")
local name = ngx.unescape_uri(ngx.var.cookie_OauthName or "")
local groups = ngx.unescape_uri(ngx.var.cookie_OauthGroups or "")
local token = ngx.unescape_uri(ngx.var.cookie_OauthAccessToken or "")

if expires == 0 and headers["oauthexpires"] then
Expand All @@ -171,14 +285,23 @@ local function is_authorized()
email = headers["oauthemail"]
end

if name:len() == 0 and headers["oauthname"] then
name = headers["oauthname"]
end

if groups:len() == 0 and headers["oauthgroups"] then
groups = headers["oauthgroups"]
end

if token:len() == 0 and headers["oauthaccesstoken"] then
token = headers["oauthaccesstoken"]
end

local expected_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires))
local expected_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. name .. groups .. expires))

if token == expected_token and expires and expires > ngx.time() - extra_validity then
on_auth(email, expected_token, expires)
on_auth(email, name, groups, expected_token, expires)
return true
else
return false
Expand All @@ -189,7 +312,7 @@ local function redirect_to_auth()
-- google seems to accept space separated domain list in the login_hint, so use this undocumented feature.
return ngx.redirect("https://accounts.google.com/o/oauth2/auth?" .. ngx.encode_args({
client_id = client_id,
scope = "email",
scope = "email profile",
response_type = "code",
redirect_uri = cb_url,
state = redirect_url,
Expand Down Expand Up @@ -229,12 +352,21 @@ local function authorize()
end

local email = profile["email"]
local user_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires))
local name = profile["name"]

local groups, groups_err = request_groups(email)
if not groups then
ngx.log(ngx.ERR, "got error during groups request: " .. groups_err)
groups = ""
end
local user_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. name .. groups .. expires))

on_auth(email, user_token, expires)
on_auth(email, name, groups, user_token, expires)

ngx.header["Set-Cookie"] = {
"OauthEmail=" .. ngx.escape_uri(email) .. cookie_tail,
"OauthName=" .. ngx.escape_uri(name) .. cookie_tail,
"OauthGroups=" .. ngx.escape_uri(groups) .. cookie_tail,
"OauthAccessToken=" .. ngx.escape_uri(user_token) .. cookie_tail,
"OauthExpires=" .. expires .. cookie_tail,
}
Expand Down

0 comments on commit 6a6edbc

Please sign in to comment.