├── LICENSE.md ├── README.md └── oauth2 ├── README.md ├── authorization-code-flow ├── README.md ├── no-token-generation │ ├── get_token.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua └── token-generation │ ├── authorize.lua │ ├── authorized_callback.lua │ ├── get_token.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua ├── client-credentials-flow ├── README.md ├── no-token-generation │ ├── get_token.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua └── token-generation │ ├── get_token.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua ├── implicit-flow ├── README.md ├── no-token-generation │ ├── authorized_callback.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua └── token-generation │ ├── authorize.lua │ ├── authorized_callback.lua │ ├── nginx.conf │ ├── nginx.lua │ └── threescale_utils.lua └── resource-owner-password-flow ├── README.md ├── no-token-generation ├── get_token.lua ├── nginx.conf ├── nginx.lua └── threescale_utils.lua └── token-generation ├── get_token.lua ├── nginx.conf ├── nginx.lua └── threescale_utils.lua /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 3scale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nginx-oauth-templates 2 | ===================== 3 | 4 | Nginx Sample Configs for OAuth Flows 5 | 6 | The repository is organized as follows: 7 | 8 | - oauth2 contains folders sample Nginx config files for OAuth v2 Flows 9 | 10 | Inside each of the different OAuth flow folders there will be 1 or more folders: 11 | 12 | - token-generation: Nginx generates tokens 13 | - no-token-generation: Nginx delegates token generation to Auth Server 14 | 15 | -------------------------------------------------------------------------------- /oauth2/README.md: -------------------------------------------------------------------------------- 1 | # OAuth 2 2 | 3 | ## API Calls 4 | 5 | ### To store an access_token 6 | 7 | `curl -X POST "http://su1.3scale.net/services//oauth_access_tokens.xml?provider_key=&app_id=&token=&ttl="` 8 | 9 | returns nothing 10 | 11 | Params: 12 | 13 | - provider_key 14 | - app_id 15 | - token 16 | - ttl (optional) - 17 | Seconds to expiry. If no ttl is set, it will be a non-expiring token, otherwise it gets automatically deleted once the time is up 18 | 19 | ### To delete an access_token 20 | 21 | `curl -X DELETE "http://su1.3scale.net/services//oauth_access_tokens/.xml?provider_key="` 22 | 23 | returns nothing 24 | 25 | ### To retrieve the access tokens issued to an application 26 | 27 | `curl -X GET "http://su1.3scale.net/services//applications//oauth_access_tokens.xml?provider_key="` 28 | 29 | returns 30 | 31 | ```xml 32 | 33 | 34 | 96c85326-6171-4058-a0e3-261cf73b3b87 35 | 7890f1a8-7df4-4d02-84e4-0c4032cd7074 36 | 78d61fdb-3d02-4fb8-8b92-3d9bdbc2e5fa 37 | 38 | ``` 39 | 40 | ### To get the application a token belongs to 41 | 42 | `curl -X GET "http://su1.3scale.net/services//oauth_access_tokens/.xml?provider_key="` 43 | 44 | returns 45 | 46 | ```xml 47 | 48 | 49 | resourceowner 50 | 51 | ``` 52 | 53 | 54 | ## client-credentials-flow 55 | 56 | ## resource-owner-password-flow 57 | 58 | ## implicit-flow 59 | 60 | ## authorization-code-flow 61 | 62 | ## utils 63 | 64 | Contains threescale_utils.lua file which is used with all flows. 65 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/README.md: -------------------------------------------------------------------------------- 1 | # Authorization Code / Server-Side Web Applications Flow 2 | 3 | ## Token Generation 4 | 5 | ### Requirements 6 | 7 | You will need to: 8 | 9 | * Install Redis on your Nginx Server (see below for instructions) 10 | * Find all instances of CHANGE_ME in the config files and replace them with the correct values for your API 11 | * Place threescale_utils.lua in /opt/openresty/lualib/threescale_utils.lua 12 | 13 | #### Installing Redis 14 | 15 | Download and install redis on Nginx server (we use version 2.6.16 which is the currently stable version at the time of writing this) 16 | 17 | ``` 18 | tar zxvf redis-VERSION.tar.gz 19 | cd redis-VERSION 20 | make 21 | sudo make install 22 | ``` 23 | 24 | In order to to install and run redis server you will need to run the following, accepting all the default values: 25 | 26 | `sudo ./utils/install_server.sh` 27 | 28 | ### Files 29 | 30 | - `authorize.lua` - This file contains the logic for authorizing the client, redirecting the end_user to the oAuth login page, generating the access token and checking that the return url matches the one specified by the API buyer. It runs when the /authorize endpoint is hit. 31 | - `authorized_callback.lua` - This file contains the logic for redirecting an API end user back to the API buyer’s redirect url. As an API provider, you will need to call this endpoint once your user successfully logs in and authorizes the API buyer’s requested access. This file gets executed when the /callback endpoint is called by your application. 32 | - `get_token.lua` - This file contains the logic to return the access token for the client identified by a client_id. It gets executed when the /oauth/token endpoint is called. 33 | - `nginx.conf` - This is a typical Nginx config file. Feel free to edit it or to copy paste it to your existing .conf if you are already running Nginx. 34 | - `nginx.lua` - This file contains the logic that you defined on the web interface to track usage for various metrics and methods as well as checking for authorization to access the various endpoints. 35 | 36 | ### Usage 37 | 38 | To get an authorization code you will need to call the authorize endpoint on the Nginx server, e.g if it's running on localhost you would visit 39 | 40 | `https://localhost/authorize?client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground%2F&response_type=code&scope=SCOPE` 41 | 42 | If credentials are correct and user grants access, the temporary authorization code will be returned at the redirect_uri specified, e.g if your redirect uri is the google oauth developer playground you will get the code returned as per the below: 43 | 44 | `https://developers.google.com/oauthplayground/?code=AUTHORIZATION_CODE&state=STATE_VALUE` 45 | 46 | You can then use that temporary code to exchange for an access token by calling the /oauth/token endpoint, e.g 47 | 48 | `curl -v -X POST "https://localhost/oauth/token" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&redirect_uri=REDIRECT_URI&code=AUTHORIZATION_CODE&grant_type=authorization_code"` 49 | 50 | ## No Token Generation 51 | 52 | ### Requirements 53 | 54 | You will need to: 55 | 56 | * Find all instances of CHANGE_ME in the config files and replace them with the correct values for your API 57 | * Place threescale_utils.lua in /opt/openresty/lualib/threescale_utils.lua 58 | 59 | _Please note, you will NOT need to install Redis for this flow_ 60 | 61 | ### Files 62 | 63 | - `get_token.lua` - This file contains the logic to return the access token for the client identified by a client_id. It gets executed when the /oauth/token endpoint is called. 64 | - `nginx.conf` - This is a typical Nginx config file. Feel free to edit it or to copy paste it to your existing .conf if you are already running Nginx. 65 | - `nginx.lua` - This file contains the logic that you defined on the web interface to track usage for various metrics and methods as well as checking for authorization to access the various endpoints. 66 | 67 | ### Usage 68 | 69 | Since the Token generation is performed by an Authorization Server other than Nginx, you will visit your Authorization Server to get an authorization code first and then request the access_token through the Nginx server by visiting the /oauth/token endpoint, e.g if Nginx is on localhost 70 | 71 | `curl -v -X POST "https://localhost/oauth/token" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&redirect_uri=REDIRECT_URI&code=AUTHORIZATION_CODE&grant_type=authorization_code"` 72 | 73 | This will return the access token in the following form: 74 | 75 | {"access_token": ACCESS_TOKEN, "expires_in": TTL, "token_type": "bearer", "refresh_token": REFRESH_TOKEN} 76 | 77 | If your Authorization Server supports refresh tokens, you can request a new access token by making the following call: 78 | 79 | `curl -v -X POST "https://localhost/oauth/token" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN&grant_type=refresh_token"` 80 | 81 | ### Notes 82 | 83 | The files above make the following assumptions about your Authorization Server: 84 | 85 | 1. Authorization Server supports refresh tokens 86 | 2. The parameters that your Authorization Server needs in order to issue authorization codes/access tokens 87 | 3. The access tokens have a limited ttl 88 | 89 | If your Authorization Server does not support/match all of the above, you will need to modify the templates accordingly. 90 | 91 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/no-token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | 4 | -- As per RFC for Authorization Code flow: extract params from Authorization header and body 5 | -- If implementation deviates from RFC, this function should be over-ridden 6 | function extract_params() 7 | local params = {} 8 | local header_params = ngx.req.get_headers() 9 | 10 | params.authorization = {} 11 | 12 | if header_params['Authorization'] then 13 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 14 | end 15 | 16 | ngx.req.read_body() 17 | local body_params = ngx.req.get_post_args() 18 | 19 | params.client_id = params.authorization[1] or body_params.client_id 20 | params.client_secret = params.authorization[2] or body_params.client_secret 21 | 22 | params.grant_type = body_params.grant_type 23 | params.redirect_uri = body_params.redirect_uri or body_params.redirect_url 24 | 25 | if params.grant_type == "refresh_token" then 26 | params.refresh_token = body_params.refresh_token 27 | else 28 | params.code = body_params.code 29 | end 30 | 31 | return params 32 | end 33 | 34 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 35 | function check_client_credentials(params) 36 | local res = ngx.location.capture("/_threescale/check_credentials", 37 | { args = { app_id = params.client_id, app_key = params.client_secret, redirect_uri = params.redirect_uri }}) 38 | return res.status == 200 39 | end 40 | 41 | -- Get the token from the OAuth Server 42 | function get_token(params) 43 | local access_token_required_params = {'client_id', 'client_secret', 'grant_type', 'code', 'redirect_uri'} 44 | local refresh_token_required_params = {'client_id', 'client_secret', 'grant_type', 'refresh_token'} 45 | 46 | local res = {} 47 | 48 | if (ts.required_params_present(access_token_required_params, params) and params['grant_type'] == 'authorization_code') or 49 | (ts.required_params_present(refresh_token_required_params, params) and params['grant_type'] == 'refresh_token') then 50 | res = request_token(params) 51 | else 52 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 53 | end 54 | 55 | if res.status ~= 200 then 56 | ngx.status = res.status 57 | ngx.header.content_type = "application/json; charset=utf-8" 58 | ngx.print(res.body) 59 | ngx.exit(ngx.HTTP_FORBIDDEN) 60 | else 61 | local token = parse_token(res.body) 62 | local stored = store_token(params, token) 63 | 64 | if stored.status ~= 200 then 65 | ngx.say('{"error":"'..stored.body..'"}') 66 | ngx.status = stored.status 67 | ngx.exit(ngx.HTTP_OK) 68 | else 69 | send_token(token) 70 | end 71 | end 72 | end 73 | 74 | -- Calls the token endpoint to request a token 75 | function request_token() 76 | local res = ngx.location.capture("/_oauth/token", { method = ngx.HTTP_POST, copy_all_vars = true }) 77 | return { ["status"] = res.status, ["body"] = res.body } 78 | end 79 | 80 | -- Parses the token - in this case we assume a json encoded token. This function may be overwritten to parse different token formats. 81 | function parse_token(body) 82 | local token = cjson.decode(body) 83 | return token 84 | end 85 | 86 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 87 | function store_token(params, token) 88 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = token.expires_in }) 89 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 90 | stored.body = stored.body or stored.status 91 | return { ["status"] = stored.status , ["body"] = stored.body } 92 | end 93 | 94 | -- Returns the token to the client 95 | function send_token(token) 96 | ngx.header.content_type = "application/json; charset=utf-8" 97 | ngx.say(cjson.encode(token)) 98 | ngx.exit(ngx.HTTP_OK) 99 | end 100 | 101 | local params = extract_params() 102 | 103 | local is_valid = check_client_credentials(params) 104 | 105 | if is_valid then 106 | get_token(params) 107 | else 108 | ngx.status = 401 109 | ngx.header.content_type = "application/json; charset=utf-8" 110 | ngx.print('{"error":"invalid_client"}') 111 | ngx.exit(ngx.HTTP_OK) 112 | end -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/no-token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | upstream backend_CHANGE_ME_IDP_BACKEND { 28 | server CHANGE_ME_IDP_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 29 | } 30 | 31 | server { 32 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 33 | lua_code_cache off; 34 | listen 80; 35 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 36 | server_name CHANGE_ME_SERVER_NAME; 37 | underscores_in_headers on; 38 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 39 | set $threescale_backend "https://su1.3scale.net:443"; 40 | 41 | location = /authorize { 42 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | 46 | ## CHANGE TO THE authorize path for your chosen Identity Provider 47 | proxy_pass https://backend_CHANGE_ME_IDP_BACKEND/oauth2/authorize; 48 | } 49 | 50 | location = /_threescale/check_credentials { 51 | internal; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 55 | 56 | set $provider_key CHANGE_ME_PROVIDER_KEY; 57 | set $service_id CHANGE_ME_SERVICE_ID; 58 | 59 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 60 | } 61 | 62 | location = /oauth/token { 63 | proxy_set_header X-Real-IP $remote_addr; 64 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 65 | proxy_set_header Host $http_host; 66 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 67 | 68 | set $provider_key CHANGE_ME_PROVIDER_KEY; 69 | 70 | content_by_lua_file get_token.lua ; 71 | } 72 | 73 | location = /_oauth/token { 74 | internal; 75 | proxy_set_header X-Real-IP $remote_addr; 76 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 77 | proxy_set_header Host $http_host; 78 | more_clear_input_headers Accept-Encoding; 79 | 80 | proxy_redirect off; 81 | proxy_max_temp_file_size 0; 82 | proxy_pass https://CHANGE_ME_IDP_BACKEND/token; 83 | } 84 | 85 | location = /_threescale/oauth_store_token { 86 | internal; 87 | proxy_set_header X-Real-IP $remote_addr; 88 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 89 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 90 | 91 | set $provider_key CHANGE_ME_PROVIDER_KEY; 92 | set $service_id CHANGE_ME_SERVICE_ID; 93 | 94 | proxy_method POST; 95 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 96 | } 97 | 98 | location = /threescale_oauth_authrep { 99 | internal; 100 | proxy_set_header Host "su1.3scale.net"; 101 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 102 | proxy_set_header X-3scale-OAuth2-Grant-Type "authorization_code"; 103 | 104 | set $provider_key CHANGE_ME_PROVIDER_KEY; 105 | set $service_id CHANGE_ME_SERVICE_ID; 106 | 107 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 108 | } 109 | 110 | location = /threescale_authrep { 111 | internal; 112 | set $provider_key CHANGE_ME_PROVIDER_KEY; 113 | 114 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 115 | proxy_set_header Host "su1.3scale.net"; 116 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 117 | } 118 | 119 | location = /out_of_band_oauth_authrep_action { 120 | internal; 121 | proxy_pass_request_headers off; 122 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 123 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 124 | set $provider_key CHANGE_ME_PROVIDER_KEY; 125 | 126 | 127 | content_by_lua "require('nginx').post_action_content()"; 128 | } 129 | 130 | location / { 131 | set $provider_key null; 132 | set $cached_key null; 133 | set $credentials null; 134 | set $usage null; 135 | set $service_id CHANGE_ME_SERVICE_ID; 136 | set $proxy_pass null; 137 | set $secret_token null; 138 | set $resp_body null; 139 | set $resp_headers null; 140 | set $access_token null; 141 | ## Add user_id value if required 142 | # e.g set $user_id null; 143 | 144 | proxy_ignore_client_abort on; 145 | 146 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 147 | access_by_lua "require('nginx').access()"; 148 | 149 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 150 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 151 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 152 | 153 | proxy_pass $proxy_pass ; 154 | proxy_set_header X-Real-IP $remote_addr; 155 | proxy_set_header Host $host; 156 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 157 | ## Send user_id to API if required 158 | # e.g proxy_set_header user_id $user_id; 159 | 160 | post_action /out_of_band_oauth_authrep_action; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/no-token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | -- If required: extract user_id token belongs to and compare with value from auth response 269 | -- local user_id = res.body:match('user_id="(%S-)">'..access_token) 270 | -- if user_id == params.user_id then 271 | -- Set this value if you need to send user_id back to your API 272 | -- ngx.var.user_id = user_id 273 | access_tokens:set(ngx.var.cached_key,200) 274 | -- else 275 | -- access_tokens:delete(ngx.var.cached_key) 276 | -- ngx.status = res.status 277 | -- ngx.header.content_type = "application/json" 278 | -- ngx.var.cached_key = nil 279 | -- error_authorization_failed(service) 280 | -- end 281 | end 282 | 283 | ngx.var.cached_key = nil 284 | end 285 | end 286 | 287 | function authrep(params, service) 288 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 289 | local api_keys = ngx.shared.api_keys 290 | local is_known = api_keys:get(ngx.var.cached_key) 291 | 292 | if is_known ~= 200 then 293 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 294 | 295 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 296 | if res.status ~= 200 then 297 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 298 | api_keys:delete(ngx.var.cached_key) 299 | ngx.status = res.status 300 | ngx.header.content_type = "application/json" 301 | ngx.var.cached_key = nil 302 | error_authorization_failed(service) 303 | else 304 | api_keys:set(ngx.var.cached_key,200) 305 | end 306 | 307 | ngx.var.cached_key = nil 308 | end 309 | end 310 | 311 | function add_trans(usage) 312 | local us = usage:split("&") 313 | local ret = "" 314 | for i,v in ipairs(us) do 315 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 316 | end 317 | return string.sub(ret, 1, -2) 318 | end 319 | 320 | 321 | function _M.access() 322 | local params = {} 323 | local host = ngx.req.get_headers()["Host"] 324 | local auth_strat = "" 325 | local service = {} 326 | 327 | if ngx.status == 403 then 328 | ngx.say("Throttling due to too many requests") 329 | ngx.exit(403) 330 | end 331 | 332 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 333 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 334 | service = service_CHANGE_ME_SERVICE_ID -- 335 | ngx.var.secret_token = service.secret_token 336 | 337 | -- If relevant, extract user_id from request 338 | -- e.g local user_id = ngx.re.match(ngx.var.uri,[=[^/api/user/([\w_\.-]+)\.json]=]) 339 | -- params.user_id = user_id 340 | 341 | -- Do this to extract token from Authorization: Bearer header 342 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 343 | -- ngx.var.access_token = params.access_token 344 | 345 | ngx.var.access_token = parameters.access_token 346 | params.access_token = parameters.access_token 347 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 348 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token .. ( params.user_id and ":" .. params.user_id or "" ) 349 | auth_strat = "oauth" 350 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 351 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 352 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 353 | end 354 | 355 | ngx.var.credentials = build_query(params) 356 | 357 | -- if true then 358 | -- log(ngx.var.app_id) 359 | -- log(ngx.var.app_key) 360 | -- log(ngx.var.usage) 361 | -- end 362 | 363 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 364 | if ngx.var.usage == nil then 365 | ngx.header["X-3scale-matched-rules"] = '' 366 | error_no_match(service) 367 | end 368 | 369 | if get_debug_value() then 370 | ngx.header["X-3scale-matched-rules"] = matched_rules2 371 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 372 | ngx.header["X-3scale-usage"] = ngx.var.usage 373 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 374 | end 375 | 376 | authorize(auth_strat, params, service) 377 | 378 | end 379 | 380 | 381 | function _M.post_action_content() 382 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 383 | 384 | local req = cjson.encode{method=method, path=path, headers=headers} 385 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 386 | 387 | local cached_key = ngx.var.cached_key 388 | if cached_key ~= nil and cached_key ~= "null" then 389 | local status_code = ngx.var.status 390 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 391 | if res1.status ~= 200 then 392 | local access_tokens = ngx.shared.api_keys 393 | access_tokens:delete(cached_key) 394 | end 395 | end 396 | 397 | ngx.exit(ngx.HTTP_OK) 398 | end 399 | 400 | 401 | return _M 402 | 403 | -- END OF SCRIPT 404 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/no-token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/token-generation/authorize.lua: -------------------------------------------------------------------------------- 1 | local random = require 'resty.random' 2 | local ts = require 'threescale_utils' 3 | local redis = require 'resty.redis' 4 | local red = redis:new() 5 | 6 | -- As per RFC for Authorization Code flow: extract params from request uri 7 | -- If implementation deviates from RFC, this function should be over-ridden 8 | function extract_params() 9 | local params = {} 10 | local uri_params = ngx.req.get_uri_args() 11 | 12 | params.response_type = uri_params.response_type 13 | params.client_id = uri_params.client_id 14 | params.redirect_uri = uri_params.redirect_uri 15 | params.scope = uri_params.scope 16 | params.state = uri_params.state 17 | 18 | return params 19 | end 20 | 21 | -- Check valid credentials 22 | function check_credentials(params) 23 | local res = check_client_credentials(params) 24 | return res.status == 200 25 | end 26 | 27 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 28 | function check_client_credentials(params) 29 | local res = ngx.location.capture("/_threescale/check_credentials", 30 | { args = { app_id = params.client_id, app_key = params.client_secret, redirect_uri = params.redirect_uri }}) 31 | 32 | if res.status ~= 200 then 33 | params.error = "invalid_client" 34 | redirect_to_auth(params) 35 | end 36 | 37 | return { ["status"] = res.status, ["body"] = res.body } 38 | end 39 | 40 | -- Authorizes the client for the given scope 41 | function authorize(params) 42 | local required_params = {'client_id', 'redirect_uri', 'response_type', 'scope'} 43 | 44 | if params["response_type"] ~= 'code' then 45 | params.error = "unsupported_response_type" 46 | elseif not ts.required_params_present(required_params, params) then 47 | params.error = "invalid_request" 48 | end 49 | 50 | redirect_to_auth(params) 51 | end 52 | 53 | -- redirects_to the authorization url of the API provider with a secret 54 | -- 'state' which will be used when the form redirects the user back to 55 | -- this server. 56 | function redirect_to_auth(params) 57 | 58 | if not params.error then 59 | local n = nonce(params.client_id) 60 | params.state = n 61 | 62 | ts.connect_redis(red) 63 | local pre_token = generate_access_token(params.client_id) 64 | params.tok = pre_token 65 | 66 | local ok, err = red:hmset(ngx.var.service_id .. "#tmp_data:".. n, 67 | {client_id = params.client_id, 68 | redirect_uri = params.redirect_uri, 69 | plan_id = params.scope, 70 | access_token = pre_token}) 71 | 72 | if not ok then 73 | ts.error(ts.dump(err)) 74 | end 75 | end 76 | 77 | local args = ts.build_query(params) 78 | 79 | ngx.header.content_type = "application/x-www-form-urlencoded" 80 | return ngx.redirect( ngx.var.auth_url .. "?" .. args ) 81 | end 82 | 83 | -- returns a unique string for the client_id. it will be short lived 84 | function nonce(client_id) 85 | return ts.sha1_digest(tostring(random.bytes(20, true)) .. "#login:" .. client_id) 86 | end 87 | 88 | function generate_access_token(client_id) 89 | return ts.sha1_digest(tostring(random.bytes(20, true)) .. client_id) 90 | end 91 | 92 | local params = extract_params() 93 | 94 | local is_valid = check_credentials(params) 95 | 96 | if is_valid then 97 | authorize(params) 98 | else 99 | params.error = "invalid_client" 100 | redirect_to_auth(params) 101 | end -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/token-generation/authorized_callback.lua: -------------------------------------------------------------------------------- 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 | local random = require 'resty.random' 8 | local ts = require 'threescale_utils' 9 | local redis = require 'resty.redis' 10 | local red = redis:new() 11 | 12 | -- The authorization server should send some data in the callback response to let the 13 | -- API Gateway know which user to associate with the token. 14 | -- We assume that this data will be sent as uri params. 15 | -- This function should be over-ridden depending on authorization server implementation. 16 | function extract_params() 17 | local params = {} 18 | local uri_params = ngx.req.get_uri_args() 19 | 20 | params.user_id = uri_params.user_id or uri_params.username 21 | params.state = uri_params.state 22 | -- In case state is no longer valid, authorization server might send this so we know where to redirect with error 23 | params.redirect_uri = uri_params.redirect_uri or uri_params.redirect_url 24 | 25 | return params 26 | end 27 | 28 | -- Check valid state parameter sent 29 | function check_state(params) 30 | local required_params = {'state'} 31 | 32 | local valid_state = false 33 | if ts.required_params_present(required_params, params) then 34 | local tmp_data = ngx.var.service_id .. "#tmp_data:".. params.state 35 | local ok = red:exists(tmp_data) 36 | 37 | if ok == 0 then 38 | ngx.header.content_type = "application/x-www-form-urlencoded" 39 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=invalid_or_expired_state&state="..params.state) 40 | end 41 | 42 | valid_state = true 43 | else 44 | ngx.header.content_type = "application/x-www-form-urlencoded" 45 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=missing_state") 46 | end 47 | 48 | return valid_state 49 | end 50 | 51 | -- Get Authorization Code 52 | function get_code(params) 53 | local client_data = retrieve_client_data(params) 54 | local code = generate_code(client_data) 55 | 56 | local stored = store_code(client_data, params, code) 57 | 58 | if stored then 59 | send_code(client_data, params, code) 60 | end 61 | end 62 | 63 | -- Retrieve client data from Redis 64 | function retrieve_client_data(params) 65 | local tmp_data = ngx.var.service_id .. "#tmp_data:".. params.state 66 | 67 | ts.connect_redis(red) 68 | local ok, err = red:hgetall(tmp_data) 69 | 70 | if not ok then 71 | ngx.log(0, "no values for tmp_data hash: ".. ts.dump(err)) 72 | ngx.header.content_type = "application/x-www-form-urlencoded" 73 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=invalid_or_expired_state&state=" .. (params.state or "")) 74 | end 75 | 76 | -- Restore client data 77 | local client_data = red:array_to_hash(ok) -- restoring client data 78 | -- Delete the tmp_data: 79 | red:del(tmp_data) 80 | 81 | return client_data 82 | end 83 | 84 | -- Generate authorization code from params 85 | function generate_code(client_data) 86 | return ts.sha1_digest(tostring(random.bytes(20, true)) .. "#code:" .. client_data.client_id) 87 | end 88 | 89 | function store_code(client_data, params, code) 90 | local ok, err = red:hmset("c:".. code, {client_id = client_data.client_id, 91 | client_secret = client_data.secret_id, 92 | redirect_uri = client_data.redirect_uri, 93 | access_token = client_data.access_token, 94 | user_id = params.user_id, 95 | code = code }) 96 | 97 | ok, err = red:expire("c:".. code, 60 * 10) -- code expires in 10 mins 98 | 99 | if not ok then 100 | ngx.header.content_type = "application/x-www-form-urlencoded" 101 | return ngx.redirect(params.redirect_uri .. "?error=server_error&error_description=code_storage_failed&state=" .. (params.state or "")) 102 | end 103 | 104 | return ok 105 | end 106 | 107 | -- Returns the code to the client 108 | function send_code(client_data, params, code) 109 | ngx.header.content_type = "application/x-www-form-urlencoded" 110 | return ngx.redirect( client_data.redirect_uri .. "?code="..code.."&state=" .. (params.state or "")) 111 | end 112 | 113 | local params = extract_params() 114 | 115 | local is_valid = check_state(params) 116 | 117 | if is_valid then 118 | get_code(params) 119 | end -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | local redis = require 'resty.redis' 4 | local red = redis:new() 5 | 6 | -- As per RFC for Authorization Code flow: extract params from Authorization header and body 7 | -- If implementation deviates from RFC, this function should be over-ridden 8 | function extract_params() 9 | local params = {} 10 | local header_params = ngx.req.get_headers() 11 | 12 | params.authorization = {} 13 | 14 | if header_params['Authorization'] then 15 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 16 | end 17 | 18 | ngx.req.read_body() 19 | local body_params = ngx.req.get_post_args() 20 | 21 | params.client_id = params.authorization[1] or body_params.client_id 22 | params.client_secret = params.authorization[2] or body_params.client_secret 23 | 24 | params.grant_type = body_params.grant_type 25 | params.code = body_params.code 26 | params.redirect_uri = body_params.redirect_uri or body_params.redirect_url 27 | 28 | return params 29 | end 30 | 31 | -- Check valid credentials 32 | function check_credentials(params) 33 | local res = check_client_credentials(params) 34 | return res.status == 200 35 | end 36 | 37 | 38 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 39 | function check_client_credentials(params) 40 | local res = ngx.location.capture("/_threescale/check_credentials", 41 | { args = { app_id = params.client_id, app_key = params.client_secret, redirect_uri = params.redirect_uri }, 42 | copy_all_vars = true }) 43 | 44 | if res.status ~= 200 then 45 | ngx.status = 401 46 | ngx.header.content_type = "application/json; charset=utf-8" 47 | ngx.print('{"error":"invalid_client"}') 48 | ngx.exit(ngx.HTTP_OK) 49 | end 50 | 51 | return { ["status"] = res.status, ["body"] = res.body } 52 | end 53 | 54 | -- Get the token from Redis 55 | function get_token(params) 56 | local required_params = {'client_id', 'client_secret', 'grant_type', 'code', 'redirect_uri'} 57 | 58 | local res = {} 59 | local token = {} 60 | 61 | if ts.required_params_present(required_params, params) and params['grant_type'] == 'authorization_code' then 62 | res = request_token(params) 63 | else 64 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 65 | end 66 | 67 | if res.status ~= 200 then 68 | ngx.status = res.status 69 | ngx.header.content_type = "application/json; charset=utf-8" 70 | ngx.print(res.body) 71 | ngx.exit(ngx.HTTP_FORBIDDEN) 72 | else 73 | token = res.body 74 | local stored = store_token(params, token) 75 | 76 | if stored.status ~= 200 then 77 | ngx.say('{"error":"'..stored.body..'"}') 78 | ngx.exit(stored.status) 79 | else 80 | send_token(token) 81 | end 82 | end 83 | end 84 | 85 | -- Returns the access token (stored in redis) for the client identified by the id 86 | -- This needs to be called within a minute of it being stored, as it expires and is deleted 87 | function request_token(params) 88 | local ok, err = ts.connect_redis(red) 89 | ok, err = red:hgetall("c:".. params.code) 90 | 91 | if ok[1] == nil then 92 | return { ["status"] = 403, ["body"] = '{"error": "expired_code"}' } 93 | else 94 | local client_data = red:array_to_hash(ok) 95 | params.user_id = client_data.user_id 96 | if params.code == client_data.code then 97 | return { ["status"] = 200, ["body"] = { ["access_token"] = client_data.access_token, ["token_type"] = "bearer", ["expires_in"] = 604800 } } 98 | else 99 | return { ["status"] = 403, ["body"] = '{"error": "invalid authorization code"}' } 100 | end 101 | end 102 | end 103 | 104 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 105 | function store_token(params, token) 106 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = token.expires_in }) 107 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 108 | stored.body = stored.body or stored.status 109 | return { ["status"] = stored.status , ["body"] = stored.body } 110 | end 111 | 112 | -- Returns the token to the client 113 | function send_token(token) 114 | ngx.header.content_type = "application/json; charset=utf-8" 115 | ngx.say(cjson.encode(token)) 116 | ngx.exit(ngx.HTTP_OK) 117 | end 118 | 119 | local params = extract_params() 120 | 121 | local is_valid = check_credentials(params) 122 | 123 | if is_valid then 124 | get_token(params) 125 | end -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | server { 28 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 29 | lua_code_cache off; 30 | listen 80; 31 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 32 | server_name CHANGE_ME_SERVER_NAME; 33 | underscores_in_headers on; 34 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 35 | set $threescale_backend "https://su1.3scale.net:443"; 36 | 37 | location = /authorize { 38 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 39 | 40 | set $red_url ""; 41 | set $client_id ""; 42 | set $auth_url "CHANGE_ME_AUTH_URL"; 43 | set $service_id CHANGE_ME_SERVICE_ID; 44 | 45 | content_by_lua_file authorize.lua; 46 | } 47 | 48 | location = /_threescale/check_credentials { 49 | internal; 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 53 | 54 | set $provider_key CHANGE_ME_PROVIDER_KEY; 55 | set $service_id CHANGE_ME_SERVICE_ID; 56 | 57 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 58 | } 59 | 60 | location = /callback { 61 | set $service_id CHANGE_ME_SERVICE_ID; 62 | 63 | content_by_lua_file authorized_callback.lua; 64 | } 65 | 66 | location = /oauth/token { 67 | proxy_set_header X-Real-IP $remote_addr; 68 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 69 | proxy_set_header Host $http_host; 70 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 71 | 72 | set $provider_key CHANGE_ME_PROVIDER_KEY; 73 | 74 | content_by_lua_file get_token.lua ; 75 | } 76 | 77 | location = /_threescale/oauth_store_token { 78 | internal; 79 | proxy_set_header X-Real-IP $remote_addr; 80 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 81 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 82 | 83 | set $provider_key CHANGE_ME_PROVIDER_KEY; 84 | set $service_id CHANGE_ME_SERVICE_ID; 85 | 86 | proxy_method POST; 87 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 88 | } 89 | 90 | location = /threescale_oauth_authrep { 91 | internal; 92 | proxy_set_header Host "su1.3scale.net"; 93 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 94 | proxy_set_header X-3scale-OAuth2-Grant-Type "authorization_code"; 95 | 96 | set $provider_key CHANGE_ME_PROVIDER_KEY; 97 | set $service_id CHANGE_ME_SERVICE_ID; 98 | 99 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 100 | } 101 | 102 | location = /threescale_authrep { 103 | internal; 104 | set $provider_key CHANGE_ME_PROVIDER_KEY; 105 | 106 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 107 | proxy_set_header Host "su1.3scale.net"; 108 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 109 | } 110 | 111 | location = /out_of_band_oauth_authrep_action { 112 | internal; 113 | proxy_pass_request_headers off; 114 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 115 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 116 | set $provider_key CHANGE_ME_PROVIDER_KEY; 117 | 118 | 119 | content_by_lua "require('nginx').post_action_content()"; 120 | } 121 | 122 | location / { 123 | set $provider_key null; 124 | set $cached_key null; 125 | set $credentials null; 126 | set $usage null; 127 | set $service_id CHANGE_ME_SERVICE_ID; 128 | set $proxy_pass null; 129 | set $secret_token null; 130 | set $resp_body null; 131 | set $resp_headers null; 132 | set $access_token null; 133 | ## Add user_id value if required 134 | # e.g set $user_id null; 135 | 136 | proxy_ignore_client_abort on; 137 | 138 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 139 | access_by_lua "require('nginx').access()"; 140 | 141 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 142 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 143 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 144 | 145 | proxy_pass $proxy_pass; 146 | proxy_set_header X-Real-IP $remote_addr; 147 | proxy_set_header Host $host; 148 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 149 | ## Send user_id to API if required 150 | # e.g proxy_set_header user_id $user_id; 151 | 152 | post_action /out_of_band_oauth_authrep_action; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /oauth2/authorization-code-flow/token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/README.md: -------------------------------------------------------------------------------- 1 | # Client Credentials Flow 2 | 3 | The calls for requesting an access token are the same regardless of which server generates and manages the access token (Nginx or an additional Authorization Server.) 4 | 5 | ## Requirements 6 | 7 | You will need to: 8 | 9 | * Find all instances of CHANGE_ME in the config files and replace them with the correct values for your API 10 | * Place threescale_utils.lua in /opt/openresty/lualib/threescale_utils.lua 11 | 12 | _Please note, you will NOT need to install Redis for this flow_ 13 | 14 | ## Files 15 | 16 | - `get_token.lua` - This file contains the logic to return the access token for the client identified by a client_id. It gets executed when the /oauth/token endpoint is called. 17 | - `nginx.conf` - This is a typical Nginx config file. Feel free to edit it or to copy paste it to your existing .conf if you are already running Nginx. 18 | - `nginx.lua` - This file contains the logic that you defined on the web interface to track usage for various metrics and methods as well as checking for authorization to access the various endpoints. 19 | 20 | ## Usage 21 | 22 | To get an access token you will need to call the oauth/token endpoint on the Nginx server, e.g if it's running on localhost 23 | 24 | `curl -v -X POST "https://localhost/oauth/token" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=client_credentials"` 25 | 26 | This will return an access token in the following form: 27 | 28 | ```json 29 | {"access_token": "ACCESS_TOKEN", "token_type": "bearer"} 30 | ``` 31 | 32 | You can then call your API using the access_token instead of the client_id/client_secret 33 | 34 | `curl -v -X GET "https://localhost/API_ENDPOINT?access_token=ACCESS_TOKEN"` 35 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/no-token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | 4 | -- As per RFC for Client Credentials flow: extract params from Authorization header and body 5 | -- If implementation deviates from RFC, this function should be over-ridden 6 | function extract_params() 7 | local params = {} 8 | local header_params = ngx.req.get_headers() 9 | 10 | params.authorization = {} 11 | 12 | if header_params['Authorization'] then 13 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 14 | end 15 | 16 | ngx.req.read_body() 17 | local body_params = ngx.req.get_post_args() 18 | 19 | params.client_id = params.authorization[1] or body_params.client_id 20 | params.client_secret = params.authorization[2] or body_params.client_secret 21 | 22 | params.grant_type = body_params.grant_type 23 | 24 | return params 25 | end 26 | 27 | -- Check valid credentials 28 | function check_credentials(params) 29 | local res = check_client_credentials(params) 30 | return res.status == 200 31 | end 32 | 33 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 34 | function check_client_credentials(params) 35 | local res = ngx.location.capture("/_threescale/check_credentials", 36 | { args = { app_id = params.client_id , app_key = params.client_secret , redirect_uri = params.redirect_uri } }) 37 | 38 | if res.status ~= 200 then 39 | ngx.status = 401 40 | ngx.header.content_type = "application/json; charset=utf-8" 41 | ngx.print('{"error":"invalid_client"}') 42 | ngx.exit(ngx.HTTP_OK) 43 | end 44 | 45 | return { ["status"] = res.status, ["body"] = res.body } 46 | end 47 | 48 | -- Get the token from the OAuth Server 49 | function get_token(params) 50 | local required_params = {'grant_type'} 51 | 52 | local res = {} 53 | 54 | if ts.required_params_present(required_params, params) and params['grant_type'] == 'client_credentials' then 55 | res = request_token(params) 56 | else 57 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 58 | end 59 | 60 | if res.status ~= 200 then 61 | ngx.status = res.status 62 | ngx.header.content_type = "application/json; charset=utf-8" 63 | ngx.print(res.body) 64 | ngx.exit(ngx.HTTP_FORBIDDEN) 65 | else 66 | local token = parse_token(res.body) 67 | local stored = store_token(params, token) 68 | 69 | if stored.status ~= 200 then 70 | ngx.say(stored.body) 71 | ngx.status = stored.status 72 | ngx.exit(ngx.HTTP_OK) 73 | else 74 | send_token(token) 75 | end 76 | end 77 | end 78 | 79 | -- Calls the token endpoint to request a token 80 | function request_token() 81 | local res = ngx.location.capture("/_oauth/token", { method = ngx.HTTP_POST, copy_all_vars = true }) 82 | return { ["status"] = res.status, ["body"] = res.body } 83 | end 84 | 85 | -- Parses the token - in this case we assume a json encoded token. This function may be overwritten to parse different token formats. 86 | function parse_token(body) 87 | local token = cjson.decode(body) 88 | return token 89 | end 90 | 91 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 92 | function store_token(params, token) 93 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = (token.expires_in or "604800") }) 94 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 95 | stored.body = stored.body or stored.status 96 | return { ["status"] = stored.status , ["body"] = stored.body } 97 | end 98 | 99 | -- Returns the token to the client 100 | function send_token(token) 101 | ngx.header.content_type = "application/json; charset=utf-8" 102 | ngx.say(cjson.encode(token)) 103 | ngx.exit(ngx.HTTP_OK) 104 | end 105 | 106 | local params = extract_params() 107 | 108 | local is_valid = check_credentials(params) 109 | 110 | if is_valid then 111 | get_token(params) 112 | end -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/no-token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | upstream backend_CHANGE_ME_IDP_BACKEND { 28 | server CHANGE_ME_IDP_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 29 | } 30 | 31 | server { 32 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 33 | lua_code_cache off; 34 | listen 80; 35 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 36 | server_name CHANGE_ME_SERVER_NAME; 37 | underscores_in_headers on; 38 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 39 | set $threescale_backend "https://su1.3scale.net:443"; 40 | 41 | location = /_threescale/check_credentials { 42 | internal; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 46 | 47 | set $provider_key CHANGE_ME_PROVIDER_KEY; 48 | set $service_id CHANGE_ME_SERVICE_ID; 49 | 50 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 51 | } 52 | 53 | location = /oauth/token { 54 | proxy_set_header X-Real-IP $remote_addr; 55 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 56 | proxy_set_header Host $http_host; 57 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 58 | 59 | set $provider_key CHANGE_ME_PROVIDER_KEY; 60 | 61 | content_by_lua_file get_token.lua; 62 | } 63 | 64 | location = /_oauth/token { 65 | internal; 66 | proxy_set_header X-Real-IP $remote_addr; 67 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 68 | proxy_set_header Host $http_host; 69 | more_clear_input_headers Accept-Encoding; 70 | 71 | proxy_redirect off; 72 | proxy_max_temp_file_size 0; 73 | 74 | proxy_method POST; 75 | proxy_pass https://CHANGE_ME_IDP_BACKEND/CHANGE_ME_TOKEN_ENDPOINT; 76 | } 77 | 78 | location = /_threescale/oauth_store_token { 79 | internal; 80 | proxy_set_header X-Real-IP $remote_addr; 81 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 82 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 83 | 84 | set $provider_key CHANGE_ME_PROVIDER_KEY; 85 | set $service_id CHANGE_ME_SERVICE_ID; 86 | 87 | proxy_method POST; 88 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 89 | } 90 | 91 | location = /threescale_oauth_authrep { 92 | internal; 93 | proxy_set_header Host "su1.3scale.net"; 94 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 95 | proxy_set_header X-3scale-OAuth2-Grant-Type "client_credentials"; 96 | 97 | set $provider_key CHANGE_ME_PROVIDER_KEY; 98 | set $service_id CHANGE_ME_SERVICE_ID; 99 | 100 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 101 | } 102 | 103 | location = /threescale_authrep { 104 | internal; 105 | set $provider_key CHANGE_ME_PROVIDER_KEY; 106 | 107 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 108 | proxy_set_header Host "su1.3scale.net"; 109 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 110 | } 111 | 112 | location = /out_of_band_oauth_authrep_action { 113 | internal; 114 | proxy_pass_request_headers off; 115 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 116 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 117 | set $provider_key CHANGE_ME_PROVIDER_KEY; 118 | 119 | 120 | content_by_lua "require('nginx').post_action_content()"; 121 | } 122 | 123 | location / { 124 | set $provider_key null; 125 | set $cached_key null; 126 | set $credentials null; 127 | set $usage null; 128 | set $service_id CHANGE_ME_SERVICE_ID; 129 | set $proxy_pass null; 130 | set $secret_token null; 131 | set $resp_body null; 132 | set $resp_headers null; 133 | set $access_token null; 134 | 135 | proxy_ignore_client_abort on; 136 | 137 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 138 | access_by_lua "require('nginx').access()"; 139 | 140 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 141 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 142 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 143 | 144 | proxy_pass $proxy_pass ; 145 | proxy_set_header X-Real-IP $remote_addr; 146 | proxy_set_header Host $host; 147 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 148 | 149 | post_action /out_of_band_oauth_authrep_action; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/no-token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | access_tokens:set(ngx.var.cached_key,200) 269 | end 270 | 271 | ngx.var.cached_key = nil 272 | end 273 | end 274 | 275 | function authrep(params, service) 276 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 277 | local api_keys = ngx.shared.api_keys 278 | local is_known = api_keys:get(ngx.var.cached_key) 279 | 280 | if is_known ~= 200 then 281 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 282 | 283 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 284 | if res.status ~= 200 then 285 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 286 | api_keys:delete(ngx.var.cached_key) 287 | ngx.status = res.status 288 | ngx.header.content_type = "application/json" 289 | ngx.var.cached_key = nil 290 | error_authorization_failed(service) 291 | else 292 | api_keys:set(ngx.var.cached_key,200) 293 | end 294 | 295 | ngx.var.cached_key = nil 296 | end 297 | end 298 | 299 | function add_trans(usage) 300 | local us = usage:split("&") 301 | local ret = "" 302 | for i,v in ipairs(us) do 303 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 304 | end 305 | return string.sub(ret, 1, -2) 306 | end 307 | 308 | 309 | function _M.access() 310 | local params = {} 311 | local host = ngx.req.get_headers()["Host"] 312 | local auth_strat = "" 313 | local service = {} 314 | 315 | if ngx.status == 403 then 316 | ngx.say("Throttling due to too many requests") 317 | ngx.exit(403) 318 | end 319 | 320 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 321 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 322 | service = service_CHANGE_ME_SERVICE_ID -- 323 | ngx.var.secret_token = service.secret_token 324 | 325 | -- Do this to remove token type, e.g Bearer from token 326 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 327 | -- ngx.var.access_token = params.access_token 328 | 329 | ngx.var.access_token = parameters.access_token 330 | params.access_token = parameters.access_token 331 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 332 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token 333 | auth_strat = "oauth" 334 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 335 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 336 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 337 | end 338 | 339 | ngx.var.credentials = build_query(params) 340 | 341 | -- if true then 342 | -- log(ngx.var.app_id) 343 | -- log(ngx.var.app_key) 344 | -- log(ngx.var.usage) 345 | -- end 346 | 347 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 348 | if ngx.var.usage == nil then 349 | ngx.header["X-3scale-matched-rules"] = '' 350 | error_no_match(service) 351 | end 352 | 353 | if get_debug_value() then 354 | ngx.header["X-3scale-matched-rules"] = matched_rules2 355 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 356 | ngx.header["X-3scale-usage"] = ngx.var.usage 357 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 358 | end 359 | 360 | -- this would be better with the whole authrep call, with user_id, and everything so that 361 | -- it can be replayed if it's a cached response 362 | 363 | authorize(auth_strat, params, service) 364 | 365 | end 366 | 367 | 368 | function _M.post_action_content() 369 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 370 | 371 | local req = cjson.encode{method=method, path=path, headers=headers} 372 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 373 | 374 | local cached_key = ngx.var.cached_key 375 | if cached_key ~= nil and cached_key ~= "null" then 376 | local status_code = ngx.var.status 377 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 378 | if res1.status ~= 200 then 379 | local access_tokens = ngx.shared.api_keys 380 | access_tokens:delete(cached_key) 381 | end 382 | end 383 | 384 | ngx.exit(ngx.HTTP_OK) 385 | end 386 | 387 | 388 | return _M 389 | 390 | -- END OF SCRIPT 391 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/no-token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | local random = require 'resty.random' 4 | 5 | -- As per RFC for Client Credentials flow: extract params from Authorization header and body 6 | -- If implementation deviates from RFC, this function should be over-ridden 7 | function extract_params() 8 | local params = {} 9 | local header_params = ngx.req.get_headers() 10 | 11 | params.authorization = {} 12 | 13 | if header_params['Authorization'] then 14 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 15 | end 16 | 17 | ngx.req.read_body() 18 | local body_params = ngx.req.get_post_args() 19 | 20 | params.client_id = params.authorization[1] or body_params.client_id 21 | params.client_secret = params.authorization[2] or body_params.client_secret 22 | 23 | params.grant_type = body_params.grant_type 24 | 25 | return params 26 | end 27 | 28 | -- Check valid credentials 29 | function check_credentials(params) 30 | local res = check_client_credentials(params) 31 | return res.status == 200 32 | end 33 | 34 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 35 | function check_client_credentials(params) 36 | local res = ngx.location.capture("/_threescale/check_credentials", 37 | { args = { app_id = params.client_id , app_key = params.client_secret , redirect_uri = params.redirect_uri } }) 38 | 39 | if res.status ~= 200 then 40 | ngx.status = 401 41 | ngx.header.content_type = "application/json; charset=utf-8" 42 | ngx.print('{"error":"invalid_client"}') 43 | ngx.exit(ngx.HTTP_OK) 44 | end 45 | 46 | return { ["status"] = res.status, ["body"] = res.body } 47 | end 48 | 49 | -- Get the token from the Gateway 50 | function get_token(params) 51 | local required_params = {'grant_type'} 52 | 53 | local res = {} 54 | local token = {} 55 | 56 | if ts.required_params_present(required_params, params) and params['grant_type'] == 'client_credentials' then 57 | token = generate_token(params.client_id) 58 | res = store_token(params, token) 59 | else 60 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 61 | end 62 | 63 | if res.status ~= 200 then 64 | ngx.status = res.status 65 | ngx.header.content_type = "application/json; charset=utf-8" 66 | ngx.print(res.body) 67 | ngx.exit(ngx.HTTP_FORBIDDEN) 68 | else 69 | send_token(token) 70 | end 71 | end 72 | 73 | -- Generates a token 74 | function generate_token(client_id) 75 | local token = ts.sha1_digest(tostring(random.bytes(20, true)) .. client_id) 76 | return { ["access_token"] = token, ["token_type"] = "bearer", ["expires_in"] = 604800 } 77 | end 78 | 79 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 80 | function store_token(params, token) 81 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = token.expires_in }) 82 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 83 | stored.body = stored.body or stored.status 84 | return { ["status"] = stored.status , ["body"] = stored.body } 85 | end 86 | 87 | -- Returns the token to the client 88 | function send_token(token) 89 | ngx.header.content_type = "application/json; charset=utf-8" 90 | ngx.say(cjson.encode(token)) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | 94 | local params = extract_params() 95 | 96 | local is_valid = check_credentials(params) 97 | 98 | if is_valid then 99 | get_token(params) 100 | end -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | server { 28 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 29 | lua_code_cache off; 30 | listen 80; 31 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 32 | server_name CHANGE_ME_SERVER_NAME; 33 | underscores_in_headers on; 34 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 35 | set $threescale_backend "https://su1.3scale.net:443"; 36 | 37 | location = /_threescale/check_credentials { 38 | internal; 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 42 | 43 | set $provider_key CHANGE_ME_PROVIDER_KEY; 44 | set $service_id CHANGE_ME_SERVICE_ID; 45 | 46 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 47 | } 48 | 49 | location = /oauth/token { 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | proxy_set_header Host $http_host; 53 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 54 | 55 | set $provider_key CHANGE_ME_PROVIDER_KEY; 56 | 57 | content_by_lua_file get_token.lua; 58 | } 59 | 60 | location = /_threescale/oauth_store_token { 61 | internal; 62 | proxy_set_header X-Real-IP $remote_addr; 63 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 64 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 65 | 66 | set $provider_key CHANGE_ME_PROVIDER_KEY; 67 | set $service_id CHANGE_ME_SERVICE_ID; 68 | 69 | proxy_method POST; 70 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 71 | } 72 | 73 | location = /threescale_oauth_authrep { 74 | internal; 75 | proxy_set_header Host "su1.3scale.net"; 76 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 77 | proxy_set_header X-3scale-OAuth2-Grant-Type "client_credentials"; 78 | 79 | set $provider_key CHANGE_ME_PROVIDER_KEY; 80 | set $service_id CHANGE_ME_SERVICE_ID; 81 | 82 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 83 | } 84 | 85 | location = /threescale_authrep { 86 | internal; 87 | set $provider_key CHANGE_ME_PROVIDER_KEY; 88 | 89 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 90 | proxy_set_header Host "su1.3scale.net"; 91 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 92 | } 93 | 94 | location = /out_of_band_oauth_authrep_action { 95 | internal; 96 | proxy_pass_request_headers off; 97 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 98 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 99 | set $provider_key CHANGE_ME_PROVIDER_KEY; 100 | 101 | 102 | content_by_lua "require('nginx').post_action_content()"; 103 | } 104 | 105 | location / { 106 | set $provider_key null; 107 | set $cached_key null; 108 | set $credentials null; 109 | set $usage null; 110 | set $service_id CHANGE_ME_SERVICE_ID; 111 | set $proxy_pass null; 112 | set $secret_token null; 113 | set $resp_body null; 114 | set $resp_headers null; 115 | set $access_token null; 116 | 117 | proxy_ignore_client_abort on; 118 | 119 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 120 | access_by_lua "require('nginx').access()"; 121 | 122 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 123 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 124 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 125 | 126 | proxy_pass $proxy_pass ; 127 | proxy_set_header X-Real-IP $remote_addr; 128 | proxy_set_header Host $host; 129 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 130 | 131 | post_action /out_of_band_oauth_authrep_action; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | access_tokens:set(ngx.var.cached_key,200) 269 | end 270 | 271 | ngx.var.cached_key = nil 272 | end 273 | end 274 | 275 | function authrep(params, service) 276 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 277 | local api_keys = ngx.shared.api_keys 278 | local is_known = api_keys:get(ngx.var.cached_key) 279 | 280 | if is_known ~= 200 then 281 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 282 | 283 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 284 | if res.status ~= 200 then 285 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 286 | api_keys:delete(ngx.var.cached_key) 287 | ngx.status = res.status 288 | ngx.header.content_type = "application/json" 289 | ngx.var.cached_key = nil 290 | error_authorization_failed(service) 291 | else 292 | api_keys:set(ngx.var.cached_key,200) 293 | end 294 | 295 | ngx.var.cached_key = nil 296 | end 297 | end 298 | 299 | function add_trans(usage) 300 | local us = usage:split("&") 301 | local ret = "" 302 | for i,v in ipairs(us) do 303 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 304 | end 305 | return string.sub(ret, 1, -2) 306 | end 307 | 308 | 309 | function _M.access() 310 | local params = {} 311 | local host = ngx.req.get_headers()["Host"] 312 | local auth_strat = "" 313 | local service = {} 314 | 315 | if ngx.status == 403 then 316 | ngx.say("Throttling due to too many requests") 317 | ngx.exit(403) 318 | end 319 | 320 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 321 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 322 | service = service_CHANGE_ME_SERVICE_ID -- 323 | ngx.var.secret_token = service.secret_token 324 | 325 | -- Do this to remove token type, e.g Bearer from token 326 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 327 | -- ngx.var.access_token = params.access_token 328 | 329 | ngx.var.access_token = parameters.access_token 330 | params.access_token = parameters.access_token 331 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 332 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token 333 | auth_strat = "oauth" 334 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 335 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 336 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 337 | end 338 | 339 | ngx.var.credentials = build_query(params) 340 | 341 | -- if true then 342 | -- log(ngx.var.app_id) 343 | -- log(ngx.var.app_key) 344 | -- log(ngx.var.usage) 345 | -- end 346 | 347 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 348 | if ngx.var.usage == nil then 349 | ngx.header["X-3scale-matched-rules"] = '' 350 | error_no_match(service) 351 | end 352 | 353 | if get_debug_value() then 354 | ngx.header["X-3scale-matched-rules"] = matched_rules2 355 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 356 | ngx.header["X-3scale-usage"] = ngx.var.usage 357 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 358 | end 359 | 360 | -- this would be better with the whole authrep call, with user_id, and everything so that 361 | -- it can be replayed if it's a cached response 362 | 363 | authorize(auth_strat, params, service) 364 | 365 | end 366 | 367 | 368 | function _M.post_action_content() 369 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 370 | 371 | local req = cjson.encode{method=method, path=path, headers=headers} 372 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 373 | 374 | local cached_key = ngx.var.cached_key 375 | if cached_key ~= nil and cached_key ~= "null" then 376 | local status_code = ngx.var.status 377 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 378 | if res1.status ~= 200 then 379 | local access_tokens = ngx.shared.api_keys 380 | access_tokens:delete(cached_key) 381 | end 382 | end 383 | 384 | ngx.exit(ngx.HTTP_OK) 385 | end 386 | 387 | 388 | return _M 389 | 390 | -- END OF SCRIPT 391 | -------------------------------------------------------------------------------- /oauth2/client-credentials-flow/token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. ngx.escape_uri(i) .. '=' .. ngx.escape_uri(v) .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/README.md: -------------------------------------------------------------------------------- 1 | # Client-Side Web Application / Implicit Grant Flow 2 | 3 | ##Requirements 4 | 5 | You will need to: 6 | 7 | * Find all instances of CHANGE_ME in the config files and replace them with the correct values for your API 8 | * Place threescale_utils.lua in /opt/openresty/lualib/threescale_utils.lua 9 | 10 | You will need to install Redis on your Nginx server in order for this to work: 11 | 12 | ### Redis 13 | 14 | Download and install redis on Nginx server (we use version 2.6.16 which is the currently stable version at the time of writing this) 15 | 16 | ``` 17 | tar zxvf redis-VERSION.tar.gz 18 | cd redis-VERSION 19 | make 20 | sudo make install 21 | ``` 22 | 23 | In order to to install and run redis server you will need to run the following, accepting all the default values: 24 | 25 | ``` 26 | sudo ./utils/install_server.sh 27 | ``` 28 | 29 | ## Token Generation 30 | 31 | Nginx acts as the OAuth provider. 32 | 33 | ### Files 34 | 35 | - `authorize.lua` - This file contains the logic for authorizing the client and redirecting the end user to the oAuth login page as well as checking that the return url matches the one specified by the API buyer. It gets executed when the /authorize endpoint is hit. 36 | - `authorized_callback.lua` - This file contains the logic for generating the access_token and redirecting back to the application’s redirect url with the access_token. As an API provider, you will need to call this endpoint once your user successfully logs in and authorizes the API buyer’s requested access. This file gets executed when the /callback endpoint is called by your application. 37 | - `nginx.conf` - This is a typical Nginx config file. Feel free to edit it or to copy paste it to your existing .conf if you are already running Nginx. 38 | - `nginx.lua` - This file contains the logic that you defined on the web interface to track usage for various metrics and methods as well as checking for authorization to access the various endpoints. 39 | 40 | ### Usage 41 | 42 | 1. Client calls /authorize endpoint to redirect user to login page: 43 | 44 | `curl -v -X GET "https://nginx-server/authorize?scope=PLAN_ID&redirect_uri=REDIRECT_URI&response_type=token&client_id=CLIENT_ID` 45 | 46 | 2. If user grants access, API Auth Server calls /callback endpoint with state and shared_secret 47 | 3. If all is well, Nginx sends access_token to redirect_url 48 | 49 | 50 | #### Notes 51 | 52 | 53 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/no-token-generation/authorized_callback.lua: -------------------------------------------------------------------------------- 1 | -- authorized_callback.lua 2 | 3 | -- Once the client has been authorized by the API provider in their 4 | -- login, the provider sends the token to the Gateway for storage. 5 | -- The Gateway will then send the token on to the client 6 | local ts = require 'threescale_utils' 7 | local redis = require 'resty.redis' 8 | local red = redis:new() 9 | 10 | -- The authorization server should send some data in the callback response to let the 11 | -- API Gateway know which user to associate with the token. 12 | -- We assume that this data will be sent as uri params. 13 | -- Additionally, the shared secret between 3scale and the Authorization code should be sent 14 | -- to authenticate the API against the Gateway. 15 | -- This function should be over-ridden depending on authorization server implementation. 16 | function extract_params() 17 | local params = {} 18 | local uri_params = ngx.req.get_uri_args() 19 | 20 | params.user_id = uri_params.user_id or uri_params.username 21 | params.state = uri_params.state 22 | -- In case state is no longer valid, authorization server might send this so we know where to redirect with error 23 | params.redirect_uri = uri_params.redirect_uri or uri_params.redirect_url 24 | params.access_token = uri_params.access_token 25 | params.token_type = uri_params.token_type or "bearer" 26 | params.expires_in = uri_params.expires_in or "604800" 27 | 28 | local header_params = ngx.req.get_headers() 29 | params.secret_token = header_params.X_3scale_proxy_secret_token 30 | 31 | return params 32 | end 33 | 34 | function check_secret(params) 35 | return params.secret_token == ngx.var.secret_token 36 | end 37 | 38 | -- Get the token from params 39 | function get_token(params) 40 | local res = {} 41 | local token = {} 42 | 43 | token = extract_token(params) 44 | res = store_token(params, token) 45 | 46 | if res.status ~= 200 then 47 | local error_code = res.body:match('') 48 | ngx.header.content_type = "application/x-www-form-urlencoded" 49 | return ngx.redirect(token.redirect_uri .. "#error=server_error&error_description="..error_code or res.body) 50 | else 51 | send_token(token) 52 | end 53 | end 54 | 55 | -- Retrieve token from params 56 | function extract_token(params) 57 | local token = {} 58 | 59 | token.access_token = params.access_token 60 | token.token_type = params.token_type 61 | token.expires_in = params.expires_in 62 | token.state = params.state 63 | 64 | return token 65 | end 66 | 67 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 68 | function store_token(params, token) 69 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = token.expires_in }) 70 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", 71 | { method = ngx.HTTP_POST, body = body } ) 72 | return { ["status"] = stored.status , ["body"] = stored.body } 73 | end 74 | 75 | -- Returns the token to the client 76 | function send_token(token) 77 | ngx.header.content_type = "application/x-www-form-urlencoded" 78 | return ngx.redirect( token.redirect_uri .. "#access_token="..token.access_token.."&state="..token.state.."&token_type="..token.token_type.."&expires_in="..token.expires_in ) 79 | end 80 | 81 | local params = extract_params() 82 | 83 | local is_valid = check_secret(params) 84 | 85 | if is_valid then 86 | get_token(params) 87 | end -------------------------------------------------------------------------------- /oauth2/implicit-flow/no-token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | upstream backend_CHANGE_ME_IDP_BACKEND { 28 | server CHANGE_ME_IDP_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 29 | } 30 | 31 | server { 32 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 33 | lua_code_cache off; 34 | listen 80; 35 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 36 | server_name CHANGE_ME_SERVER_NAME; 37 | underscores_in_headers on; 38 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 39 | set $threescale_backend "https://su1.3scale.net:443"; 40 | 41 | location = /authorize { 42 | set $secret_token CHANGE_ME_SECRET_TOKEN; 43 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 44 | proxy_set_header X-Real-IP $remote_addr; 45 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 46 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 47 | 48 | ## CHANGE TO THE authorize path for your chosen Identity Provider 49 | proxy_pass https://backend_CHANGE_ME_IDP_BACKEND/oauth2/authorize; 50 | } 51 | 52 | location = /_threescale/check_credentials { 53 | internal; 54 | proxy_set_header X-Real-IP $remote_addr; 55 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 56 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 57 | 58 | set $provider_key CHANGE_ME_PROVIDER_KEY; 59 | set $service_id CHANGE_ME_SERVICE_ID; 60 | 61 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 62 | } 63 | 64 | location = /callback { 65 | set $secret_token CHANGE_ME_SECRET_TOKEN; 66 | 67 | content_by_lua_file authorized_callback.lua; 68 | } 69 | 70 | location = /_threescale/oauth_store_token { 71 | internal; 72 | proxy_set_header X-Real-IP $remote_addr; 73 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 74 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 75 | 76 | set $provider_key CHANGE_ME_PROVIDER_KEY; 77 | set $service_id CHANGE_ME_SERVICE_ID; 78 | 79 | proxy_method POST; 80 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 81 | } 82 | 83 | location = /threescale_oauth_authrep { 84 | internal; 85 | proxy_set_header Host "su1.3scale.net"; 86 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 87 | proxy_set_header X-3scale-OAuth2-Grant-Type "implicit"; 88 | 89 | set $provider_key CHANGE_ME_PROVIDER_KEY; 90 | set $service_id CHANGE_ME_SERVICE_ID; 91 | 92 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 93 | } 94 | 95 | location = /threescale_authrep { 96 | internal; 97 | set $provider_key CHANGE_ME_PROVIDER_KEY; 98 | 99 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 100 | proxy_set_header Host "su1.3scale.net"; 101 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 102 | } 103 | 104 | location = /out_of_band_oauth_authrep_action { 105 | internal; 106 | proxy_pass_request_headers off; 107 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 108 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 109 | set $provider_key CHANGE_ME_PROVIDER_KEY; 110 | 111 | 112 | content_by_lua "require('nginx').post_action_content()"; 113 | } 114 | 115 | location / { 116 | set $provider_key null; 117 | set $cached_key null; 118 | set $credentials null; 119 | set $usage null; 120 | set $service_id CHANGE_ME_SERVICE_ID; 121 | set $proxy_pass null; 122 | set $secret_token null; 123 | set $resp_body null; 124 | set $resp_headers null; 125 | set $access_token null; 126 | ## Add user_id value if required 127 | # e.g set $user_id null; 128 | 129 | proxy_ignore_client_abort on; 130 | 131 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 132 | access_by_lua "require('nginx').access()"; 133 | 134 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 135 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 136 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 137 | 138 | proxy_pass $proxy_pass; 139 | proxy_set_header X-Real-IP $remote_addr; 140 | proxy_set_header Host $host; 141 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 142 | ## Send user_id to API if required 143 | # e.g proxy_set_header user_id $user_id; 144 | 145 | post_action /out_of_band_oauth_authrep_action; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/no-token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | -- If required: extract user_id token belongs to and compare with value from auth response 269 | -- local user_id = res.body:match('user_id="(%S-)">'..access_token) 270 | -- if user_id == params.user_id then 271 | -- Set this value if you need to send user_id back to your API 272 | -- ngx.var.user_id = user_id 273 | access_tokens:set(ngx.var.cached_key,200) 274 | -- else 275 | -- access_tokens:delete(ngx.var.cached_key) 276 | -- ngx.status = res.status 277 | -- ngx.header.content_type = "application/json" 278 | -- ngx.var.cached_key = nil 279 | -- error_authorization_failed(service) 280 | -- end 281 | end 282 | 283 | ngx.var.cached_key = nil 284 | end 285 | end 286 | 287 | function authrep(params, service) 288 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 289 | local api_keys = ngx.shared.api_keys 290 | local is_known = api_keys:get(ngx.var.cached_key) 291 | 292 | if is_known ~= 200 then 293 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 294 | 295 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 296 | if res.status ~= 200 then 297 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 298 | api_keys:delete(ngx.var.cached_key) 299 | ngx.status = res.status 300 | ngx.header.content_type = "application/json" 301 | ngx.var.cached_key = nil 302 | error_authorization_failed(service) 303 | else 304 | api_keys:set(ngx.var.cached_key,200) 305 | end 306 | 307 | ngx.var.cached_key = nil 308 | end 309 | end 310 | 311 | function add_trans(usage) 312 | local us = usage:split("&") 313 | local ret = "" 314 | for i,v in ipairs(us) do 315 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 316 | end 317 | return string.sub(ret, 1, -2) 318 | end 319 | 320 | 321 | function _M.access() 322 | local params = {} 323 | local host = ngx.req.get_headers()["Host"] 324 | local auth_strat = "" 325 | local service = {} 326 | 327 | if ngx.status == 403 then 328 | ngx.say("Throttling due to too many requests") 329 | ngx.exit(403) 330 | end 331 | 332 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 333 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 334 | service = service_CHANGE_ME_SERVICE_ID -- 335 | ngx.var.secret_token = service.secret_token 336 | 337 | -- If relevant, extract user_id from request 338 | -- e.g local user_id = ngx.re.match(ngx.var.uri,[=[^/api/user/([\w_\.-]+)\.json]=]) 339 | -- params.user_id = user_id 340 | 341 | -- Do this to extract token from Authorization: Bearer header 342 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 343 | -- ngx.var.access_token = params.access_token 344 | 345 | ngx.var.access_token = parameters.access_token 346 | params.access_token = parameters.access_token 347 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 348 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token .. ( params.user_id and ":" .. params.user_id or "" ) 349 | auth_strat = "oauth" 350 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 351 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 352 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 353 | end 354 | 355 | ngx.var.credentials = build_query(params) 356 | 357 | -- if true then 358 | -- log(ngx.var.app_id) 359 | -- log(ngx.var.app_key) 360 | -- log(ngx.var.usage) 361 | -- end 362 | 363 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 364 | if ngx.var.usage == nil then 365 | ngx.header["X-3scale-matched-rules"] = '' 366 | error_no_match(service) 367 | end 368 | 369 | if get_debug_value() then 370 | ngx.header["X-3scale-matched-rules"] = matched_rules2 371 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 372 | ngx.header["X-3scale-usage"] = ngx.var.usage 373 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 374 | end 375 | 376 | authorize(auth_strat, params, service) 377 | 378 | end 379 | 380 | 381 | function _M.post_action_content() 382 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 383 | 384 | local req = cjson.encode{method=method, path=path, headers=headers} 385 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 386 | 387 | local cached_key = ngx.var.cached_key 388 | if cached_key ~= nil and cached_key ~= "null" then 389 | local status_code = ngx.var.status 390 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 391 | if res1.status ~= 200 then 392 | local access_tokens = ngx.shared.api_keys 393 | access_tokens:delete(cached_key) 394 | end 395 | end 396 | 397 | ngx.exit(ngx.HTTP_OK) 398 | end 399 | 400 | 401 | return _M 402 | 403 | -- END OF SCRIPT 404 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/no-token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/token-generation/authorize.lua: -------------------------------------------------------------------------------- 1 | local random = require 'resty.random' 2 | local ts = require 'threescale_utils' 3 | local redis = require 'resty.redis' 4 | local red = redis:new() 5 | 6 | -- As per RFC for Implicit flow: extract params from request uri 7 | -- If implementation deviates from RFC, this function should be over-ridden 8 | function extract_params() 9 | local params = {} 10 | local uri_params = ngx.req.get_uri_args() 11 | 12 | params.response_type = uri_params.response_type 13 | params.client_id = uri_params.client_id 14 | params.redirect_uri = uri_params.redirect_uri or uri_params.redirect_url 15 | params.scope = uri_params.scope 16 | params.state = uri_params.state 17 | 18 | return params 19 | end 20 | 21 | -- Check valid credentials 22 | function check_credentials(params) 23 | local res = check_client_credentials(params) 24 | return res.status == 200 25 | end 26 | 27 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 28 | function check_client_credentials(params) 29 | local res = ngx.location.capture("/_threescale/check_credentials", 30 | { args = { app_id = params.client_id , app_key = params.client_secret , redirect_uri = params.redirect_uri }}) 31 | 32 | if res.status ~= 200 then 33 | params.error = "invalid_client" 34 | redirect_to_auth(params) 35 | end 36 | 37 | return { ["status"] = res.status, ["body"] = res.body } 38 | end 39 | 40 | -- Authorizes the client for the given scope 41 | function authorize(params) 42 | local required_params = {'client_id', 'redirect_uri', 'response_type', 'scope'} 43 | 44 | if params["response_type"] ~= 'token' then 45 | params.error = "unsupported_response_type" 46 | elseif not ts.required_params_present(required_params, params) then 47 | params.error = "invalid_request" 48 | end 49 | 50 | redirect_to_auth(params) 51 | end 52 | 53 | -- redirects_to the authorization url of the API provider with a secret 54 | -- 'state' which will be used when the form redirects the user back to 55 | -- this server. 56 | function redirect_to_auth(params) 57 | 58 | if not params.error then 59 | local n = nonce(params.client_id) 60 | params.state = n 61 | 62 | ts.connect_redis(red) 63 | local pre_token = generate_access_token(params.client_id) 64 | params.tok = pre_token 65 | 66 | local ok, err = red:hmset(ngx.var.service_id .. "#tmp_data:".. n, 67 | {client_id = params.client_id, 68 | redirect_uri = params.redirect_uri, 69 | plan_id = params.scope, 70 | access_token = pre_token}) 71 | 72 | if not ok then 73 | ts.error(ts.dump(err)) 74 | end 75 | end 76 | 77 | local args = ts.build_query(params) 78 | 79 | ngx.header.content_type = "application/x-www-form-urlencoded" 80 | return ngx.redirect( ngx.var.auth_url .. "?" .. args ) 81 | end 82 | 83 | -- returns a unique string for the client_id. it will be short lived 84 | function nonce(client_id) 85 | return ts.sha1_digest(tostring(random.bytes(20, true)) .. "#login:" .. client_id) 86 | end 87 | 88 | function generate_access_token(client_id) 89 | return ts.sha1_digest(tostring(random.bytes(20, true)) .. client_id) 90 | end 91 | 92 | local params = extract_params() 93 | 94 | local is_valid = check_credentials(params) 95 | 96 | if is_valid then 97 | authorize(params) 98 | else 99 | params.error = "invalid_client" 100 | redirect_to_auth(params) 101 | end -------------------------------------------------------------------------------- /oauth2/implicit-flow/token-generation/authorized_callback.lua: -------------------------------------------------------------------------------- 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 | local ts = require 'threescale_utils' 8 | local redis = require 'resty.redis' 9 | local red = redis:new() 10 | 11 | -- The authorization server should send some data in the callback response to let the 12 | -- API Gateway know which user to associate with the token. 13 | -- We assume that this data will be sent as uri params. 14 | -- This function should be over-ridden depending on authorization server implementation. 15 | function extract_params() 16 | local params = {} 17 | local uri_params = ngx.req.get_uri_args() 18 | 19 | params.user_id = uri_params.user_id or uri_params.username 20 | params.state = uri_params.state 21 | -- In case state is no longer valid, authorization server might send this so we know where to redirect with error 22 | params.redirect_uri = uri_params.redirect_uri or uri_params.redirect_url 23 | 24 | return params 25 | end 26 | 27 | -- Check valid state parameter sent 28 | function check_state(params) 29 | local required_params = {'state'} 30 | 31 | local valid_state = false 32 | if ts.required_params_present(required_params, params) then 33 | local tmp_data = ngx.var.service_id .. "#tmp_data:".. params.state 34 | local ok = red:exists(tmp_data) 35 | 36 | if ok == 0 then 37 | ngx.header.content_type = "application/x-www-form-urlencoded" 38 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=invalid_or_expired_state&state="..params.state) 39 | end 40 | 41 | valid_state = true 42 | else 43 | ngx.header.content_type = "application/x-www-form-urlencoded" 44 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=missing_state") 45 | end 46 | 47 | return valid_state 48 | end 49 | 50 | -- Get the token from Redis 51 | function get_token(params) 52 | local res = {} 53 | local token = {} 54 | 55 | token = request_token(params) 56 | res = store_token(params, token) 57 | 58 | if res.status ~= 200 then 59 | local error_code = res.body:match('') 60 | ngx.header.content_type = "application/x-www-form-urlencoded" 61 | return ngx.redirect(token.redirect_uri .. "#error=server_error&error_description="..error_code or res.body) 62 | else 63 | send_token(token) 64 | end 65 | end 66 | 67 | -- Retrieve client data from Redis 68 | function request_token(params) 69 | local tmp_data = ngx.var.service_id .. "#tmp_data:".. params.state 70 | 71 | ts.connect_redis(red) 72 | local ok, err = red:hgetall(tmp_data) 73 | 74 | if not ok then 75 | ngx.log(0, "no values for tmp_data hash: ".. ts.dump(err)) 76 | ngx.header.content_type = "application/x-www-form-urlencoded" 77 | return ngx.redirect(params.redirect_uri .. "#error=invalid_request&error_description=invalid_or_expired_state") 78 | end 79 | 80 | -- Restore client data into token hash 81 | local token = red:array_to_hash(ok) 82 | -- Delete tmp_data: 83 | red:del(tmp_data) 84 | 85 | token.expires_in = 604800 86 | token.state = params.state 87 | 88 | return token 89 | end 90 | 91 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 92 | function store_token(params, token) 93 | local body = ts.build_query({ app_id = token.client_id, token = token.access_token, user_id = params.user_id , ttl = token.expires_in }) 94 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", 95 | { method = ngx.HTTP_POST, body = body } ) 96 | return { ["status"] = stored.status , ["body"] = stored.body } 97 | end 98 | 99 | -- Returns the token to the client 100 | function send_token(token) 101 | ngx.header.content_type = "application/x-www-form-urlencoded" 102 | return ngx.redirect( token.redirect_uri .. "#access_token="..token.access_token.."&state="..token.state.."&token_type=bearer&expires_in="..token.expires_in ) 103 | end 104 | 105 | local params = extract_params() 106 | 107 | local is_valid = check_state(params) 108 | 109 | if is_valid then 110 | get_token(params) 111 | end -------------------------------------------------------------------------------- /oauth2/implicit-flow/token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | server { 28 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 29 | lua_code_cache off; 30 | listen 80; 31 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 32 | server_name CHANGE_ME_SERVER_NAME; 33 | underscores_in_headers on; 34 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 35 | set $threescale_backend "https://su1.3scale.net:443"; 36 | 37 | location = /authorize { 38 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 39 | 40 | set $red_url ""; 41 | set $client_id ""; 42 | set $auth_url "CHANGE_ME_AUTH_URL"; 43 | set $service_id CHANGE_ME_SERVICE_ID; 44 | 45 | content_by_lua_file authorize.lua; 46 | } 47 | 48 | location = /_threescale/check_credentials { 49 | internal; 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 53 | 54 | set $provider_key CHANGE_ME_PROVIDER_KEY; 55 | set $service_id CHANGE_ME_SERVICE_ID; 56 | 57 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 58 | } 59 | 60 | location = /callback { 61 | set $service_id CHANGE_ME_SERVICE_ID; 62 | 63 | content_by_lua_file authorized_callback.lua; 64 | } 65 | 66 | location = /_threescale/oauth_store_token { 67 | internal; 68 | proxy_set_header X-Real-IP $remote_addr; 69 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 70 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 71 | 72 | set $provider_key CHANGE_ME_PROVIDER_KEY; 73 | set $service_id CHANGE_ME_SERVICE_ID; 74 | 75 | proxy_method POST; 76 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 77 | } 78 | 79 | location = /threescale_oauth_authrep { 80 | internal; 81 | proxy_set_header Host "su1.3scale.net"; 82 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 83 | proxy_set_header X-3scale-OAuth2-Grant-Type "implicit"; 84 | 85 | set $provider_key CHANGE_ME_PROVIDER_KEY; 86 | set $service_id CHANGE_ME_SERVICE_ID; 87 | 88 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 89 | } 90 | 91 | location = /threescale_authrep { 92 | internal; 93 | set $provider_key CHANGE_ME_PROVIDER_KEY; 94 | 95 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 96 | proxy_set_header Host "su1.3scale.net"; 97 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 98 | } 99 | 100 | location = /out_of_band_oauth_authrep_action { 101 | internal; 102 | proxy_pass_request_headers off; 103 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 104 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 105 | set $provider_key CHANGE_ME_PROVIDER_KEY; 106 | 107 | 108 | content_by_lua "require('nginx').post_action_content()"; 109 | } 110 | 111 | location / { 112 | set $provider_key null; 113 | set $cached_key null; 114 | set $credentials null; 115 | set $usage null; 116 | set $service_id CHANGE_ME_SERVICE_ID; 117 | set $proxy_pass null; 118 | set $secret_token null; 119 | set $resp_body null; 120 | set $resp_headers null; 121 | set $access_token null; 122 | ## Add user_id value if required 123 | # e.g set $user_id null; 124 | 125 | proxy_ignore_client_abort on; 126 | 127 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 128 | access_by_lua "require('nginx').access()"; 129 | 130 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 131 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 132 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 133 | 134 | proxy_pass $proxy_pass; 135 | proxy_set_header X-Real-IP $remote_addr; 136 | proxy_set_header Host $host; 137 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 138 | ## Send user_id to API if required 139 | # e.g proxy_set_header user_id $user_id; 140 | 141 | post_action /out_of_band_oauth_authrep_action; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | -- If required: extract user_id token belongs to and compare with value from auth response 269 | -- local user_id = res.body:match('user_id="(%S-)">'..access_token) 270 | -- if user_id == params.user_id then 271 | -- Set this value if you need to send user_id back to your API 272 | -- ngx.var.user_id = user_id 273 | access_tokens:set(ngx.var.cached_key,200) 274 | -- else 275 | -- access_tokens:delete(ngx.var.cached_key) 276 | -- ngx.status = res.status 277 | -- ngx.header.content_type = "application/json" 278 | -- ngx.var.cached_key = nil 279 | -- error_authorization_failed(service) 280 | -- end 281 | end 282 | 283 | ngx.var.cached_key = nil 284 | end 285 | end 286 | 287 | function authrep(params, service) 288 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 289 | local api_keys = ngx.shared.api_keys 290 | local is_known = api_keys:get(ngx.var.cached_key) 291 | 292 | if is_known ~= 200 then 293 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 294 | 295 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 296 | if res.status ~= 200 then 297 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 298 | api_keys:delete(ngx.var.cached_key) 299 | ngx.status = res.status 300 | ngx.header.content_type = "application/json" 301 | ngx.var.cached_key = nil 302 | error_authorization_failed(service) 303 | else 304 | api_keys:set(ngx.var.cached_key,200) 305 | end 306 | 307 | ngx.var.cached_key = nil 308 | end 309 | end 310 | 311 | function add_trans(usage) 312 | local us = usage:split("&") 313 | local ret = "" 314 | for i,v in ipairs(us) do 315 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 316 | end 317 | return string.sub(ret, 1, -2) 318 | end 319 | 320 | 321 | function _M.access() 322 | local params = {} 323 | local host = ngx.req.get_headers()["Host"] 324 | local auth_strat = "" 325 | local service = {} 326 | 327 | if ngx.status == 403 then 328 | ngx.say("Throttling due to too many requests") 329 | ngx.exit(403) 330 | end 331 | 332 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 333 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 334 | service = service_CHANGE_ME_SERVICE_ID -- 335 | ngx.var.secret_token = service.secret_token 336 | 337 | -- If relevant, extract user_id from request 338 | -- e.g local user_id = ngx.re.match(ngx.var.uri,[=[^/api/user/([\w_\.-]+)\.json]=]) 339 | -- params.user_id = user_id 340 | 341 | -- Do this to extract token from Authorization: Bearer header 342 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 343 | -- ngx.var.access_token = params.access_token 344 | 345 | ngx.var.access_token = parameters.access_token 346 | params.access_token = parameters.access_token 347 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 348 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token .. ( params.user_id and ":" .. params.user_id or "" ) 349 | auth_strat = "oauth" 350 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 351 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 352 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 353 | end 354 | 355 | ngx.var.credentials = build_query(params) 356 | 357 | -- if true then 358 | -- log(ngx.var.app_id) 359 | -- log(ngx.var.app_key) 360 | -- log(ngx.var.usage) 361 | -- end 362 | 363 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 364 | if ngx.var.usage == nil then 365 | ngx.header["X-3scale-matched-rules"] = '' 366 | error_no_match(service) 367 | end 368 | 369 | if get_debug_value() then 370 | ngx.header["X-3scale-matched-rules"] = matched_rules2 371 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 372 | ngx.header["X-3scale-usage"] = ngx.var.usage 373 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 374 | end 375 | 376 | authorize(auth_strat, params, service) 377 | 378 | end 379 | 380 | 381 | function _M.post_action_content() 382 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 383 | 384 | local req = cjson.encode{method=method, path=path, headers=headers} 385 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 386 | 387 | local cached_key = ngx.var.cached_key 388 | if cached_key ~= nil and cached_key ~= "null" then 389 | local status_code = ngx.var.status 390 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 391 | if res1.status ~= 200 then 392 | local access_tokens = ngx.shared.api_keys 393 | access_tokens:delete(cached_key) 394 | end 395 | end 396 | 397 | ngx.exit(ngx.HTTP_OK) 398 | end 399 | 400 | 401 | return _M 402 | 403 | -- END OF SCRIPT 404 | -------------------------------------------------------------------------------- /oauth2/implicit-flow/token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/README.md: -------------------------------------------------------------------------------- 1 | # Resource Owner Password Flow 2 | 3 | Nginx is **NOT** the OAuth provider in this flow. It could potentially be the OAuth provider, but since user details are held by the Provider it makes sense for the API Authorization server to authenticate user and issue the access token. 4 | 5 | ## Requirements 6 | 7 | You will need to: 8 | 9 | * Find all instances of CHANGE_ME in the config files and replace them with the correct values for your API 10 | * Place threescale_utils.lua in /opt/openresty/lualib/threescale_utils.lua 11 | 12 | ## Files 13 | 14 | - `get_token.lua` - This file contains the logic to return the access token for the client identified by a client_id. It gets executed when the /oauth/token endpoint is called. 15 | - `nginx.conf` - This is a typical Nginx config file. Feel free to edit it or to copy paste it to your existing .conf if you are already running Nginx. 16 | - `nginx.lua` - This file contains the logic that you defined on the web interface to track usage for various metrics and methods as well as checking for authorization to access the various endpoints. 17 | 18 | ## Usage 19 | 20 | - Authorize: 21 | 22 | `curl -X GET "https://localhost/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=password&username=USERNAME&password=PASSWORD"` 23 | 24 | Returns Access Token from API Auth Server. 25 | 26 | - You can then call API using the access_token: 27 | 28 | `curl -v -X GET "https://localhost/API_ENDPOINT?access_token=ACCESS_TOKEN"` 29 | 30 | ### Notes 31 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/no-token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | 4 | -- As per RFC for Resource Owner Password flow: extract params from Authorization header and body 5 | -- If implementation deviates from RFC, this function should be over-ridden 6 | function extract_params() 7 | local params = {} 8 | local header_params = ngx.req.get_headers() 9 | 10 | params.authorization = {} 11 | 12 | if header_params['Authorization'] then 13 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 14 | end 15 | 16 | ngx.req.read_body() 17 | local body_params = ngx.req.get_post_args() 18 | 19 | params.client_id = params.authorization[1] or body_params.client_id 20 | params.client_secret = params.authorization[2] or body_params.client_secret 21 | 22 | params.grant_type = body_params.grant_type 23 | params.user_id = body_params.user_id or body_params.username 24 | params.password = body_params.password 25 | 26 | if params.grant_type == "refresh_token" then 27 | params.refresh_token = body_params.refresh_token 28 | end 29 | 30 | return params 31 | end 32 | 33 | -- Check valid credentials 34 | function check_credentials(params) 35 | local res = check_client_credentials(params) 36 | return res.status == 200 37 | end 38 | 39 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 40 | function check_client_credentials(params) 41 | local res = ngx.location.capture("/_threescale/check_credentials", 42 | { args = { app_id = params.client_id , app_key = params.client_secret , redirect_uri = params.redirect_uri } }) 43 | 44 | if res.status ~= 200 then 45 | ngx.status = 401 46 | ngx.header.content_type = "application/json; charset=utf-8" 47 | ngx.print('{"error":"invalid_client"}') 48 | ngx.exit(ngx.HTTP_OK) 49 | end 50 | 51 | return { ["status"] = res.status, ["body"] = res.body } 52 | end 53 | 54 | -- Get the token from the OAuth Server 55 | function get_token(params) 56 | local access_token_required_params = {'user_id', 'password', 'grant_type'} 57 | local refresh_token_required_params = {'client_id', 'client_secret', 'grant_type', 'refresh_token'} 58 | 59 | local res = {} 60 | 61 | if (ts.required_params_present(access_token_required_params, params) and params['grant_type'] == 'password') or 62 | (ts.required_params_present(refresh_token_required_params, params) and params['grant_type'] == 'refresh_token') then 63 | res = request_token(params) 64 | else 65 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 66 | end 67 | 68 | if res.status ~= 200 then 69 | ngx.status = res.status 70 | ngx.header.content_type = "application/json; charset=utf-8" 71 | ngx.print(res.body) 72 | ngx.exit(ngx.HTTP_FORBIDDEN) 73 | else 74 | local token = parse_token(res.body) 75 | local stored = store_token(params, token) 76 | 77 | if stored.status ~= 200 then 78 | ngx.say(stored.body) 79 | ngx.status = stored.status 80 | ngx.exit(ngx.HTTP_OK) 81 | else 82 | send_token(token) 83 | end 84 | end 85 | end 86 | 87 | -- Calls the token endpoint to request a token 88 | function request_token() 89 | local res = ngx.location.capture("/_oauth/token", { method = ngx.HTTP_POST, copy_all_vars = true }) 90 | return { ["status"] = res.status, ["body"] = res.body } 91 | end 92 | 93 | -- Parses the token - in this case we assume a json encoded token. This function may be overwritten to parse different token formats. 94 | function parse_token(body) 95 | local token = cjson.decode(body) 96 | return token 97 | end 98 | 99 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 100 | function store_token(params, token) 101 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id, ttl = token.expires_in or "604800" }) 102 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 103 | stored.body = stored.body or stored.status 104 | return { ["status"] = stored.status , ["body"] = stored.body } 105 | end 106 | 107 | -- Returns the token to the client 108 | function send_token(token) 109 | ngx.header.content_type = "application/json; charset=utf-8" 110 | ngx.say(cjson.encode(token)) 111 | ngx.exit(ngx.HTTP_OK) 112 | end 113 | 114 | local params = extract_params() 115 | 116 | local is_valid = check_credentials(params) 117 | 118 | if is_valid then 119 | get_token(params) 120 | end -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/no-token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | upstream backend_CHANGE_ME_IDP_BACKEND { 28 | server CHANGE_ME_IDP_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 29 | } 30 | 31 | server { 32 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 33 | lua_code_cache off; 34 | listen 80; 35 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 36 | server_name CHANGE_ME_SERVER_NAME; 37 | underscores_in_headers on; 38 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 39 | set $threescale_backend "https://su1.3scale.net:443"; 40 | 41 | location = /_threescale/check_credentials { 42 | internal; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 46 | 47 | set $provider_key CHANGE_ME_PROVIDER_KEY; 48 | set $service_id CHANGE_ME_SERVICE_ID; 49 | 50 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 51 | } 52 | 53 | location = /oauth/token { 54 | proxy_set_header X-Real-IP $remote_addr; 55 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 56 | proxy_set_header Host $http_host; 57 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 58 | 59 | set $provider_key CHANGE_ME_PROVIDER_KEY; 60 | 61 | content_by_lua_file get_token.lua; 62 | } 63 | 64 | location = /_oauth/token { 65 | internal; 66 | proxy_set_header X-Real-IP $remote_addr; 67 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 68 | proxy_set_header Host $http_host; 69 | more_clear_input_headers Accept-Encoding; 70 | 71 | proxy_redirect off; 72 | proxy_max_temp_file_size 0; 73 | 74 | proxy_method POST; 75 | proxy_pass https://CHANGE_ME_IDP_BACKEND/CHANGE_ME_TOKEN_ENDPOINT; 76 | } 77 | 78 | location = /_threescale/oauth_store_token { 79 | internal; 80 | proxy_set_header X-Real-IP $remote_addr; 81 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 82 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 83 | 84 | set $provider_key CHANGE_ME_PROVIDER_KEY; 85 | set $service_id CHANGE_ME_SERVICE_ID; 86 | 87 | proxy_method POST; 88 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 89 | } 90 | 91 | location = /threescale_oauth_authrep { 92 | internal; 93 | proxy_set_header Host "su1.3scale.net"; 94 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 95 | proxy_set_header X-3scale-OAuth2-Grant-Type "password"; 96 | 97 | set $provider_key CHANGE_ME_PROVIDER_KEY; 98 | set $service_id CHANGE_ME_SERVICE_ID; 99 | 100 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 101 | } 102 | 103 | location = /threescale_authrep { 104 | internal; 105 | set $provider_key CHANGE_ME_PROVIDER_KEY; 106 | 107 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 108 | proxy_set_header Host "su1.3scale.net"; 109 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 110 | } 111 | 112 | location = /out_of_band_oauth_authrep_action { 113 | internal; 114 | proxy_pass_request_headers off; 115 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 116 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 117 | set $provider_key CHANGE_ME_PROVIDER_KEY; 118 | 119 | 120 | content_by_lua "require('nginx').post_action_content()"; 121 | } 122 | 123 | location / { 124 | set $provider_key null; 125 | set $cached_key null; 126 | set $credentials null; 127 | set $usage null; 128 | set $service_id CHANGE_ME_SERVICE_ID; 129 | set $proxy_pass null; 130 | set $secret_token null; 131 | set $resp_body null; 132 | set $resp_headers null; 133 | set $access_token null; 134 | 135 | proxy_ignore_client_abort on; 136 | 137 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 138 | access_by_lua "require('nginx').access()"; 139 | 140 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 141 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 142 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 143 | 144 | proxy_pass $proxy_pass; 145 | proxy_set_header X-Real-IP $remote_addr; 146 | proxy_set_header Host $host; 147 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 148 | 149 | post_action /out_of_band_oauth_authrep_action; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/no-token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | access_tokens:set(ngx.var.cached_key,200) 269 | end 270 | 271 | ngx.var.cached_key = nil 272 | end 273 | end 274 | 275 | function authrep(params, service) 276 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 277 | local api_keys = ngx.shared.api_keys 278 | local is_known = api_keys:get(ngx.var.cached_key) 279 | 280 | if is_known ~= 200 then 281 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 282 | 283 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 284 | if res.status ~= 200 then 285 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 286 | api_keys:delete(ngx.var.cached_key) 287 | ngx.status = res.status 288 | ngx.header.content_type = "application/json" 289 | ngx.var.cached_key = nil 290 | error_authorization_failed(service) 291 | else 292 | api_keys:set(ngx.var.cached_key,200) 293 | end 294 | 295 | ngx.var.cached_key = nil 296 | end 297 | end 298 | 299 | function add_trans(usage) 300 | local us = usage:split("&") 301 | local ret = "" 302 | for i,v in ipairs(us) do 303 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 304 | end 305 | return string.sub(ret, 1, -2) 306 | end 307 | 308 | 309 | function _M.access() 310 | local params = {} 311 | local host = ngx.req.get_headers()["Host"] 312 | local auth_strat = "" 313 | local service = {} 314 | 315 | if ngx.status == 403 then 316 | ngx.say("Throttling due to too many requests") 317 | ngx.exit(403) 318 | end 319 | 320 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 321 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 322 | service = service_CHANGE_ME_SERVICE_ID -- 323 | ngx.var.secret_token = service.secret_token 324 | 325 | -- Do this to remove token type, e.g Bearer from token 326 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 327 | -- ngx.var.access_token = params.access_token 328 | 329 | ngx.var.access_token = parameters.access_token 330 | params.access_token = parameters.access_token 331 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 332 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token 333 | auth_strat = "oauth" 334 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 335 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 336 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 337 | end 338 | 339 | ngx.var.credentials = build_query(params) 340 | 341 | -- if true then 342 | -- log(ngx.var.app_id) 343 | -- log(ngx.var.app_key) 344 | -- log(ngx.var.usage) 345 | -- end 346 | 347 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 348 | if ngx.var.usage == nil then 349 | ngx.header["X-3scale-matched-rules"] = '' 350 | error_no_match(service) 351 | end 352 | 353 | if get_debug_value() then 354 | ngx.header["X-3scale-matched-rules"] = matched_rules2 355 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 356 | ngx.header["X-3scale-usage"] = ngx.var.usage 357 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 358 | end 359 | 360 | -- this would be better with the whole authrep call, with user_id, and everything so that 361 | -- it can be replayed if it's a cached response 362 | 363 | authorize(auth_strat, params, service) 364 | 365 | end 366 | 367 | 368 | function _M.post_action_content() 369 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 370 | 371 | local req = cjson.encode{method=method, path=path, headers=headers} 372 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 373 | 374 | local cached_key = ngx.var.cached_key 375 | if cached_key ~= nil and cached_key ~= "null" then 376 | local status_code = ngx.var.status 377 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 378 | if res1.status ~= 200 then 379 | local access_tokens = ngx.shared.api_keys 380 | access_tokens:delete(cached_key) 381 | end 382 | end 383 | 384 | ngx.exit(ngx.HTTP_OK) 385 | end 386 | 387 | 388 | return _M 389 | 390 | -- END OF SCRIPT 391 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/no-token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/token-generation/get_token.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local ts = require 'threescale_utils' 3 | local random = require 'resty.random' 4 | 5 | -- As per RFC for Resource Owner Password flow: extract params from Authorization header and body 6 | -- If implementation deviates from RFC, this function should be over-ridden 7 | function extract_params() 8 | local params = {} 9 | local header_params = ngx.req.get_headers() 10 | 11 | params.authorization = {} 12 | 13 | if header_params['Authorization'] then 14 | params.authorization = ngx.decode_base64(header_params['Authorization']:split(" ")[2]):split(":") 15 | end 16 | 17 | ngx.req.read_body() 18 | local body_params = ngx.req.get_post_args() 19 | 20 | params.client_id = params.authorization[1] or body_params.client_id 21 | params.client_secret = params.authorization[2] or body_params.client_secret 22 | 23 | params.grant_type = body_params.grant_type 24 | params.user_id = body_params.user_id or body_params.username 25 | params.password = body_params.password 26 | 27 | if params.grant_type == "refresh_token" then 28 | params.refresh_token = body_params.refresh_token 29 | end 30 | 31 | return params 32 | end 33 | 34 | -- Check valid client and user credentials 35 | function check_credentials(params) 36 | local res_user = check_user_credentials(params) 37 | local res_client = check_client_credentials(params) 38 | 39 | return res_client.status == 200 and res_user.status == 200 40 | end 41 | 42 | -- Check user credentials against IDP 43 | function check_user_credentials(params) 44 | local body = "CHANGE_ME_REQUEST_PARAMS" 45 | -- e.g local body = '{"type": "basic", "value": "'..ngx.encode_base64(params.user_id..':'..params.password)..'" }' 46 | local res = ngx.location.capture("/_idp/check_credentials", { method = ngx.HTTP_POST, body = body}) 47 | 48 | if res.status ~= 200 then 49 | ngx.status = res.status 50 | ngx.header.content_type = "application/json; charset=utf-8" 51 | ngx.print(res.body) 52 | ngx.exit(ngx.HTTP_FORBIDDEN) 53 | end 54 | 55 | return { ["status"] = res.status, ["body"] = res.body } 56 | end 57 | 58 | -- Check valid params ( client_id / secret / redirect_url, whichever are sent) against 3scale 59 | function check_client_credentials(params) 60 | local res = ngx.location.capture("/_threescale/check_credentials", 61 | { args = { app_id = params.client_id , app_key = params.client_secret , redirect_uri = params.redirect_uri } }) 62 | 63 | if res.status ~= 200 then 64 | ngx.status = 401 65 | ngx.header.content_type = "application/json; charset=utf-8" 66 | ngx.print('{"error":"invalid_client"}') 67 | ngx.exit(ngx.HTTP_OK) 68 | end 69 | 70 | return { ["status"] = res.status, ["body"] = res.body } 71 | end 72 | 73 | -- Get the token from the Gateway 74 | function get_token(params) 75 | local required_params = {'user_id', 'password', 'grant_type'} 76 | 77 | local res = {} 78 | local token = {} 79 | 80 | if ts.required_params_present(required_params, params) and params['grant_type'] == 'password' then 81 | token = generate_token(params.client_id) 82 | res = store_token(params, token) 83 | else 84 | res = { ["status"] = 403, ["body"] = '{"error": "invalid_request"}' } 85 | end 86 | 87 | if res.status ~= 200 then 88 | ngx.status = res.status 89 | ngx.header.content_type = "application/json; charset=utf-8" 90 | ngx.print(res.body) 91 | ngx.exit(ngx.HTTP_FORBIDDEN) 92 | else 93 | send_token(token) 94 | end 95 | end 96 | 97 | -- Generates a token 98 | function generate_token(client_id) 99 | local token = ts.sha1_digest(tostring(random.bytes(20, true)) .. client_id) 100 | return { ["access_token"] = token, ["token_type"] = "bearer", ["expires_in"] = 604800 } 101 | end 102 | 103 | -- Stores the token in 3scale. You can change the default ttl value of 604800 seconds (7 days) to your desired ttl. 104 | function store_token(params, token) 105 | local body = ts.build_query({ app_id = params.client_id, token = token.access_token, user_id = params.user_id , ttl = token.expires_in }) 106 | local stored = ngx.location.capture( "/_threescale/oauth_store_token", { method = ngx.HTTP_POST, body = body } ) 107 | return { ["status"] = stored.status , ["body"] = stored.body } 108 | end 109 | 110 | -- Returns the token to the client 111 | function send_token(token) 112 | ngx.header.content_type = "application/json; charset=utf-8" 113 | ngx.say(cjson.encode(token)) 114 | ngx.exit(ngx.HTTP_OK) 115 | end 116 | 117 | local params = extract_params() 118 | 119 | local is_valid = check_credentials(params) 120 | 121 | if is_valid then 122 | get_token(params) 123 | end 124 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/token-generation/nginx.conf: -------------------------------------------------------------------------------- 1 | ## NEED CHANGE (defines the user of the nginx workers) 2 | # user user group; 3 | 4 | ## THIS PARAMETERS BE SAFELY OVER RIDDEN BY YOUR DEFAULT NGINX CONF 5 | worker_processes 2; 6 | env THREESCALE_DEPLOYMENT_ENV; 7 | # error_log stderr notice; 8 | # daemon off; 9 | # error_log logs/error.log warn; 10 | events { 11 | worker_connections 256; 12 | } 13 | 14 | http { 15 | lua_shared_dict api_keys 10m; 16 | server_names_hash_bucket_size 128; 17 | lua_package_path ";;$prefix/?.lua;$prefix/conf/?.lua"; 18 | init_by_lua 'math.randomseed(ngx.time()) ; cjson = require("cjson")'; 19 | 20 | resolver 8.8.8.8 8.8.4.4; 21 | 22 | upstream backend_CHANGE_ME_API_BACKEND { 23 | # service name: CHANGE_ME_SERVICE_NAME ; 24 | server CHANGE_ME_API_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 25 | } 26 | 27 | upstream backend_CHANGE_ME_IDP_BACKEND { 28 | server CHANGE_ME_IDP_BACKEND:CHANGE_ME_PORT max_fails=5 fail_timeout=30; 29 | } 30 | 31 | server { 32 | # Enabling the Lua code cache is strongly encouraged for production use. Here it is enabled by default for testing and development purposes 33 | lua_code_cache off; 34 | listen 80; 35 | ## CHANGE YOUR SERVER_NAME TO YOUR CUSTOM DOMAIN OR LEAVE IT BLANK IF ONLY HAVE ONE 36 | server_name CHANGE_ME_SERVER_NAME; 37 | underscores_in_headers on; 38 | set_by_lua $deployment 'return os.getenv("THREESCALE_DEPLOYMENT_ENV")'; 39 | set $threescale_backend "https://su1.3scale.net:443"; 40 | 41 | location = /_threescale/check_credentials { 42 | internal; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 46 | 47 | set $provider_key CHANGE_ME_PROVIDER_KEY; 48 | set $service_id CHANGE_ME_SERVICE_ID; 49 | 50 | proxy_pass $threescale_backend/transactions/oauth_authorize.xml?provider_key=$provider_key&service_id=$service_id&$args; 51 | } 52 | 53 | location = /oauth/token { 54 | proxy_set_header X-Real-IP $remote_addr; 55 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 56 | proxy_set_header Host $http_host; 57 | proxy_set_header Content-Type "application/x-www-form-urlencoded"; 58 | 59 | set $provider_key CHANGE_ME_PROVIDER_KEY; 60 | 61 | content_by_lua_file get_token.lua; 62 | } 63 | 64 | location = /_idp/check_credentials { 65 | internal; 66 | ## CHANGE_ME: You will need to add any authentication and other headers required by the IDP in this location block 67 | ## e.g proxy_set_header Authorization "Basic ${auth}"; 68 | proxy_set_header X-Real-IP $remote_addr; 69 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 70 | proxy_set_header Host $host; 71 | 72 | proxy_redirect off; 73 | proxy_max_temp_file_size 0; 74 | 75 | proxy_method POST; 76 | proxy_pass https://CHANGE_ME_IDP_BACKEND/CHANGE_ME_USER_VALIDATION_ENDPOINT; 77 | } 78 | 79 | location = /_threescale/oauth_store_token { 80 | internal; 81 | proxy_set_header X-Real-IP $remote_addr; 82 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 83 | proxy_set_header Host "su1.3scale.net"; #needed. backend discards other hosts 84 | 85 | set $provider_key CHANGE_ME_PROVIDER_KEY; 86 | set $service_id CHANGE_ME_SERVICE_ID; 87 | 88 | proxy_method POST; 89 | proxy_pass $threescale_backend/services/$service_id/oauth_access_tokens.xml?provider_key=$provider_key; 90 | } 91 | 92 | location = /threescale_oauth_authrep { 93 | internal; 94 | proxy_set_header Host "su1.3scale.net"; 95 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 96 | proxy_set_header X-3scale-OAuth2-Grant-Type "password"; 97 | 98 | set $provider_key CHANGE_ME_PROVIDER_KEY; 99 | set $service_id CHANGE_ME_SERVICE_ID; 100 | 101 | proxy_pass $threescale_backend/transactions/oauth_authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 102 | } 103 | 104 | location = /threescale_authrep { 105 | internal; 106 | set $provider_key CHANGE_ME_PROVIDER_KEY; 107 | 108 | proxy_pass $threescale_backend/transactions/authrep.xml?provider_key=$provider_key&service_id=$service_id&$usage&$credentials&log%5Bcode%5D=$arg_code&log%5Brequest%5D=$arg_req&log%5Bresponse%5D=$arg_resp; 109 | proxy_set_header Host "su1.3scale.net"; 110 | proxy_set_header X-3scale-User-Agent "nginx$deployment"; 111 | } 112 | 113 | location = /out_of_band_oauth_authrep_action { 114 | internal; 115 | proxy_pass_request_headers off; 116 | ##set $provider_key "YOUR_3SCALE_PROVIDER_KEY"; 117 | ##needs to be in both places, better not to have it on location / for potential security issues, req. are internal 118 | set $provider_key CHANGE_ME_PROVIDER_KEY; 119 | 120 | 121 | content_by_lua "require('nginx').post_action_content()"; 122 | } 123 | 124 | location / { 125 | set $provider_key null; 126 | set $cached_key null; 127 | set $credentials null; 128 | set $usage null; 129 | set $service_id CHANGE_ME_SERVICE_ID; 130 | set $proxy_pass null; 131 | set $secret_token null; 132 | set $resp_body null; 133 | set $resp_headers null; 134 | set $access_token null; 135 | 136 | proxy_ignore_client_abort on; 137 | 138 | ## CHANGE THE PATH TO POINT TO THE RIGHT FILE ON YOUR FILESYSTEM IF NEEDED 139 | access_by_lua "require('nginx').access()"; 140 | 141 | body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or "") .. string.sub(ngx.arg[1], 1, 1000) 142 | if ngx.arg[2] then ngx.var.resp_body = ngx.ctx.buffered end'; 143 | header_filter_by_lua 'ngx.var.resp_headers = cjson.encode(ngx.resp.get_headers())'; 144 | 145 | proxy_pass $proxy_pass; 146 | proxy_set_header X-Real-IP $remote_addr; 147 | proxy_set_header Host $host; 148 | proxy_set_header X-3scale-proxy-secret-token $secret_token; 149 | 150 | post_action /out_of_band_oauth_authrep_action; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/token-generation/nginx.lua: -------------------------------------------------------------------------------- 1 | -- -*- mode: lua; -*- 2 | -- Version: 3 | -- Error Messages per service 4 | local _M = {} 5 | 6 | service_CHANGE_ME_SERVICE_ID = { 7 | error_auth_failed = 'Authentication failed', 8 | error_auth_missing = 'Authentication parameters missing', 9 | auth_failed_headers = 'text/plain; charset=us-ascii', 10 | auth_missing_headers = 'text/plain; charset=us-ascii', 11 | error_no_match = 'No Mapping Rule matched', 12 | no_match_headers = 'text/plain; charset=us-ascii', 13 | no_match_status = 404, 14 | auth_failed_status = 403, 15 | auth_missing_status = 403, 16 | secret_token = 'Shared_secret_sent_from_proxy_to_API_backend' 17 | } 18 | 19 | 20 | -- Logging Helpers 21 | function show_table(a) 22 | for k,v in pairs(a) do 23 | local msg = "" 24 | msg = msg.. k 25 | if type(v) == "string" then 26 | msg = msg.. " => " .. v 27 | end 28 | ngx.log(0,msg) 29 | end 30 | end 31 | 32 | function log_message(str) 33 | ngx.log(0, str) 34 | end 35 | 36 | function log(content) 37 | if type(content) == "table" then 38 | show_table(content) 39 | else 40 | log_message(content) 41 | end 42 | newline() 43 | end 44 | 45 | function newline() 46 | ngx.log(0," --- ") 47 | end 48 | -- End Logging Helpers 49 | 50 | -- Error Codes 51 | function error_no_credentials(service) 52 | ngx.status = service.auth_missing_status 53 | ngx.header.content_type = service.auth_missing_headers 54 | ngx.print(service.error_auth_missing) 55 | ngx.exit(ngx.HTTP_OK) 56 | end 57 | 58 | function error_authorization_failed(service) 59 | ngx.status = service.auth_failed_status 60 | ngx.header.content_type = service.auth_failed_headers 61 | ngx.print(service.error_auth_failed) 62 | ngx.exit(ngx.HTTP_OK) 63 | end 64 | 65 | function error_no_match(service) 66 | ngx.status = service.no_match_status 67 | ngx.header.content_type = service.no_match_headers 68 | ngx.print(service.error_no_match) 69 | ngx.exit(ngx.HTTP_OK) 70 | end 71 | -- End Error Codes 72 | 73 | --[[ 74 | Aux function to split a string 75 | ]]-- 76 | 77 | function string:split(delimiter) 78 | local result = { } 79 | local from = 1 80 | local delim_from, delim_to = string.find( self, delimiter, from ) 81 | if delim_from == nil then return {self} end 82 | while delim_from do 83 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 84 | from = delim_to + 1 85 | delim_from, delim_to = string.find( self, delimiter, from ) 86 | end 87 | table.insert( result, string.sub( self, from ) ) 88 | return result 89 | end 90 | 91 | function first_values(a) 92 | r = {} 93 | for k,v in pairs(a) do 94 | if type(v) == "table" then 95 | r[k] = v[1] 96 | else 97 | r[k] = v 98 | end 99 | end 100 | return r 101 | end 102 | 103 | function set_or_inc(t, name, delta) 104 | return (t[name] or 0) + delta 105 | end 106 | 107 | function build_querystring(query) 108 | local qstr = "" 109 | 110 | for i,v in pairs(query) do 111 | qstr = qstr .. 'usage[' .. i .. ']' .. '=' .. v .. '&' 112 | end 113 | return string.sub(qstr, 0, #qstr-1) 114 | end 115 | 116 | 117 | --- 118 | -- Builds a query string from a table. 119 | -- 120 | -- This is the inverse of parse_query. 121 | -- @param query A dictionary table where table['name'] = 122 | -- value. 123 | -- @return A query string (like "name=value2&name=value2"). 124 | ----------------------------------------------------------------------------- 125 | function build_query(query) 126 | local qstr = "" 127 | 128 | for i,v in pairs(query) do 129 | qstr = qstr .. i .. '=' .. v .. '&' 130 | end 131 | return string.sub(qstr, 0, #qstr-1) 132 | end 133 | 134 | --[[ 135 | 136 | Mapping between url path to 3scale methods. In here you must output the usage string encoded as a query_string param. 137 | Here there is an example of 2 resources (word, and sentence) and 3 methods. The complexity of this function depends 138 | on the level of control you want to apply. If you only want to report hits for any of your methods it would be as simple 139 | as this: 140 | 141 | function extract_usage(request) 142 | return "usage[hits]=1&" 143 | end 144 | 145 | In addition. You do not have to do this on LUA, you can do it straight from the nginx conf via the location. For instance: 146 | 147 | location ~ ^/v1/word { 148 | set $provider_key null; 149 | set $app_id null; 150 | set $app_key null; 151 | set $usage "usage[hits]=1&"; 152 | 153 | access_by_lua_file /Users/solso/3scale/proxy/nginx_sentiment.lua; 154 | 155 | proxy_pass http://sentiment_backend; 156 | proxy_set_header X-Real-IP $remote_addr; 157 | proxy_set_header Host $host; 158 | } 159 | 160 | This is totally up to you. We prefer to keep the nginx conf as clean as possible. But you might already have declared 161 | the resources there, in this case, it's better to declare the $usage explicitly 162 | 163 | ]]-- 164 | 165 | matched_rules2 = "" 166 | 167 | function extract_usage_CHANGE_ME_SERVICE_ID(request) 168 | local t = string.split(request," ") 169 | local method = t[1] 170 | local q = string.split(t[2], "?") 171 | local path = q[1] 172 | local found = false 173 | local usage_t = {} 174 | local m = "" 175 | local matched_rules = {} 176 | local params = {} 177 | 178 | local args = get_auth_params(nil, method) 179 | 180 | -- mapping rules go here, e.g 181 | local m = ngx.re.match(path,[=[^/]=]) 182 | if (m and method == "GET") then 183 | -- rule: / -- 184 | 185 | table.insert(matched_rules, "/") 186 | 187 | usage_t["hits"] = set_or_inc(usage_t, "hits", 1) 188 | found = true 189 | end 190 | 191 | -- if there was no match, usage is set to nil and it will respond a 404, this behavior can be changed 192 | if found then 193 | matched_rules2 = table.concat(matched_rules, ", ") 194 | return build_querystring(usage_t) 195 | else 196 | return nil 197 | end 198 | end 199 | 200 | --[[ 201 | Authorization logic 202 | ]]-- 203 | 204 | function get_auth_params(where, method) 205 | local params = {} 206 | if where == "headers" then 207 | params = ngx.req.get_headers() 208 | elseif method == "GET" then 209 | params = ngx.req.get_uri_args() 210 | else 211 | ngx.req.read_body() 212 | params = ngx.req.get_post_args() 213 | end 214 | return first_values(params) 215 | end 216 | 217 | function get_credentials_app_id_app_key(params, service) 218 | if params["app_id"] == nil or params["app_key"] == nil then 219 | error_no_credentials(service) 220 | end 221 | end 222 | 223 | function get_credentials_access_token(params, service) 224 | if params["access_token"] == nil and params["authorization"] == nil then -- TODO: check where the params come 225 | error_no_credentials(service) 226 | end 227 | end 228 | 229 | function get_credentials_user_key(params, service) 230 | if params["user_key"] == nil then 231 | error_no_credentials(service) 232 | end 233 | end 234 | 235 | function get_debug_value() 236 | local h = ngx.req.get_headers() 237 | if h["X-3scale-debug"] == 'CHANGE_ME_PROVIDER_KEY' then 238 | return true 239 | else 240 | return false 241 | end 242 | end 243 | 244 | function authorize(auth_strat, params, service) 245 | if auth_strat == 'oauth' then 246 | oauth(params, service) 247 | else 248 | authrep(params, service) 249 | end 250 | end 251 | 252 | function oauth(params, service) 253 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 254 | local access_tokens = ngx.shared.api_keys 255 | local is_known = access_tokens:get(ngx.var.cached_key) 256 | 257 | if is_known ~= 200 then 258 | local res = ngx.location.capture("/threescale_oauth_authrep", { share_all_vars = true }) 259 | 260 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 261 | if res.status ~= 200 then 262 | access_tokens:delete(ngx.var.cached_key) 263 | ngx.status = res.status 264 | ngx.header.content_type = "application/json" 265 | ngx.var.cached_key = nil 266 | error_authorization_failed(service) 267 | else 268 | access_tokens:set(ngx.var.cached_key,200) 269 | end 270 | 271 | ngx.var.cached_key = nil 272 | end 273 | end 274 | 275 | function authrep(params, service) 276 | ngx.var.cached_key = ngx.var.cached_key .. ":" .. ngx.var.usage 277 | local api_keys = ngx.shared.api_keys 278 | local is_known = api_keys:get(ngx.var.cached_key) 279 | 280 | if is_known ~= 200 then 281 | local res = ngx.location.capture("/threescale_authrep", { share_all_vars = true }) 282 | 283 | -- IN HERE YOU DEFINE THE ERROR IF CREDENTIALS ARE PASSED, BUT THEY ARE NOT VALID 284 | if res.status ~= 200 then 285 | -- remove the key, if it's not 200 let's go the slow route, to 3scale's backend 286 | api_keys:delete(ngx.var.cached_key) 287 | ngx.status = res.status 288 | ngx.header.content_type = "application/json" 289 | ngx.var.cached_key = nil 290 | error_authorization_failed(service) 291 | else 292 | api_keys:set(ngx.var.cached_key,200) 293 | end 294 | 295 | ngx.var.cached_key = nil 296 | end 297 | end 298 | 299 | function add_trans(usage) 300 | local us = usage:split("&") 301 | local ret = "" 302 | for i,v in ipairs(us) do 303 | ret = ret .. "transactions[0][usage]" .. string.sub(v, 6) .. "&" 304 | end 305 | return string.sub(ret, 1, -2) 306 | end 307 | 308 | 309 | function _M.access() 310 | local params = {} 311 | local host = ngx.req.get_headers()["Host"] 312 | local auth_strat = "" 313 | local service = {} 314 | 315 | if ngx.status == 403 then 316 | ngx.say("Throttling due to too many requests") 317 | ngx.exit(403) 318 | end 319 | 320 | if ngx.var.service_id == 'CHANGE_ME_SERVICE_ID' then 321 | local parameters = get_auth_params("CHANGE_ME_AUTH_PARAMS_LOCATION", string.split(ngx.var.request, " ")[1] ) 322 | service = service_CHANGE_ME_SERVICE_ID -- 323 | ngx.var.secret_token = service.secret_token 324 | 325 | -- Do this to remove token type, e.g Bearer from token 326 | -- params.access_token = string.split(parameters["authorization"], " ")[2] 327 | -- ngx.var.access_token = params.access_token 328 | 329 | ngx.var.access_token = parameters.access_token 330 | params.access_token = parameters.access_token 331 | get_credentials_access_token(params , service_CHANGE_ME_SERVICE_ID) 332 | ngx.var.cached_key = "CHANGE_ME_SERVICE_ID" .. ":" .. params.access_token 333 | auth_strat = "oauth" 334 | ngx.var.service_id = "CHANGE_ME_SERVICE_ID" 335 | ngx.var.proxy_pass = "https://backend_CHANGE_ME_API_BACKEND" 336 | ngx.var.usage = extract_usage_CHANGE_ME_SERVICE_ID(ngx.var.request) 337 | end 338 | 339 | ngx.var.credentials = build_query(params) 340 | 341 | -- if true then 342 | -- log(ngx.var.app_id) 343 | -- log(ngx.var.app_key) 344 | -- log(ngx.var.usage) 345 | -- end 346 | 347 | -- WHAT TO DO IF NO USAGE CAN BE DERIVED FROM THE REQUEST. 348 | if ngx.var.usage == nil then 349 | ngx.header["X-3scale-matched-rules"] = '' 350 | error_no_match(service) 351 | end 352 | 353 | if get_debug_value() then 354 | ngx.header["X-3scale-matched-rules"] = matched_rules2 355 | ngx.header["X-3scale-credentials"] = ngx.var.credentials 356 | ngx.header["X-3scale-usage"] = ngx.var.usage 357 | ngx.header["X-3scale-hostname"] = ngx.var.hostname 358 | end 359 | 360 | -- this would be better with the whole authrep call, with user_id, and everything so that 361 | -- it can be replayed if it's a cached response 362 | 363 | authorize(auth_strat, params, service) 364 | 365 | end 366 | 367 | 368 | function _M.post_action_content() 369 | local method, path, headers = ngx.req.get_method(), ngx.var.request_uri, ngx.req.get_headers() 370 | 371 | local req = cjson.encode{method=method, path=path, headers=headers} 372 | local resp = cjson.encode{ body = ngx.var.resp_body, headers = cjson.decode(ngx.var.resp_headers)} 373 | 374 | local cached_key = ngx.var.cached_key 375 | if cached_key ~= nil and cached_key ~= "null" then 376 | local status_code = ngx.var.status 377 | local res1 = ngx.location.capture("/threescale_oauth_authrep?code=".. status_code .. "&req=" .. ngx.escape_uri(req) .. "&resp=" .. ngx.escape_uri(resp), { share_all_vars = true }) 378 | if res1.status ~= 200 then 379 | local access_tokens = ngx.shared.api_keys 380 | access_tokens:delete(cached_key) 381 | end 382 | end 383 | 384 | ngx.exit(ngx.HTTP_OK) 385 | end 386 | 387 | 388 | return _M 389 | 390 | -- END OF SCRIPT 391 | -------------------------------------------------------------------------------- /oauth2/resource-owner-password-flow/token-generation/threescale_utils.lua: -------------------------------------------------------------------------------- 1 | -- threescale_utils.lua 2 | local M = {} -- public interface 3 | 4 | -- private 5 | -- Logging Helpers 6 | function M.show_table(t, ...) 7 | local indent = 0 --arg[1] or 0 8 | local indentStr="" 9 | for i = 1,indent do indentStr=indentStr.." " end 10 | 11 | for k,v in pairs(t) do 12 | if type(v) == "table" then 13 | msg = indentStr .. M.show_table(v or '', indent+1) 14 | else 15 | msg = indentStr .. k .. " => " .. v 16 | end 17 | M.log_message(msg) 18 | end 19 | end 20 | 21 | function M.log_message(str) 22 | ngx.log(0, str) 23 | end 24 | 25 | function M.newline() 26 | ngx.log(0," --- ") 27 | end 28 | 29 | function M.log(content) 30 | if type(content) == "table" then 31 | M.log_message(M.show_table(content)) 32 | else 33 | M.log_message(content) 34 | end 35 | M.newline() 36 | end 37 | 38 | -- End Logging Helpers 39 | 40 | -- Table Helpers 41 | function M.keys(t) 42 | local n=0 43 | local keyset = {} 44 | for k,v in pairs(t) do 45 | n=n+1 46 | keyset[n]=k 47 | end 48 | return keyset 49 | end 50 | -- End Table Helpers 51 | 52 | 53 | function M.dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then 58 | k = '"'..k..'"' 59 | end 60 | s = s .. '['..k..'] = ' .. M.dump(v) .. ',' 61 | end 62 | return s .. '} ' 63 | else 64 | return tostring(o) 65 | end 66 | end 67 | 68 | function M.sha1_digest(s) 69 | local str = require "resty.string" 70 | return str.to_hex(ngx.sha1_bin(s)) 71 | end 72 | 73 | -- returns true iif all elems of f_req are among actual's keys 74 | function M.required_params_present(f_req, actual) 75 | local req = {} 76 | for k,v in pairs(actual) do 77 | req[k] = true 78 | end 79 | for i,v in ipairs(f_req) do 80 | if not req[v] then 81 | return false 82 | end 83 | end 84 | return true 85 | end 86 | 87 | function M.connect_redis(red) 88 | local ok, err = red:connect("127.0.0.1", 6379) 89 | if not ok then 90 | ngx.say("failed to connect: ", err) 91 | ngx.exit(ngx.HTTP_OK) 92 | end 93 | return ok, err 94 | end 95 | 96 | -- error and exist 97 | function M.error(text) 98 | ngx.say(text) 99 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 100 | end 101 | 102 | function M.missing_args(text) 103 | ngx.say(text) 104 | ngx.exit(ngx.HTTP_OK) 105 | end 106 | 107 | --- 108 | -- Builds a query string from a table. 109 | -- 110 | -- This is the inverse of parse_query. 111 | -- @param query A dictionary table where table['name'] = 112 | -- value. 113 | -- @return A query string (like "name=value2&name=value2"). 114 | ----------------------------------------------------------------------------- 115 | function M.build_query(query) 116 | local qstr = "" 117 | 118 | for i,v in pairs(query) do 119 | qstr = qstr .. i .. '=' .. v .. '&' 120 | end 121 | return string.sub(qstr, 0, #qstr-1) 122 | end 123 | 124 | --[[ 125 | Aux function to split a string 126 | ]]-- 127 | 128 | function string:split(delimiter) 129 | local result = { } 130 | local from = 1 131 | local delim_from, delim_to = string.find( self, delimiter, from ) 132 | if delim_from == nil then return {self} end 133 | while delim_from do 134 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 135 | from = delim_to + 1 136 | delim_from, delim_to = string.find( self, delimiter, from ) 137 | end 138 | table.insert( result, string.sub( self, from ) ) 139 | return result 140 | end 141 | 142 | return M 143 | 144 | -- -- Example usage: 145 | -- local MM = require 'mymodule' 146 | -- MM.bar() 147 | --------------------------------------------------------------------------------