stormpath / stormpath-nginx

A Stormpath integration written in Lua for the nginx web server.
Apache License 2.0
24 stars 7 forks source link

Require Group #3

Open reggiepierce opened 8 years ago

reggiepierce commented 8 years ago

It would be great to have the ability to require group membership. Unfortunately, I'm not very experienced with LUA, and this may already be possible for all I know.

Something like this would be great:

location /api/ {
        access_by_lua_block {
            local stormpath = require("stormpath-nginx")
            stormpath.requireAccountWithMembership('https://stormpath-group-href')
        }
        proxy_pass http://localhost:3000/;
    }
reggiepierce commented 8 years ago

Took a stab at a feature update. First time using Lua, so probably inefficient.

I added two new functionalities:

See the code below:

local jwt = require('resty.jwt')
local lrucache = require('resty.lrucache')
local validators = require('resty.jwt-validators')
local stormpathApplicationHref = os.getenv('STORMPATH_APPLICATION_HREF')
local stormpathApiKeyId = os.getenv('STORMPATH_CLIENT_APIKEY_ID')
local stormpathApiKeySecret = os.getenv('STORMPATH_CLIENT_APIKEY_SECRET')
local groupMembershipCacheExpireSeconds = os.getenv('STORMPATH_GROUP_MEMBERSHIP_CACHE_EXPIRE_SECONDS')
local jwtCookieName = os.getenv('STORMPATH_JWT_COOKIE_NAME')

local cache = lrucache.new(200)
local M = {}
local Helpers = {}

function M.getAccount()
  getAccount(false)
end

function M.requireAccount()
  getAccount(true)
end

function M.requireAccountWithGroupMembership(groupHref)
    local jwt=getAccount(true)
    checkGroupMembership(jwt, groupHref)
end

function getAccount(required)
    local jwtString = Helpers.getBearerToken()
    if isEmpty(jwtString) then
        jwtString = getJwtCookie()
    end
    local jwt=getJwt(jwtString)
    if jwt==nil then
        return Helpers.exit(required)
    end
    ngx.req.set_header('x-stormpath-application-href', jwt.payload.iss)
    ngx.req.set_header('x-stormpath-account-href', jwt.payload.sub)
    return jwt
end

local http = require('resty.http')
local cjson = require('cjson')

-- custom methods for group stuff

function getJwtCookie()
    local cookie_name = jwtCookieName
    if isEmpty(cookie_name) then
        cookie_name='account'
    end
    cookie_name='cookie_' .. cookie_name
    return ngx.var[cookie_name]
end

function getJwt(jwtString)
    if isEmpty(jwtString) then
        return nil
    end

    local claimSpec = {
        exp = validators.required(validators.opt_is_not_expired()),
    }

    local jwt = jwt:verify(stormpathApiKeySecret, jwtString, claimSpec)

    if not (jwt.verified and jwt.header.stt == 'access' and jwt.header.alg == 'HS256') then
        return nil
    end
    return jwt
end

function isEmpty(s)
  return s == nil or s == ''
end

function splitStr(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; i=0
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            i = i + 1
            t[i] = str

    end
    local size=i+1
    return t, size
end

function getAccountId(accountHref)
    local t, size =splitStr(accountHref,'/')
    return t[size-1]
end

function checkGroupMembership(jwt, groupHref)
    -- prereq check
    if jwt==nil then
        ngx.log(ngx.STDERR, 'no jwt found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    if  isEmpty(groupHref) then
        ngx.log(ngx.STDERR, 'group href is blank or nil')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountHref=jwt.payload.sub
    if isEmpty(accountHref) then
        ngx.log(ngx.STDERR, 'no account href found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountId=getAccountId(accountHref)
    if isEmpty(accountId) then
        ngx.log(ngx.STDERR, 'could not parse account id: ' .. accountHref)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    -- end prereq check
    -- cache check
    local cacheKey= accountId .. groupHref
    local cachedSuccess = cache:get(cacheKey)
    if cachedSuccess ~= nil and cachedSuccess then
        return nil
    end
    if cachedSuccess ~= nil and not cachedSuccess then
        ngx.log(ngx.STDERR, 'account is not member of group: ' .. accountHref .. ' - ' .. groupHref)
        return Helpers.exit(required)
    end
    -- done cache check
    -- api check
    local httpc = http.new()
    ngx.req.read_body()
    local headers = ngx.req.get_headers()

    -- Proxy these certain parameters to the Stormpath API

    local request = {
        method = ngx.var.request_method,
        body = ngx.req.get_body_data(),
        headers = {
          authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
          ['content-type'] = headers['content-type'],
          accept = 'application/json',
          ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
        }
    }

    -- For client credentials requests, we need to transform basic auth to post body parameters

    local apiKeyId, apiKeySecret = Helpers.getBasicAuthCredentials()

    if apiKeyId and apiKeySecret then
        request.body = (request.body or '') .. '&apiKeyId=' .. ngx.escape_uri(apiKeyId) .. 
        '&apiKeySecret=' .. ngx.escape_uri(apiKeySecret)
    end

    -- We also need to pass the X-Stormpath-Agent if present

    if headers['x-stormpath-agent'] then
        request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
    end

    -- Make the request to get memberships

    local membershipUrl='https://api.stormpath.com/v1/accounts/' .. accountId .. '/groupMemberships'
    local res, err = httpc:request_uri(membershipUrl , request)

    --check status
    if not res or res.status ~=200 then
        ngx.log(ngx.STDERR, 'group membership lookup failed: ' .. membershipUrl)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local json = cjson.decode(res.body)
    local found=false
    for i,item in pairs(json.items) do
        if item.group.href == groupHref then
            found=true
            break
        end
    end
    -- done api check
    -- cache val
    local cacheSeconds=10;
    if not isEmpty(groupMembershipCacheExpireSeconds) and toNumber(groupMembershipCacheExpireSeconds) > 0 then
        cacheSeconds=toNumber(groupMembershipCacheExpireSeconds)
    end
    cache:set(cacheKey,found,cacheSeconds)
    if not found then
        ngx.log(ngx.STDERR, 'account is not member of group: ' .. accountHref .. ' - ' .. groupHref)
        return Helpers.exit(required)
    end
    -- done cache val
end

-- end group stuff

function M.oauthTokenEndpoint(applicationHref)
  applicationHref = applicationHref or stormpathApplicationHref
  oauthTokenEndpoint(applicationHref)
end

function oauthTokenEndpoint(applicationHref)
  local httpc = http.new()
  ngx.req.read_body()

  local headers = ngx.req.get_headers()

  -- Proxy these certain parameters to the Stormpath API

  local request = {
    method = ngx.var.request_method,
    body = ngx.req.get_body_data(),
    headers = {
      authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
      ['content-type'] = headers['content-type'],
      accept = 'application/json',
      ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
    }
  }

  -- For client credentials requests, we need to transform basic auth to post body parameters

  local apiKeyId, apiKeySecret = Helpers.getBasicAuthCredentials()

  if apiKeyId and apiKeySecret then
    request.body = (request.body or '') .. '&apiKeyId=' .. ngx.escape_uri(apiKeyId) .. 
    '&apiKeySecret=' .. ngx.escape_uri(apiKeySecret)
  end

  -- We also need to pass the X-Stormpath-Agent if present

  if headers['x-stormpath-agent'] then
    request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
  end

  -- Make the request

  local res, err = httpc:request_uri(applicationHref .. '/oauth/token' , request)

  if not res or res.status >= 500 then
    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  end

  local json = cjson.decode(res.body)
  local response = {}

  -- Respond with a stripped token response or error

  if res.status == 200 then
    response = {
      access_token = json.access_token,
      refresh_token = json.refresh_token,
      token_type = json.token_type,
      expires_in = json.expires_in
    }
  else
    response = {
      error = json.error,
      message = json.message
    }
  end

  ngx.status = res.status
  ngx.header.content_type = res.headers['Content-Type']
  ngx.header.cache_control = 'no-store'
  ngx.header.pragma = 'no-cache'
  ngx.say(cjson.encode(response))
  ngx.exit(ngx.HTTP_OK)
end

function Helpers.exit(required)
  if required then
    return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  else
    return ngx.exit(ngx.OK)
  end
end

function Helpers.getBearerToken()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Bearer ') then
    return nil
  else
    return authorizationHeader:sub(8)
  end
end

function Helpers.getBasicAuthCredentials()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Basic ') then
    return nil
  else
    local decodedHeader = ngx.decode_base64(authorizationHeader:sub(7))
    local position = decodedHeader:find(':')
    local username = decodedHeader:sub(1,position-1)
    local password = decodedHeader:sub(position+1)

    return username, password
  end
end

function string:startsWith(partialString)
  local partialStringLength = partialString:len()
  return self:len() >= partialStringLength and self:sub(1, partialStringLength) == partialString
end

function Helpers.copy(headers)
  local result = {}
  for k,v in pairs(headers) do
    result[k] = v
  end
  return result
end

return M
reggiepierce commented 8 years ago

I think I messed up the caching before, and so I took another stab. See below:

local jwt = require('resty.jwt')

local validators = require('resty.jwt-validators')
local stormpathApplicationHref = os.getenv('STORMPATH_APPLICATION_HREF')
local stormpathApiKeyId = os.getenv('STORMPATH_CLIENT_APIKEY_ID')
local stormpathApiKeySecret = os.getenv('STORMPATH_CLIENT_APIKEY_SECRET')
local groupMembershipCacheExpireSeconds = os.getenv('STORMPATH_GROUP_MEMBERSHIP_CACHE_EXPIRE_SECONDS')
local jwtCookieName = os.getenv('STORMPATH_JWT_COOKIE_NAME')

local M = {}
local Helpers = {}

function M.getAccount()
  getAccount(false)
end

function M.requireAccount()
  getAccount(true)
end

function M.requireAccountWithGroupMembership(groupHref)
    local jwt=getAccount(true)
    checkGroupMembership(true, jwt, groupHref)
end

function getAccount(required)
    local jwtString = Helpers.getBearerToken()
    if Helpers.isEmpty(jwtString) then
        jwtString = Helpers.getJwtCookie()
    end
    local jwt= Helpers.getAndVeriftyJwt(jwtString)
    if jwt==nil then
        return Helpers.exit(required)
    end
    ngx.req.set_header('x-stormpath-application-href', jwt.payload.iss)
    ngx.req.set_header('x-stormpath-account-href', jwt.payload.sub)
    return jwt
end

local http = require('resty.http')
local cjson = require('cjson')

-- custom methods for group stuff

function checkGroupMembership(required, jwt, groupHref)
    -- prereq check
    if jwt==nil then
        ngx.log(ngx.STDERR, 'no jwt found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    if  Helpers.isEmpty(groupHref) then
        ngx.log(ngx.STDERR, 'group href is blank or nil')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountHref=jwt.payload.sub
    if Helpers.isEmpty(accountHref) then
        ngx.log(ngx.STDERR, 'no account href found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountId=Helpers.getAccountId(accountHref)
    if Helpers.isEmpty(accountId) then
        ngx.log(ngx.STDERR, 'could not parse account id: ' .. accountHref)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    -- end prereq check
    -- cache check
    local cacheKey= accountId .. groupHref
    local cacheValue = Helpers.cache():get(cacheKey)
    if not (cacheValue == nil) then
        if  cacheValue.success then
            return nil
        else
            return Helpers.exit(required)
        end

    end
    -- done cache check
    -- api check
    local httpc = http.new()
    ngx.req.read_body()
    local headers = ngx.req.get_headers()

    -- Proxy these certain parameters to the Stormpath API

    local request = {
        method = ngx.var.request_method,
        body = ngx.req.get_body_data(),
        headers = {
          authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
          ['content-type'] = headers['content-type'],
          accept = 'application/json',
          ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
        }
    }

    -- For client credentials requests, we need to transform basic auth to post body parameters

    local apiKeyId, apiKeySecret = Helpers.getBasicAuthCredentials()

    if apiKeyId and apiKeySecret then
        request.body = (request.body or '') .. '&apiKeyId=' .. ngx.escape_uri(apiKeyId) .. 
        '&apiKeySecret=' .. ngx.escape_uri(apiKeySecret)
    end

    -- We also need to pass the X-Stormpath-Agent if present

    if headers['x-stormpath-agent'] then
        request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
    end

    -- Make the request to get memberships

    local membershipUrl='https://api.stormpath.com/v1/accounts/' .. accountId .. '/groupMemberships'
    local res, err = httpc:request_uri(membershipUrl , request)

    --check status
    if not res or res.status ~=200 then
        ngx.log(ngx.STDERR, 'group membership lookup failed: ' .. membershipUrl)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local json = cjson.decode(res.body)
    local found=false
    for i,item in pairs(json.items) do
        if item.group.href == groupHref then
            found=true
            break
        end
    end
    -- done api check
    -- cache val
    local cacheSeconds=10
    if not Helpers.isEmpty(groupMembershipCacheExpireSeconds) then
        local envCacheSeconds=toNumber(groupMembershipCacheExpireSeconds)
        if envCacheSeconds > 0 then
            cacheSeconds=envCacheSeconds
        end
    end
    Helpers.cache():set(cacheKey,{ success = found },cacheSeconds)
    if not found then
        return Helpers.exit(required)
    end
    -- done cache val
end

-- end group stuff

function M.oauthTokenEndpoint(applicationHref)
  applicationHref = applicationHref or stormpathApplicationHref
  oauthTokenEndpoint(applicationHref)
end

function oauthTokenEndpoint(applicationHref)
  local httpc = http.new()
  ngx.req.read_body()

  local headers = ngx.req.get_headers()

  -- Proxy these certain parameters to the Stormpath API

  local request = {
    method = ngx.var.request_method,
    body = ngx.req.get_body_data(),
    headers = {
      authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
      ['content-type'] = headers['content-type'],
      accept = 'application/json',
      ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
    }
  }

  -- For client credentials requests, we need to transform basic auth to post body parameters

  local apiKeyId, apiKeySecret = Helpers.getBasicAuthCredentials()

  if apiKeyId and apiKeySecret then
    request.body = (request.body or '') .. '&apiKeyId=' .. ngx.escape_uri(apiKeyId) .. 
    '&apiKeySecret=' .. ngx.escape_uri(apiKeySecret)
  end

  -- We also need to pass the X-Stormpath-Agent if present

  if headers['x-stormpath-agent'] then
    request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
  end

  -- Make the request

  local res, err = httpc:request_uri(applicationHref .. '/oauth/token' , request)

  if not res or res.status >= 500 then
    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  end

  local json = cjson.decode(res.body)
  local response = {}

  -- Respond with a stripped token response or error

  if res.status == 200 then
    response = {
      access_token = json.access_token,
      refresh_token = json.refresh_token,
      token_type = json.token_type,
      expires_in = json.expires_in
    }
  else
    response = {
      error = json.error,
      message = json.message
    }
  end

  ngx.status = res.status
  ngx.header.content_type = res.headers['Content-Type']
  ngx.header.cache_control = 'no-store'
  ngx.header.pragma = 'no-cache'
  ngx.say(cjson.encode(response))
  ngx.exit(ngx.HTTP_OK)
end

function Helpers.exit(required)
  if required then
    return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  else
    return ngx.exit(ngx.OK)
  end
end

function Helpers.getBearerToken()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Bearer ') then
    return nil
  else
    return authorizationHeader:sub(8)
  end
end

function Helpers.getBasicAuthCredentials()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Basic ') then
    return nil
  else
    local decodedHeader = ngx.decode_base64(authorizationHeader:sub(7))
    local position = decodedHeader:find(':')
    local username = decodedHeader:sub(1,position-1)
    local password = decodedHeader:sub(position+1)

    return username, password
  end
end

function string:startsWith(partialString)
  local partialStringLength = partialString:len()
  return self:len() >= partialStringLength and self:sub(1, partialStringLength) == partialString
end

function Helpers.copy(headers)
  local result = {}
  for k,v in pairs(headers) do
    result[k] = v
  end
  return result
end

--new helpers

function Helpers.isEmpty(s)
  return s == nil or s == ''
end

function Helpers.splitString(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; i=0
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            i = i + 1
            t[i] = str

    end
    local size=i+1
    return t, size
end

function Helpers.getAccountId(accountHref)
    local t, size =Helpers.splitString(accountHref,'/')
    return t[size-1]
end

function Helpers.getAndVeriftyJwt(jwtString)
    if Helpers.isEmpty(jwtString) then
        return nil
    end

    local claimSpec = {
        exp = validators.required(validators.opt_is_not_expired()),
    }

    local jwt = jwt:verify(stormpathApiKeySecret, jwtString, claimSpec)

    if not (jwt.verified and jwt.header.stt == 'access' and jwt.header.alg == 'HS256') then
        return nil
    end
    return jwt
end

function Helpers.getJwtCookie()
    local cookie_name = jwtCookieName
    if Helpers.isEmpty(cookie_name) then
        cookie_name='account'
    end
    cookie_name='cookie_' .. cookie_name
    return ngx.var[cookie_name]
end

function Helpers.cache()
    local cache=_G.groupMembershipCache
    if cache == nil then
        local lrucache = require('resty.lrucache')
        _G.groupMembershipCache=lrucache.new(100 * 1000);
        cache=_G.groupMembershipCache
    end
    return cache
end

return M
reggiepierce commented 8 years ago

Final update. Simplified the API call to stormpath and added some debug logging. Let me know if anyone has feedback.

local jwt = require('resty.jwt')

local validators = require('resty.jwt-validators')
local stormpathApplicationHref = os.getenv('STORMPATH_APPLICATION_HREF')
local stormpathApiKeyId = os.getenv('STORMPATH_CLIENT_APIKEY_ID')
local stormpathApiKeySecret = os.getenv('STORMPATH_CLIENT_APIKEY_SECRET')
local groupMembershipCacheExpireSeconds = os.getenv('STORMPATH_GROUP_MEMBERSHIP_CACHE_EXPIRE_SECONDS')
local jwtCookieName = os.getenv('STORMPATH_JWT_COOKIE_NAME')

local M = {}
local Helpers = {}

function M.getAccount()
  getAccount(false)
end

function M.requireAccount()
  getAccount(true)
end

function M.requireAccountWithGroupMembership(groupHref)
    local jwt=getAccount(true)
    checkGroupMembership(true, jwt, groupHref)
end

function getAccount(required)
    local jwtString = Helpers.getBearerToken()
    if Helpers.isEmpty(jwtString) then
        jwtString = Helpers.getJwtCookie()
    end
    local jwt= Helpers.getAndVeriftyJwt(jwtString)
    if jwt==nil then
        return Helpers.exit(required)
    end
    ngx.req.set_header('x-stormpath-application-href', jwt.payload.iss)
    ngx.req.set_header('x-stormpath-account-href', jwt.payload.sub)
    return jwt
end

local http = require('resty.http')
local cjson = require('cjson')

-- custom methods for group stuff

function checkGroupMembership(required, jwt, groupHref)
    -- prereq check
    if jwt==nil then
        ngx.log(ngx.STDERR, 'no jwt found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    if  Helpers.isEmpty(groupHref) then
        ngx.log(ngx.STDERR, 'group href is blank or nil')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountHref=jwt.payload.sub
    if Helpers.isEmpty(accountHref) then
        ngx.log(ngx.STDERR, 'no account href found')
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local accountId=Helpers.getAccountId(accountHref)
    if Helpers.isEmpty(accountId) then
        ngx.log(ngx.STDERR, 'could not parse account id: ' .. accountHref)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    -- end prereq check
    -- cache check
    local cacheKey= accountId .. groupHref
    local cacheValue = Helpers.cache():get(cacheKey)
    if not (cacheValue == nil) then
        if  cacheValue.success then
            ngx.log(ngx.DEBUG, 'cache: membership found - ' .. accountId .. ' - ' .. groupHref)
            return nil
        else
            ngx.log(ngx.DEBUG, 'cache: membership not found - ' .. accountId .. ' - ' .. groupHref)
            return Helpers.exit(required)
        end
    end
    -- done cache check
    -- api check
    local httpc = http.new()
    local headers = ngx.req.get_headers()

    -- Proxy these certain parameters to the Stormpath API

    local request = {
        method = "GET",
        headers = {
          authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
          ['content-type'] = headers['content-type'],
          accept = 'application/json',
          ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
        }
    }

    -- We also need to pass the X-Stormpath-Agent if present

    if headers['x-stormpath-agent'] then
        request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
    end

    -- Make the request to get memberships

    local membershipUrl='https://api.stormpath.com/v1/accounts/' .. accountId .. '/groupMemberships'
    local res, err = httpc:request_uri(membershipUrl , request)

    --check status
    if not res or res.status ~=200 then
        ngx.log(ngx.STDERR, 'group membership lookup failed: ' .. membershipUrl)
        return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    local json = cjson.decode(res.body)
    local found=false
    for i,item in pairs(json.items) do
        if item.group.href == groupHref then
            found=true
            break
        end
    end
    -- done api check
    -- cache val
    local cacheSeconds=10
    if not Helpers.isEmpty(groupMembershipCacheExpireSeconds) then
        local envCacheSeconds=toNumber(groupMembershipCacheExpireSeconds)
        if envCacheSeconds > 0 then
            cacheSeconds=envCacheSeconds
        end
    end
    Helpers.cache():set(cacheKey,{ success = found },cacheSeconds)
    if not found then
        ngx.log(ngx.DEBUG, 'api: membership not found - ' .. accountId .. ' - ' .. groupHref)
        return Helpers.exit(required)
    end
    ngx.log(ngx.DEBUG, 'api: membership found - ' .. accountId .. ' - ' .. groupHref)
    -- done cache val
end

-- end group stuff

function M.oauthTokenEndpoint(applicationHref)
  applicationHref = applicationHref or stormpathApplicationHref
  oauthTokenEndpoint(applicationHref)
end

function oauthTokenEndpoint(applicationHref)
  local httpc = http.new()
  ngx.req.read_body()

  local headers = ngx.req.get_headers()

  -- Proxy these certain parameters to the Stormpath API

  local request = {
    method = ngx.var.request_method,
    body = ngx.req.get_body_data(),
    headers = {
      authorization = 'Basic ' .. ngx.encode_base64(stormpathApiKeyId .. ':' .. stormpathApiKeySecret),
      ['content-type'] = headers['content-type'],
      accept = 'application/json',
      ['user-agent'] = 'stormpath-nginx/1.0.1 nginx/' .. ngx.var.nginx_version
    }
  }

  -- For client credentials requests, we need to transform basic auth to post body parameters

  local apiKeyId, apiKeySecret = Helpers.getBasicAuthCredentials()

  if apiKeyId and apiKeySecret then
    request.body = (request.body or '') .. '&apiKeyId=' .. ngx.escape_uri(apiKeyId) .. 
    '&apiKeySecret=' .. ngx.escape_uri(apiKeySecret)
  end

  -- We also need to pass the X-Stormpath-Agent if present

  if headers['x-stormpath-agent'] then
    request.headers['user-agent'] = headers['x-stormpath-agent'] .. ' ' .. request.headers['user-agent']
  end

  -- Make the request

  local res, err = httpc:request_uri(applicationHref .. '/oauth/token' , request)

  if not res or res.status >= 500 then
    return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  end

  local json = cjson.decode(res.body)
  local response = {}

  -- Respond with a stripped token response or error

  if res.status == 200 then
    response = {
      access_token = json.access_token,
      refresh_token = json.refresh_token,
      token_type = json.token_type,
      expires_in = json.expires_in
    }
  else
    response = {
      error = json.error,
      message = json.message
    }
  end

  ngx.status = res.status
  ngx.header.content_type = res.headers['Content-Type']
  ngx.header.cache_control = 'no-store'
  ngx.header.pragma = 'no-cache'
  ngx.say(cjson.encode(response))
  ngx.exit(ngx.HTTP_OK)
end

function Helpers.exit(required)
  if required then
    return ngx.exit(ngx.HTTP_UNAUTHORIZED)
  else
    return ngx.exit(ngx.OK)
  end
end

function Helpers.getBearerToken()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Bearer ') then
    return nil
  else
    return authorizationHeader:sub(8)
  end
end

function Helpers.getBasicAuthCredentials()
  local authorizationHeader = ngx.var.http_authorization

  if not authorizationHeader or not authorizationHeader:startsWith('Basic ') then
    return nil
  else
    local decodedHeader = ngx.decode_base64(authorizationHeader:sub(7))
    local position = decodedHeader:find(':')
    local username = decodedHeader:sub(1,position-1)
    local password = decodedHeader:sub(position+1)

    return username, password
  end
end

function string:startsWith(partialString)
  local partialStringLength = partialString:len()
  return self:len() >= partialStringLength and self:sub(1, partialStringLength) == partialString
end

function Helpers.copy(headers)
  local result = {}
  for k,v in pairs(headers) do
    result[k] = v
  end
  return result
end

--new helpers

function Helpers.isEmpty(s)
  return s == nil or s == ''
end

function Helpers.splitString(inputstr, sep)
    if sep == nil then
            sep = "%s"
    end
    local t={} ; i=0
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            i = i + 1
            t[i] = str

    end
    local size=i+1
    return t, size
end

function Helpers.getAccountId(accountHref)
    local t, size =Helpers.splitString(accountHref,'/')
    return t[size-1]
end

function Helpers.getAndVeriftyJwt(jwtString)
    if Helpers.isEmpty(jwtString) then
        return nil
    end

    local claimSpec = {
        exp = validators.required(validators.opt_is_not_expired()),
    }

    local jwt = jwt:verify(stormpathApiKeySecret, jwtString, claimSpec)

    if not (jwt.verified and jwt.header.stt == 'access' and jwt.header.alg == 'HS256') then
        return nil
    end
    return jwt
end

function Helpers.getJwtCookie()
    local cookie_name = jwtCookieName
    if Helpers.isEmpty(cookie_name) then
        cookie_name='account'
    end
    cookie_name='cookie_' .. cookie_name
    return ngx.var[cookie_name]
end

function Helpers.cache()
    local cache=_G.groupMembershipCache
    if cache == nil then
        local lrucache = require('resty.lrucache')
        _G.groupMembershipCache=lrucache.new(100 * 1000);
        cache=_G.groupMembershipCache
    end
    return cache
end

return M
edjiang commented 8 years ago

Hey @reggiepierce, this is a great idea -- thanks so much for sharing! This is actually really interesting for me, because of two reasons:

Let's set up some time to chat more about how you're using the nginx plugin, and how we can better support it =] If it's supporting more web based use cases, we can figure out how we can put this in the design. I'll email you with some good times.

(Also, it might be easier to show this code by making a fork of this repo!)

reggiepierce commented 8 years ago

Hi @edjiang,

I really should have just created a fork. However, I just kept noticing bugs and so I kept updating. Sorry for the clutter.

As per custom data, I can see that there may be potential use cases, but at this time they don't apply to my team. (Maybe requiring information from 3rd party providers like Facebook)

javierbq commented 8 years ago

@edjiang: My company runs many internal services that we would love to access through the internet but we don't wont to make them publicly accessible. We also don't want to manage individual accounts for all this services.

Group level permission would allow us grant access to different parts of our infrastructure to members of my organization very easily.

Any chance stormpath can get @reggiepierce modifications merged?

edjiang commented 8 years ago

Hey @javierbq, are you currently using the nginx plugin, or just looking for a good solution for your needs? I'm happy to discuss how the current integrations we have could potentially make this work.

I discussed this with Reggie on the phone, and while his contributions are great, it'll definitely take more work to make this useful on a more general level, and document it, etc =]