Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A few usability changes and security fixes #4

Merged
merged 8 commits into from
Mar 20, 2015
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 @@ -169,6 +174,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