Skip to content

Commit

Permalink
Merge pull request #4 from eschwim/master
Browse files Browse the repository at this point in the history
A few usability changes and security fixes
  • Loading branch information
awestendorf committed Mar 20, 2015
2 parents b987532 + c4ef146 commit 3c34b27
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 24 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ server {
set $ngo_client_id "abc-def.apps.googleusercontent.com";
set $ngo_client_secret "abcdefg-123-xyz";
set $ngo_token_secret "a very long randomish string";
set $ngo_secure_cookies "true";
access_by_lua_file "/etc/nginx/nginx-google-oauth/access.lua";
}
Expand All @@ -61,6 +62,7 @@ variables are:

- **$ngo_client_id** This is the client id key
- **$ngo_client_secret** This is the client secret
- **$ngo_token_secret** The key used to encrypt the session token stored in the user cookie. Should be long & unguessable.
- **$ngo_domain** The domain to use for validating users when not using white- or blacklists
- **$ngo_whitelist** Optional list of authorized email addresses
- **$ngo_blacklist** Optional list of unauthorized email addresses
Expand All @@ -70,6 +72,8 @@ variables are:
- **$ngo_debug** If defined, will enable debug logging through nginx error logger
- **$ngo_secure_cookies** If defined, will ensure that cookies can only be transfered over a secure connection
- **$ngo_css** An optional stylesheet to replace the default stylesheet when using the body_filter
- **$ngo_user** If set, will be populated with the OAuth username 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

## Configuring OAuth Access

Expand Down Expand Up @@ -118,6 +122,7 @@ server {
set $ngo_client_id 'abc-def.apps.googleusercontent.com';
set $ngo_client_secret 'abcdefg-123-xyz';
set $ngo_token_secret 'a very long randomish string';
access_by_lua_file "/etc/nginx/nginx-google-oauth/access.lua";
location / {
Expand Down Expand Up @@ -170,6 +175,36 @@ The filter operates by performing a regular expression match on ``<body>``,
and so should act as a no-op for non-HTML content types. It may be necessary
to use the body filter only on a subset of routes depending on your application.

## Username variable

If you wish to pass the username returned from Google to an external FastCGI/UWSGI script, consider using the ``$ngo_user`` variable:

```
server {
server_name supersecret.net;
listen 443;

ssl on;
ssl_certificate /etc/nginx/certs/supersecret.net.pem;
ssl_certificate_key /etc/nginx/certs/supersecret.net.key;

set $ngo_client_id "abc-def.apps.googleusercontent.com";
set $ngo_client_secret "abcdefg-123-xyz";
set $ngo_token_secret "a very long randomish string";
set $ngo_secure_cookies "true";
access_by_lua_file "/etc/nginx/nginx-google-oauth/access.lua";

set $ngo_user "unknown@unknown.com";

include uwsgi_params;
uwsgi_param REMOTE_USER $ngo_user;
uwsgi_param AUTH_TYPE Basic;
uwsgi_pass 127.0.0.1:3031;
}
```

If you wish the full email address returned from Google to be set as the username, set the ``$ngo_email_as_user`` variable to any non-empty value.

## Development

Bug reports and pull requests are [welcome](https://github.com/agoragames/nginx-google-oauth).
Expand Down
95 changes: 71 additions & 24 deletions access.lua
Original file line number Diff line number Diff line change
@@ -1,43 +1,76 @@

-- import requirements
local cjson = require "cjson"

-- allow either cjson, or th-LuaJSON
local has_cjson, jsonmod = pcall(require, "cjson")
if not has_cjson then
jsonmod = require "json"
end

-- Ubuntu broke the install. Puts the source in /usr/share/lua/5.1/https.lua,
-- but since the source defines itself as the module "ssl.https", after we
-- load the source, we need to grab the actual thing. Building from source
-- wasn't practical.
-- TODO: make this more generic but still work with Ubuntu
require "https" --
-- load the source, we need to grab the actual thing.
pcall(require,"https")
local https = require "ssl.https" -- /usr/share/lua/5.1/https.lua
local ltn12 = require("ltn12")

local uri = ngx.var.uri
local uri_args = ngx.req.get_uri_args()
local scheme = ngx.var.scheme
local server_name = ngx.var.server_name

-- setup some app-level vars
local client_id = ngx.var.ngo_client_id
local client_secret = ngx.var.ngo_client_secret
local domain = ngx.var.ngo_domain
local cb_scheme = ngx.var.ngo_callback_scheme or ngx.var.scheme
local cb_server_name = ngx.var.ngo_callback_host or ngx.var.server_name
local cb_scheme = ngx.var.ngo_callback_scheme or scheme
local cb_server_name = ngx.var.ngo_callback_host or server_name
local cb_uri = ngx.var.ngo_callback_uri or "/_oauth"
local cb_url = cb_scheme.."://"..cb_server_name..cb_uri
local redir_url = cb_scheme.."://"..cb_server_name..uri
local signout_uri = ngx.var.ngo_signout_uri or "/_signout"
local debug = ngx.var.ngo_debug
local whitelist = ngx.var.ngo_whitelist
local blacklist = ngx.var.ngo_blacklist
local secure_cookies = ngx.var.ngo_secure_cookies

local uri_args = ngx.req.get_uri_args()
local token_secret = ngx.var.ngo_token_secret or "UNSET"
local set_user = ngx.var.ngo_user
local email_as_user = ngx.var.ngo_email_as_user

-- Force the user to set a token secret
if token_secret == "UNSET" then
ngx.log(ngx.ERR, "$ngo_token_secret must be set in Nginx config!")
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end

-- See https://developers.google.com/accounts/docs/OAuth2WebServer
if ngx.var.uri == signout_uri then
if uri == signout_uri then
ngx.header["Set-Cookie"] = "AccessToken=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
return ngx.redirect(ngx.var.scheme.."://"..ngx.var.server_name)
return ngx.redirect(cb_scheme.."://"..server_name)
end

if not ngx.var.cookie_AccessToken then
-- Enforce token security and expiration
local oauth_expires = tonumber(ngx.var.cookie_OauthExpires) or 0
local oauth_email = ngx.unescape_uri(ngx.var.cookie_OauthEmail or "")
local oauth_access_token = ngx.unescape_uri(ngx.var.cookie_OauthAccessToken or "")
local expected_token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. oauth_email .. oauth_expires))

if oauth_access_token == expected_token and oauth_expires and oauth_expires > ngx.time() then
-- Populate the nginx 'ngo_user' variable with our Oauth username, if requested
if set_user then
local oauth_user, oauth_domain = oauth_email:match("([^@]+)@(.+)")
if email_as_user then
ngx.var.ngo_user = email
else
ngx.var.ngo_user = oauth_user
end
end
return
else
-- If no access token and this isn't the callback URI, redirect to oauth
if ngx.var.uri ~= cb_uri then
if uri ~= cb_uri then
-- Redirect to the /oauth endpoint, request access to ALL scopes
return ngx.redirect("https://accounts.google.com/o/oauth2/auth?client_id="..client_id.."&scope=email&response_type=code&redirect_uri="..ngx.escape_uri(cb_url).."&state="..ngx.escape_uri(ngx.var.uri).."&login_hint="..ngx.escape_uri(domain))
return ngx.redirect("https://accounts.google.com/o/oauth2/auth?client_id="..client_id.."&scope=email&response_type=code&redirect_uri="..ngx.escape_uri(cb_url).."&state="..ngx.escape_uri(redir_url).."&login_hint="..ngx.escape_uri(domain))
end

-- Fetch teh authorization code from the parameters
Expand Down Expand Up @@ -72,8 +105,9 @@ if not ngx.var.cookie_AccessToken then
end

-- use version 1 cookies so we don't have to encode. MSIE-old beware
local json = cjson.decode( res )
local json = jsonmod.decode( res )
local access_token = json["access_token"]
local expires = ngx.time() + json["expires_in"]
local cookie_tail = ";version=1;path=/;Max-Age="..json["expires_in"]
if secure_cookies then
cookie_tail = cookie_tail..";secure"
Expand All @@ -100,15 +134,18 @@ if not ngx.var.cookie_AccessToken then
ngx.log(ngx.ERR, "DEBUG: userinfo response "..res2..code2..status2..table.concat(result_table))
end

json = cjson.decode( table.concat(result_table) )
json = jsonmod.decode( table.concat(result_table) )

local name = json["name"]
local email = json["email"]
local picture = json["picture"]
local token = ngx.encode_base64(ngx.hmac_sha1(token_secret, cb_server_name .. email .. expires))

local oauth_user, oauth_domain = email:match("([^@]+)@(.+)")

-- If no whitelist or blacklist, match on domain
if not whitelist and not blacklist then
if not string.find(email, "@"..domain) then
if not whitelist and not blacklist and domain then
if oauth_domain ~= domain then
if debug then
ngx.log(ngx.ERR, "DEBUG: "..email.." not in "..domain)
end
Expand All @@ -117,7 +154,7 @@ if not ngx.var.cookie_AccessToken then
end

if whitelist then
if not string.find(whitelist, email) then
if not string.find(" " .. whitelist .. " ", " " .. email .. " ") then
if debug then
ngx.log(ngx.ERR, "DEBUG: "..email.." not in whitelist")
end
Expand All @@ -126,7 +163,7 @@ if not ngx.var.cookie_AccessToken then
end

if blacklist then
if string.find(blacklist, email) then
if string.find(" " .. blacklist .. " ", " " .. email .. " ") then
if debug then
ngx.log(ngx.ERR, "DEBUG: "..email.." in blacklist")
end
Expand All @@ -135,12 +172,22 @@ if not ngx.var.cookie_AccessToken then
end

ngx.header["Set-Cookie"] = {
"AccessToken="..access_token..cookie_tail,
"Name="..ngx.escape_uri(name)..cookie_tail,
"Email="..ngx.escape_uri(email)..cookie_tail,
"Picture="..ngx.escape_uri(picture)..cookie_tail
"OauthAccessToken="..ngx.escape_uri(token)..cookie_tail,
"OauthExpires="..expires..cookie_tail,
"OauthName="..ngx.escape_uri(name)..cookie_tail,
"OauthEmail="..ngx.escape_uri(email)..cookie_tail,
"OauthPicture="..ngx.escape_uri(picture)..cookie_tail
}

-- Poplate our ngo_user variable
if set_user then
if email_as_user then
ngx.var.ngo_user = email
else
ngx.var.ngo_user = oauth_user
end
end

-- Redirect
if debug then
ngx.log(ngx.ERR, "DEBUG: authorized "..json["email"]..", redirecting to "..uri_args["state"])
Expand Down

0 comments on commit 3c34b27

Please sign in to comment.