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

Commit 311d5c9

Browse files
committed
Adding support for Google Suite Directory Groups
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.
1 parent 381845f commit 311d5c9

File tree

2 files changed

+175
-13
lines changed

2 files changed

+175
-13
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,32 @@ variables are:
6565
returned from Google (portion left of '@' in email).
6666
- **$ngo_email_as_user** If set and `$ngo_user` is defined, username
6767
returned will be full email address.
68+
- **$ngo_email** Optional, boolean. If set to true, it will be populated with
69+
the OAuth email returned from Google.
70+
- **$ngo_name** Optional, boolean. If set to true, it will be populated with
71+
the OAuth name returned from Google.
72+
- **$ngo_groups** Optional, boolean. If set to true, it will be populated with
73+
the Google Directory Groups of which the user is a member, within the Google
74+
Suite domain defined in variable **$ngo_groups_domain**.
75+
Requires the definition of **$ngo_service_account_json_file**,
76+
**$ngo_organization_admin_email** and **$ngo_groups_domain**.
77+
- **$ngo_allowed_groups** Optional, space separated list of email addresses of
78+
Google Directory Groups. If set, will be used for access control, so that
79+
only members of the defined groups will be authorized.
80+
Requires the definition of **$ngo_service_account_json_file**,
81+
**$ngo_organization_admin_email** and **$ngo_groups_domain**.
82+
- **$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.
83+
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:
84+
```
85+
https://www.googleapis.com/auth/admin.directory.user.readonly
86+
https://www.googleapis.com/auth/admin.directory.group.readonly
87+
https://www.googleapis.com/auth/admin.directory.group.member.readonly
88+
```
89+
- **$ngo_organization_admin_email** Optional, the email of a Google Suite
90+
administrator account. The Service Account will impersonate this user to
91+
access Google Directory API.
92+
- **ngo_groups_domain** Optional, the domain of the Google Suite account where
93+
groups are fetched from.
6894

6995
## Available endpoints
7096

@@ -80,6 +106,8 @@ Endpoint that reports your OAuth token in a JSON object:
80106
```json
81107
{
82108
"email": "[email protected]",
109+
"name": "Foo Name",
110+
83111
"token": "abc..xyz",
84112
"expires": 1445455680
85113
}
@@ -91,6 +119,8 @@ Endpoint that reports your OAuth token in text format:
91119

92120
```
93121
122+
name: Foo Name
123+
94124
token: abc..xyz
95125
expires: 1445455680
96126
```
@@ -100,7 +130,7 @@ expires: 1445455680
100130
Endpoint that reports your OAuth token as `curl` arguments for header auth:
101131

102132
```
103-
-H "OauthEmail: [email protected]" -H "OauthAccessToken: abc..xyz" -H "OauthExpires: 1445455680"
133+
-H "OauthEmail: [email protected]" -H "OauthName: Foo Name" -H "OauthGroups: [email protected] [email protected]" -H "OauthAccessToken: abc..xyz" -H "OauthExpires: 1445455680"
104134
```
105135

106136
You can add it to your `curl` command to make it work with OAuth.

access.lua

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ local blacklist = ngx.var.ngo_blacklist or ""
2424
local secure_cookies = ngx.var.ngo_secure_cookies == "true" or false
2525
local http_only_cookies = ngx.var.ngo_http_only_cookies == "true" or false
2626
local set_user = ngx.var.ngo_user or false
27+
local set_email = ngx.var.ngo_email or false
28+
local set_name = ngx.var.ngo_name or false
2729
local email_as_user = ngx.var.ngo_email_as_user == "true" or false
30+
local sa_json_file = ngx.var.ngo_service_account_json_file or false
31+
local org_admin_email = ngx.var.ngo_organization_admin_email or false
32+
local set_groups = ngx.var.ngo_groups or false
33+
local allowed_groups = ngx.var.ngo_allowed_groups or ""
34+
local groups_domain = ngx.var.ngo_groups_domain or false
2835

2936
if whitelist:len() == 0 then
3037
whitelist = nil
@@ -34,32 +41,38 @@ if blacklist:len() == 0 then
3441
blacklist = nil
3542
end
3643

37-
local function handle_token_uris(email, token, expires)
44+
if allowed_groups:len() == 0 then
45+
allowed_groups = nil
46+
end
47+
48+
local function handle_token_uris(email, name, groups, token, expires)
3849
if uri == "/_token.json" then
39-
ngx.header["Content-type"] = "application/json"
50+
ngx.header["Content-type"] = "application/json; charset=utf-8"
4051
ngx.say(json.encode({
4152
email = email,
53+
name = name,
54+
groups = groups,
4255
token = token,
4356
expires = expires,
4457
}))
4558
ngx.exit(ngx.OK)
4659
end
4760

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

5467
if uri == "/_token.curl" then
55-
ngx.header["Content-type"] = "text/plain"
56-
ngx.say("-H \"OauthEmail: " .. email .. "\" -H \"OauthAccessToken: " .. token .. "\" -H \"OauthExpires: " .. expires .. "\"\n")
68+
ngx.header["Content-type"] = "text/plain; charset=utf-8"
69+
ngx.say("-H \"OauthEmail: " .. email .. "\" -H \"OauthName: " .. name .. "\" -H \"OauthGroups: " .. groups .. "\" -H \"OauthAccessToken: " .. token .. "\" -H \"OauthExpires: " .. expires .. "\"\n")
5770
ngx.exit(ngx.OK)
5871
end
5972
end
6073

6174

62-
local function on_auth(email, token, expires)
75+
local function on_auth(email, name, groups, token, expires)
6376
local oauth_domain = email:match("[^@]+@(.+)")
6477

6578
if not (whitelist or blacklist) then
@@ -85,6 +98,20 @@ local function on_auth(email, token, expires)
8598
end
8699
end
87100

101+
if allowed_groups then
102+
local allow_group = false
103+
for group in groups:gmatch("%S+") do
104+
if string.find(" " .. allowed_groups .. " ", " " .. group .. " ", 1, true) then
105+
allow_group = true
106+
break
107+
end
108+
end
109+
if not allow_group then
110+
ngx.log(ngx.ERR, "none of the user groups (" .. groups .. ") are present in allowed_groups (" .. allowed_groups .. ")")
111+
return ngx.exit(ngx.HTTP_FORBIDDEN)
112+
end
113+
end
114+
88115
if set_user then
89116
if email_as_user then
90117
ngx.var.ngo_user = email
@@ -93,7 +120,19 @@ local function on_auth(email, token, expires)
93120
end
94121
end
95122

96-
handle_token_uris(email, token, expires)
123+
if set_email then
124+
ngx.var.ngo_email = email
125+
end
126+
127+
if set_name then
128+
ngx.var.ngo_name = name
129+
end
130+
131+
if set_groups then
132+
ngx.var.ngo_groups = groups
133+
end
134+
135+
handle_token_uris(email, name, groups, token, expires)
97136
end
98137

99138
local function request_access_token(code)
@@ -126,6 +165,79 @@ local function request_access_token(code)
126165
return json.decode(res.body)
127166
end
128167

168+
local function base64url(text)
169+
return ngx.encode_base64(text):gsub("+", "-"):gsub("/", "_")
170+
end
171+
172+
local function request_groups(email)
173+
if not (sa_json_file and org_admin_email and groups_domain) then
174+
return ""
175+
end
176+
177+
local digest = require("openssl.digest")
178+
local pkey = require("openssl.pkey")
179+
180+
local json_file = io.open(sa_json_file, "r")
181+
if json_file then
182+
service_account = json.decode(json_file:read("*a"))
183+
io.close(json_file)
184+
else
185+
ngx.log(ngx.ERR, "failed to open service account JSON file: " .. sa_json_file)
186+
return ""
187+
end
188+
189+
local now = os.time()
190+
local header = '{"alg":"RS256","typ":"JWT"}'
191+
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 .. '}'
192+
local signature = pkey.new(service_account["private_key"]):sign(digest.new("sha256"):update(base64url(header) .. "." .. base64url(claims)))
193+
local assertion = base64url(header) .. "." .. base64url(claims) .. "." .. base64url(signature)
194+
195+
local request = http.new()
196+
request:set_timeout(7000)
197+
local res, err = request:request_uri("https://www.googleapis.com/oauth2/v4/token", {
198+
method = "POST",
199+
body = ngx.encode_args({
200+
assertion = assertion,
201+
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
202+
}),
203+
headers = {
204+
["Content-type"] = "application/x-www-form-urlencoded"
205+
},
206+
ssl_verify = true,
207+
})
208+
if not res then
209+
return nil, (err or "domain auth token request failed: " .. (err or "unknown reason"))
210+
end
211+
if res.status ~= 200 then
212+
return nil, "received " .. res.status .. " from https://www.googleapis.com/oauth2/v4/token: " .. res.body
213+
end
214+
access_token = json.decode(res.body)['access_token']
215+
216+
local request = http.new()
217+
request:set_timeout(7000)
218+
local res, err = request:request_uri("https://www.googleapis.com/admin/directory/v1/groups?domain=" .. groups_domain .. "&userKey=" .. email, {
219+
headers = {
220+
["Authorization"] = "Bearer " .. access_token,
221+
},
222+
ssl_verify = true,
223+
})
224+
if not res then
225+
return nil, "auth info request failed: " .. (err or "unknown reason")
226+
end
227+
228+
if res.status ~= 200 then
229+
return nil, "received " .. res.status .. " from https://www.googleapis.com/admin/directory/v1/groups?domain=" .. groups_domain .. "&userKey=" .. email
230+
end
231+
232+
local user_groups = json.decode(res.body)
233+
local groups = ""
234+
for i, group in ipairs(user_groups["groups"]) do
235+
groups = groups .. " " .. group["email"]
236+
end
237+
238+
return groups
239+
end
240+
129241
local function request_profile(token)
130242
local request = http.new()
131243

@@ -153,6 +265,8 @@ local function is_authorized()
153265

154266
local expires = tonumber(ngx.var.cookie_OauthExpires) or 0
155267
local email = ngx.unescape_uri(ngx.var.cookie_OauthEmail or "")
268+
local name = ngx.unescape_uri(ngx.var.cookie_OauthName or "")
269+
local groups = ngx.unescape_uri(ngx.var.cookie_OauthGroups or "")
156270
local token = ngx.unescape_uri(ngx.var.cookie_OauthAccessToken or "")
157271

158272
if expires == 0 and headers["oauthexpires"] then
@@ -163,14 +277,23 @@ local function is_authorized()
163277
email = headers["oauthemail"]
164278
end
165279

280+
if name:len() == 0 and headers["oauthname"] then
281+
name = headers["oauthname"]
282+
end
283+
284+
if groups:len() == 0 and headers["oauthgroups"] then
285+
groups = headers["oauthgroups"]
286+
end
287+
166288
if token:len() == 0 and headers["oauthaccesstoken"] then
167289
token = headers["oauthaccesstoken"]
168290
end
169291

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

172295
if token == expected_token and expires and expires > ngx.time() - extra_validity then
173-
on_auth(email, expected_token, expires)
296+
on_auth(email, name, groups, expected_token, expires)
174297
return true
175298
else
176299
return false
@@ -181,7 +304,7 @@ local function redirect_to_auth()
181304
-- google seems to accept space separated domain list in the login_hint, so use this undocumented feature.
182305
return ngx.redirect("https://accounts.google.com/o/oauth2/auth?" .. ngx.encode_args({
183306
client_id = client_id,
184-
scope = "email",
307+
scope = "email profile",
185308
response_type = "code",
186309
redirect_uri = cb_url,
187310
state = redirect_url,
@@ -221,12 +344,21 @@ local function authorize()
221344
end
222345

223346
local email = profile["email"]
224-
local user_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires))
347+
local name = profile["name"]
348+
349+
local groups, groups_err = request_groups(email)
350+
if not groups then
351+
ngx.log(ngx.ERR, "got error during groups request: " .. groups_err)
352+
groups = ""
353+
end
354+
local user_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. name .. groups .. expires))
225355

226-
on_auth(email, user_token, expires)
356+
on_auth(email, name, groups, user_token, expires)
227357

228358
ngx.header["Set-Cookie"] = {
229359
"OauthEmail=" .. ngx.escape_uri(email) .. cookie_tail,
360+
"OauthName=" .. ngx.escape_uri(name) .. cookie_tail,
361+
"OauthGroups=" .. ngx.escape_uri(groups) .. cookie_tail,
230362
"OauthAccessToken=" .. ngx.escape_uri(user_token) .. cookie_tail,
231363
"OauthExpires=" .. expires .. cookie_tail,
232364
}

0 commit comments

Comments
 (0)