site/api/oauth.lua (120 lines of code) (raw):
--[[
 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.
]]--
-- This is oauth.lua - an oauth providing script for ponymail
local JSON = require 'cjson'
local http = require 'socket.http'
local elastic = require 'lib/elastic'
local https = require 'ssl.https'
local user = require 'lib/user'
local cross = require 'lib/cross'
local config = require 'lib/config'
function handle(r)
    cross.contentType(r, "application/json")
    local get = r:parseargs()
    local post = r:parsebody()
    local valid, json
    local scheme = "https"
    if r.port == 80 then
        scheme = "http"
    end
    
    local oauth_domain = ""
    if config.oauth_fields and config.oauth_fields[get.key] then
        for k, v in pairs(config.oauth_fields[get.key]) do
            r.args = r.args .. ("&%s=%s"):format(k,v)
        end
        if config.oauth_fields[get.key].oauth_token then
            get.oauth_token = config.oauth_fields[get.key].oauth_token
        end
    end
    
    -- Google Auth callback
    if get.oauth_token and get.oauth_token:match("^https://www.google") and get.code then
        oauth_domain = "www.googleapis.com"
        local result = https.request("https://www.googleapis.com/oauth2/v4/token",
                                     ("client_secret=%s&code=%s&client_id=%s&grant_type=authorization_code&redirect_uri=%s" ):format(
                                        r:escape(config.oauth_fields.google.client_secret),
                                        r:escape(get.code),
                                        r:escape(config.oauth_fields.google.client_id),
                                        r:escape(config.oauth_fields.google.redirect_uri)
                                        ))
        valid, json = pcall(function() return JSON.decode(result) end)
        if valid and json and json.access_token then
            local ac = json.access_token
            local result = https.request("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" .. r:escape(ac))
            valid, json = pcall(function() return JSON.decode(result) end)
        else
            json = nil
            valid = false
        end
    -- GitHub Auth callback
    elseif get.oauth_token and get.key == 'github' then
        local result = https.request(get.oauth_token, r.args)
        local token = result:match("(access_token=[a-f0-9]+)")
        if token then
            local result = https.request("https://api.github.com/user/emails?" .. token)
            valid, json = pcall(function() return JSON.decode(result) end)
            if valid and json then
                json = json[1]
            end
        end
        
    -- OAuth.online callback
    elseif get.oauth_token and get.key == 'online' then
        local result = https.request("https://verify.oauth.online/token", r.args)
        valid, json = pcall(function() return JSON.decode(result) end)
        
    -- Generic callback (like ASF Oauth2)
    elseif get.state and get.code and get.oauth_token then
        oauth_domain = get.oauth_token:match("https?://(.-)/")
        local result = https.request(get.oauth_token, r.args)
        valid, json = pcall(function() return JSON.decode(result) end)
    --[[
        CAS or other internal auth mechanism though request or env headers:
        Remember to set the 'internal' field vars in config.lua to enable this, for instance:
        ...
        oauth_fields = {
            internal = {
                email = 'CAS-EMAIL',
                name = 'CAS-NAME',
                uid = 'REMOTE-USER',
                env = 'subprocess' -- use environment vars instead of request headers
            }
        },
        oauth_admin = { "localhost" },
        ...
    ]]--
    elseif get.key == 'internal' and config.oauth_fields['internal'] then
        oauth_domain = "localhost"
        local tbl = r.headers_in
        if config.oauth_fields['internal'].env and config.oauth_fields['internal'].env == 'subprocess' then
            tbl = r.subprocess_env
        end
        json = {
            email = tbl[config.oauth_fields['internal']['email'] or 0],
            name = tbl[config.oauth_fields['internal']['name'] or 0],
            uid = tbl[config.oauth_fields['internal']['uid'] or 0]
        }
        -- if httpd borks, bail!
        if json.uid == '(null)' or json.email == '(null)' then
            json = nil
        end
        -- Only use internal thing if localhost is trusted
        for k, v in pairs(config.admin_oauth or {}) do
            if r.strcmp_match(oauth_domain, v) then
                valid = true
                break
            end
        end
    end
    
    -- Did we get something useful from the backend?
    if valid and json then
        local eml = json.email
        local fname = json.fullname or json.name or json.email
        local admin = json.isMember
        
        -- If we got an email and a name, log in the user and set cookie etc
        if eml and fname then
            local cid = json.uid or json.email
            -- Does the user exist already?
            local oaccount = user.get(r, cid)
            local usr = {}
            if oaccount then
                usr.preferences = oaccount.preferences
            else
                usr.preferences = {}
            end
            usr.gauth = get.id_token
            usr.fullname = fname
            
            -- if the oauth provider can set admin status, do so if needed
            local authority = false
            for k, v in pairs(config.admin_oauth or {}) do
                if r.strcmp_match(oauth_domain, v) then
                    authority = true
                    break
                end
            end
            if authority then
                usr.admin = admin
            end
            
            usr.email = eml
            usr.uid = json.uid
            usr.oauth_used = oauth_domain
            user.update(r, cid, usr)
            r:puts[[{"okay": true, "msg": "Logged in successfully!"}]]
        
        -- didn't get email or name, bork!
        else
            r:puts[[{"okay": false, "msg": "Erroneous or missing response from backend!"}]]
        end
    -- Backend borked, let the user know
    else
        r:puts[[{"okay": false, "msg": "Invalid oauth response!"}]]
    end
    return cross.OK
end
cross.start(handle)