Skip to content

Commit 10d21df

Browse files
author
Maria Pilar Guerra Arias
committed
Initial commit
0 parents  commit 10d21df

File tree

9 files changed

+912
-0
lines changed

9 files changed

+912
-0
lines changed

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: start_nginx.sh

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## 3Scale API Proxy with OAuth2 Authorization Code Flow hosted on Heroku
2+
3+
This is based on Taytay's excellent implementation of a 3scale API Proxy using Heroku: Taytay/api-proxy-3scale-heroku
4+
5+
Please check out his [repo](Taytay/api-proxy-3scale-heroku) for the README and basic instructions on setting this up.
6+
7+
I have added some OAuth extensions on top of this to implement an API Gateway acting as an OAuth2 provider for my simple Address Book App API. I will outline how these work (as well as any additional set up steps where they differ from the original repo) and how to use them in conjunction with the example [Address Book App API](mpguerra/address-book-app-api)
8+
9+
Usage
10+
---------
11+
12+
#### Step 1: Get 3Scale and Heroku Accounts ####
13+
14+
##### Step 1a: Get RedisToGo addon for Heroku #####
15+
16+
TODO: Instructions for installing and connecting to ReidsToGo
17+
18+
#### Step 2: Configure 3Scale Api Proxy and download Nginx config files ####
19+
20+
You will need to choose oauth authentication mode from the API Settings for Authentication Mode.
21+
22+
#### Step 3: Clone this repo ####
23+
24+
#### Step 4: Rename the generated .conf files ####
25+
26+
#### Step 5: Modify nginx.conf ####
27+
Make the following mandatory modifications to the nginx.conf file:
28+
29+
#1. Add this line to the top of the file
30+
daemon off;
31+
#2. replace 'listen 80;' with:
32+
listen ${{PORT}};
33+
#3. replace 'access_by_lua_file lua_tmp.lua;' with:
34+
access_by_lua_file nginx_3scale_access.lua;
35+
36+
See the sample **nginx.sample.conf** file for details, and for notes on other optional changes you can make.
37+
38+
39+
Test your API proxy using an app_id and app_key you get from your 3scale control panel. More info about these credentials [here](https://support.3scale.net/howtos/api-configuration/nginx-proxy)
40+
41+
$ curl http://<heroku-app-name>.herokuapp.com/v1/word/awesome.json\?app_id\=YOUR_USER_APP_ID\&app_key\=YOUR_USER_APP_KEY
42+
43+
{"word":"awesome","sentiment":4}%
44+
45+
46+
Credits
47+
-------
48+
49+
The [OpenResty buildpack](https://github.com/leafo/heroku-openresty) did the hard Heroku work! Thanks!
50+
51+
Our thanks to Taylor Brown [Taytay](http://taytay.com/) for providing the base configuration and files.
52+
53+
I'm Maria Pilar Guerra-Arias (aka Pili) an API Solution Engineer at [3scale](http://www.3scale.net)

authorize.lua

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
local cjson = require 'cjson'
2+
local ts = require 'threescale_utils'
3+
local redis = require 'resty.redis'
4+
local red = redis:new()
5+
6+
function check_return_url(client_id, return_url)
7+
local res = ngx.location.capture("/_threescale/redirect_uri_matches",
8+
{ vars = { red_url = return_url ,
9+
client_id = client_id }})
10+
return (res.status == 200)
11+
end
12+
13+
-- returns 2 values: ok, err
14+
-- if ok == true, err is undefined
15+
-- if ok == false, err contains the errors
16+
function authorize(params)
17+
-- scope is required because the provider needs to know which plan
18+
-- is the user trying to log to
19+
local required_params = {'client_id', 'redirect_uri', 'response_type', 'scope'}
20+
21+
if ts.required_params_present(required_params, params) and
22+
params["response_type"] == 'code' and
23+
check_return_url(params.client_id, params.redirect_uri) then
24+
redirect_to_login(params)
25+
elseif params["response_type"] ~= 'code' then
26+
return false, 'unsupported_response_type'
27+
else
28+
return false, 'invalid_request'
29+
end
30+
ts.error("we should never be here")
31+
end
32+
33+
-- returns a unique string for the client_id. it will be short lived
34+
function nonce(client_id)
35+
return ts.sha1_digest(math.random() .. "#login:" .. client_id)
36+
end
37+
38+
function generate_access_token(client_id)
39+
return ts.sha1_digest(math.random() .. client_id)
40+
end
41+
42+
-- redirects_to the login form of the API provider with a secret
43+
-- 'state' which will be used when the form redirects the user back to
44+
-- this server.
45+
function redirect_to_login(params)
46+
local n = nonce(params.client_id)
47+
48+
params.scope = params.scope
49+
ts.connect_redis(red)
50+
local pre_token = generate_access_token(params.client_id)
51+
52+
local ok, err = red:hmset(ngx.var.service_id .. "#tmp_data:".. n,
53+
{client_id = params.client_id,
54+
redirect_uri = params.redirect_uri,
55+
plan_id = params.scope,
56+
pre_access_token = pre_token})
57+
58+
if not ok then
59+
ts.error(ts.dump(err))
60+
end
61+
62+
-- TODO: If the login_url has already the parameter state bad
63+
-- things are to happen
64+
ngx.redirect(ngx.var.login_url .. "?&scope=".. params.scope .. "&state=" .. n .. "&tok=".. pre_token)
65+
ngx.exit(ngx.HTTP_OK)
66+
end
67+
68+
local params = ngx.req.get_uri_args()
69+
local _ok, a_err = authorize(params)
70+
71+
if not a_ok then
72+
ngx.redirect(ngx.var.login_url .. "?&scope=" .. params.scope .. "&state=" .. (params.state or '') .. "&error=" .. a_err)
73+
end

authorized_callback.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- authorized_callback.lua
2+
3+
-- Once the client has been authorized by the API provider in their
4+
-- login, the provider is supposed to send the client (via redirect)
5+
-- to this endpoint, with the same status code that we sent him at the
6+
-- moment of the first redirect
7+
8+
local cjson = require 'cjson'
9+
local ts = require 'threescale_utils'
10+
local redis = require 'resty.redis'
11+
local red = redis:new()
12+
13+
local ok, err
14+
local params = ngx.req.get_uri_args()
15+
16+
if ts.required_params_present({'state'}, params) then
17+
ts.connect_redis(red)
18+
local tmp_data = ngx.var.service_id .. "#tmp_data:".. params.state
19+
ok , err = red:exists(tmp_data)
20+
if 0 == ok then
21+
-- TODO: Redirect? to the initial state?
22+
ts.missing_args("state does not exist. Probably expired")
23+
end
24+
ok, err = red:hgetall(tmp_data)
25+
if not ok then
26+
ts.error("no values for tmp_data hash: ".. ts.dump(err))
27+
end
28+
29+
local client_data = red:array_to_hash(ok) -- restoring client data
30+
31+
-- Delete the tmp_data:
32+
red:del(tmp_data)
33+
34+
local code = ts.sha1_digest(ngx.time() .. "#code:" .. client_data.client_id)
35+
ok, err = red:hmset("c:".. client_data.client_id, {client_id = client_data.client_id,
36+
client_secret = client_data.secret_id,
37+
redirect_uri = client_data.redirect_uri,
38+
pre_access_token = client_data.pre_access_token,
39+
code = code,
40+
user_id = params.username })
41+
42+
ok, err = red:expire("c:".. client_data.client_id, 60 * 10) -- code expires in 10 mins
43+
44+
if not ok then
45+
ngx.say("failed to hmset: ", err)
46+
ngx.exit(ngx.HTTP_OK)
47+
end
48+
49+
ngx.req.set_header("Content-Type", "application/x-www-form-urlencoded")
50+
return ngx.redirect(client_data.redirect_uri .. "?code="..code .. "&state=" .. (client_data.state or ""))
51+
else
52+
ts.missing_args("{ 'error': '".. "invalid_client_data from login form" .. "'}")
53+
end

get_token.lua

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
local cjson = require 'cjson'
2+
local redis = require 'resty.redis'
3+
local ts = require 'threescale_utils'
4+
local red = redis:new()
5+
6+
function generate_token(client_id)
7+
return ts.sha1_digest(ngx.time() .. client_id)
8+
end
9+
10+
-- Returns the access token (stored in redis) for the client identified by the id
11+
-- This needs to be called within a minute of it being stored, as it expires and is deleted
12+
function generate_access_token_for(client_id)
13+
local ok, err = ts.connect_redis(red)
14+
ok, err = red:hgetall("c:".. client_id) -- code?
15+
ts.log(ok)
16+
if ok[1] == nil then
17+
ngx.say("expired_code")
18+
return ngx.exit(ngx.HTTP_OK)
19+
else
20+
return red:array_to_hash(ok).pre_access_token..":"..red:array_to_hash(ok).user_id
21+
end
22+
end
23+
24+
local function store_token(client_id, token)
25+
local stored = ngx.location.capture("/_threescale/oauth_store_token",
26+
{method = ngx.HTTP_POST,
27+
body = "provider_key=" ..ngx.var.provider_key ..
28+
"&app_id=".. client_id ..
29+
"&token=".. token})
30+
if stored.status ~= 200 then
31+
ngx.say("eeeerror")
32+
ngx.exit(ngx.HTTP_OK)
33+
end
34+
35+
access_token = token:split(":")[1]
36+
37+
ngx.header.content_type = "application/json; charset=utf-8"
38+
ngx.say({'{"access_token": "'.. access_token .. '", "token_type": "bearer"}'})
39+
ngx.exit(ngx.HTTP_OK)
40+
end
41+
42+
function get_token()
43+
44+
local params = {}
45+
if "GET" == ngx.req.get_method() then
46+
params = ngx.req.get_uri_args()
47+
else
48+
ngx.req.read_body()
49+
params = ngx.req.get_post_args()
50+
end
51+
52+
local required_params = {'client_id', 'redirect_uri', 'client_secret', 'code', 'grant_type'}
53+
54+
if ts.required_params_present(required_params, params) and params['grant_type'] == 'authorization_code' then
55+
local token = generate_access_token_for(params.client_id)
56+
store_token(params.client_id, token)
57+
else
58+
ngx.log(0, "NOPE")
59+
ngx.exit(ngx.HTTP_FORBIDDEN)
60+
end
61+
end
62+
63+
local s = get_token()

0 commit comments

Comments
 (0)