nginx_omniauth_adapter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Dockerfile +18 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/Rakefile +6 -0
- data/config.ru +106 -0
- data/example/Procfile +3 -0
- data/example/nginx-site.conf +73 -0
- data/example/nginx.conf +17 -0
- data/example/test_backend.rb +21 -0
- data/lib/nginx_omniauth_adapter/app.rb +281 -0
- data/lib/nginx_omniauth_adapter/version.rb +3 -0
- data/lib/nginx_omniauth_adapter.rb +8 -0
- data/nginx_omniauth_adapter.gemspec +29 -0
- data/script/console +14 -0
- data/script/setup +7 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3908b68bd6b56d6a923bca0dc36763e55645a03
|
4
|
+
data.tar.gz: 4aaf9ce2fdb95257094210060b44fdb9e533d163
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b42a2d07c831778ea2b9c96e17e928cf2c5c0891edac619d89dfa74d27a14a0994469b2d8fb08e1082b3ce648659e9fbfdc7dcffe461679196621717199dc08
|
7
|
+
data.tar.gz: 366cf4d330531166973f61168e0ccbebf6cbcd7939e5ebc2dd91f87d15aa6cca0ecbca9b18463ce0e44b5b5e27406b6e3c034a889e2ac904dda3185fc9386de8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
FROM quay.io/sorah/rbenv:2.2
|
2
|
+
MAINTAINER sorah
|
3
|
+
|
4
|
+
RUN mkdir -p /app
|
5
|
+
|
6
|
+
ADD Gemfile* /tmp/
|
7
|
+
ADD nginx_omniauth_adapter.gemspec /tmp/
|
8
|
+
RUN mkdir -p /tmp/lib/nginx_omniauth_adapter
|
9
|
+
ADD lib/nginx_omniauth_adapter/version.rb /tmp/lib/nginx_omniauth_adapter/version.rb
|
10
|
+
RUN cd /tmp && bundle install -j4 --path vendor/bundle --without 'development test'
|
11
|
+
|
12
|
+
WORKDIR /app
|
13
|
+
ADD . /app
|
14
|
+
RUN cp -a /tmp/.bundle /tmp/vendor /app/
|
15
|
+
|
16
|
+
EXPOSE 8080
|
17
|
+
ENV RACK_ENV=production
|
18
|
+
CMD ["bundle", "exec", "rackup", "-p", "8080", "-o", "0.0.0.0", "config.ru"]
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Shota Fukumori (sora_h)
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# NginxOmniauthAdapter - Use omniauth for nginx `auth_request`
|
2
|
+
|
3
|
+
Use [omniauth](https://github.com/intridea/omniauth) for your nginx's authentication via ngx_http_auth_request_module.
|
4
|
+
|
5
|
+
NginxOmniauthAdapter provides small Rack app (built with Sinatra) for `auth_request`.
|
6
|
+
|
7
|
+
## Prerequisite
|
8
|
+
|
9
|
+
- nginx with ngx_http_auth_request_module
|
10
|
+
|
11
|
+
## Quick example
|
12
|
+
|
13
|
+
```
|
14
|
+
$ bundle install
|
15
|
+
|
16
|
+
$ cd example/
|
17
|
+
$ foreman start
|
18
|
+
```
|
19
|
+
|
20
|
+
http://ngx-auth-test.127.0.0.1.xip.io:18080/
|
21
|
+
|
22
|
+
(make sure to have nginx on your PATH)
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### Steps
|
27
|
+
|
28
|
+
1. Start adapter app with proper configuration
|
29
|
+
2. enable `auth_request` and add some endpoints on nginx
|
30
|
+
- See `example/nginx-site.conf` for nginx configuration.
|
31
|
+
|
32
|
+
### Running with Rubygems
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# Gemfile
|
36
|
+
gem 'nginx_omniauth_adapter'
|
37
|
+
```
|
38
|
+
|
39
|
+
Then write `config.ru` then deploy it. (see ./config.ru for example)
|
40
|
+
|
41
|
+
### Using docker
|
42
|
+
|
43
|
+
TBD
|
44
|
+
|
45
|
+
## Configuration
|
46
|
+
|
47
|
+
environment variable is available only on included config.ru (or Docker image).
|
48
|
+
|
49
|
+
- `:providers`: omniauth provider names.
|
50
|
+
- `:secret` `$NGX_OMNIAUTH_SESSION_SECRET`: Rack session secret. Should be set when not on dev mode
|
51
|
+
- `:host` `$NGX_OMNIAUTH_HOST`: URL of adapter. This is used for redirection. Should include protocol (e.g. `http://example.com`.)
|
52
|
+
- If this is not specified, adapter will perform redirect using given `Host` header.
|
53
|
+
- `:allowed_app_callback_url` `$NGX_OMNIAUTH_ALLOWED_APP_CALLBACK_URL` (regexp): If specified, URL only matches to this are allowed for app callback url.
|
54
|
+
- `:allowed_back_to_url` `$NGX_OMNIAUTH_ALLOWED_BACK_TO_URL` (regexp): If specified, URL only matches to this are allowed for back_to url.
|
55
|
+
- `:app_refresh_interval` `NGX_OMNIAUTH_APP_REFRESH_INTERVAL` (integer): Interval to require refresh session cookie on app domain (in second, default 1 day).
|
56
|
+
- `:adapter_refresh_interval` `NGX_OMNIAUTH_ADAPTER_REFRESH_INTERVAL` (integer): Interval to require re-logging in on adapter domain (in second, default 3 days).
|
57
|
+
|
58
|
+
### Included config.ru (or Docker)
|
59
|
+
|
60
|
+
You can set configuration via environment variables.
|
61
|
+
|
62
|
+
The following variables are only available on included config.ru:
|
63
|
+
|
64
|
+
- `$NGX_OMNIAUTH_SESSION_COOKIE_NAME`: session cookie name (default `ngx_oauth`)
|
65
|
+
- `$NGX_OMNIAUTH_SESSION_COOKIE_TIMEOUT`: session cookie expiry (default 3 days)
|
66
|
+
- `$NGX_OMNIAUTH_DEV=1` or `$RACK_ENV=development`
|
67
|
+
- enable dev mode (omniauth developer provider)
|
68
|
+
- github provider
|
69
|
+
- `$NGX_OMNIAUTH_GITHUB_KEY`, `$NGX_OMNIAUTH_GITHUB_SECRET`: application key + secret.
|
70
|
+
- `$NGX_OMNIAUTH_GITHUB_HOST`: (optional) Set if you'd like to use GitHub Enterprise instance (e.g. `https://YOUR-GITHUB-ENTERPRISE`)
|
71
|
+
- `$NGX_OMNIAUTH_GITHUB_TEAMS`: (optional) Restrict to specified teams (e.g. `awesomeorganization/owners`)
|
72
|
+
- google_oauth2 provider
|
73
|
+
- `$NGX_OMNIAUTH_GOOGLE_KEY`, `$NGX_OMNIAUTH_GOOGLE_SECRET`: oauth2 key + secret.
|
74
|
+
- `$NGX_OMNIAUTH_GOOGLE_HD`: (optional) Restrict to specified hosted domain (Google Apps Domain).
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
### Manually (Rack)
|
79
|
+
|
80
|
+
If you're going to write `config.ru` from scratch, make sure:
|
81
|
+
|
82
|
+
- OmniAuth is included in middleware stack
|
83
|
+
- Rack session is enabled in middleware stack
|
84
|
+
|
85
|
+
Then run:
|
86
|
+
|
87
|
+
``` ruby
|
88
|
+
run NginxOmniauthAdapter.app(
|
89
|
+
providers: %i(developer),
|
90
|
+
secret: secret_base64, # optional
|
91
|
+
# ... (set more configuration, see above variable list)
|
92
|
+
)
|
93
|
+
```
|
94
|
+
|
95
|
+
## How it works
|
96
|
+
|
97
|
+
1. _browser_ access to restricted area (where `auth_request` has enabled)
|
98
|
+
2. _nginx_ sends subrequest to `/_auth/challenge`. It will be proxied to _adapter app_ (`GET /test`)
|
99
|
+
3. _adapter app_ `/test` returns 401 when _request (browser)_ doesn't have valid cookie
|
100
|
+
4. _nginx_ handles 401 with `error_page`, so do internal redirection (`/_auth/initiate`)
|
101
|
+
5. _nginx_ handles `/_auth/initiate`. It will be proxied to _adapter app_ `GET /initiate`.
|
102
|
+
- Also _nginx_ passes some information for callback to _adapter app._
|
103
|
+
- `x-ngx-oauth-initiate-back-to` URL to back after logged in
|
104
|
+
- `x-ngx-oauth-initiate-callback` URL that proxies to _adapter app_ `/callback`. This must be same domain to _backend app_ for cookie.
|
105
|
+
6. _adapter app_ `GET /initiate` redirects to `/auth/:provider`.
|
106
|
+
7. _Browser_ do some authenticate in _adapter app_ with Omniauth.
|
107
|
+
8. _adapter app's_ omniauth callback sets valid session, then redirects to `/_auth/callback`, where specified at `x-ngx-oauth-initiate-callback`.
|
108
|
+
- _Adapter app_ gives GET parameter named `session` on redirect. It contains encrypted session.
|
109
|
+
9. _nginx_ handles `/_auth/callback`. It will be proxied to _adapter app_ `/callback`.
|
110
|
+
- This decrypts given encrypted session string and set to cookie.
|
111
|
+
- Then redirect to `x-ngx-oauth-initiate-back-to`.
|
112
|
+
10. _browser_ backs to URL where attempted to access first, at step 1.
|
113
|
+
11. _nginx_ sends auth subrequest to _backend app_ `/test`.
|
114
|
+
12. _backend app_ `/test` returns 200, because request has valid session cookie.
|
115
|
+
13. _nginx_ returns response as usual.
|
116
|
+
|
117
|
+
## Development
|
118
|
+
|
119
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
120
|
+
|
121
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
122
|
+
|
123
|
+
## Contributing
|
124
|
+
|
125
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/nginx_omniauth_adapter.
|
126
|
+
|
127
|
+
|
128
|
+
## License
|
129
|
+
|
130
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
131
|
+
|
data/Rakefile
ADDED
data/config.ru
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'nginx_omniauth_adapter'
|
2
|
+
require 'omniauth'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
dev = ENV['NGX_OMNIAUTH_DEV'] == '1' || ENV['RACK_ENV'] == 'development'
|
7
|
+
test = ENV['RACK_ENV'] == 'test'
|
8
|
+
|
9
|
+
if test
|
10
|
+
dev = true
|
11
|
+
warn 'TEST MODE'
|
12
|
+
OmniAuth.config.test_mode = true
|
13
|
+
OmniAuth.config.mock_auth[:developer] = {provider: 'developer', uid: '42', info: {}}
|
14
|
+
end
|
15
|
+
|
16
|
+
if !dev && !ENV['NGX_OMNIAUTH_SESSION_SECRET']
|
17
|
+
raise 'You should specify $NGX_OMNIAUTH_SESSION_SECRET'
|
18
|
+
end
|
19
|
+
|
20
|
+
allowed_app_callback_url = if ENV['NGX_OMNIAUTH_ALLOWED_APP_CALLBACK_URL']
|
21
|
+
Regexp.new(ENV['NGX_OMNIAUTH_ALLOWED_APP_CALLBACK_URL'])
|
22
|
+
else
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
allowed_back_to_url = if ENV['NGX_OMNIAUTH_ALLOWED_BACK_TO_URL']
|
27
|
+
Regexp.new(ENV['NGX_OMNIAUTH_ALLOWED_BACK_TO_URL'])
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
use(
|
33
|
+
Rack::Session::Cookie,
|
34
|
+
key: ENV['NGX_OMNIAUTH_SESSION_COOKIE_NAME'] || 'ngx_oauth',
|
35
|
+
expire_after: ENV['NGX_OMNIAUTH_SESSION_COOKIE_TIMEOUT'] ? ENV['NGX_OMNIAUTH_SESSION_COOKIE_TIMEOUT'].to_i : (60 * 60 * 24 * 3),
|
36
|
+
secret: ENV['NGX_OMNIAUTH_SESSION_SECRET'] || 'ngx_oauth_secret_dev',
|
37
|
+
old_secret: ENV['NGX_OMNIAUTH_SESSION_SECRET_OLD'],
|
38
|
+
)
|
39
|
+
|
40
|
+
providers = []
|
41
|
+
|
42
|
+
gh_teams = ENV['NGX_OMNIAUTH_GITHUB_TEAMS'] && ENV['NGX_OMNIAUTH_GITHUB_TEAMS'].split(/[, ]/)
|
43
|
+
|
44
|
+
use OmniAuth::Builder do
|
45
|
+
if ENV['NGX_OMNIAUTH_GITHUB_KEY'] && ENV['NGX_OMNIAUTH_GITHUB_SECRET']
|
46
|
+
require 'omniauth-github'
|
47
|
+
gh_client_options = {}
|
48
|
+
if ENV['NGX_OMNIAUTH_GITHUB_HOST']
|
49
|
+
gh_client_options[:site] = "#{ENV['NGX_OMNIAUTH_GITHUB_HOST']}/api/v3"
|
50
|
+
gh_client_options[:authorize_url] = "#{ENV['NGX_OMNIAUTH_GITHUB_HOST']}/login/oauth/authorize"
|
51
|
+
gh_client_options[:token_url] = "#{ENV['NGX_OMNIAUTH_GITHUB_HOST']}/login/oauth/access_token"
|
52
|
+
end
|
53
|
+
|
54
|
+
gh_scope = ''
|
55
|
+
if ENV['NGX_OMNIAUTH_GITHUB_TEAMS']
|
56
|
+
gh_scope = 'read:org'
|
57
|
+
end
|
58
|
+
|
59
|
+
provider :github, ENV['NGX_OMNIAUTH_GITHUB_KEY'], ENV['NGX_OMNIAUTH_GITHUB_SECRET'], client_options: gh_client_options, scope: gh_scope
|
60
|
+
providers << :github
|
61
|
+
end
|
62
|
+
|
63
|
+
if ENV['NGX_OMNIAUTH_GOOGLE_KEY'] && ENV['NGX_OMNIAUTH_GOOGLE_SECRET']
|
64
|
+
require 'omniauth-google-oauth2'
|
65
|
+
provider :google_oauth2, ENV['NGX_OMNIAUTH_GOOGLE_KEY'], ENV['NGX_OMNIAUTH_GOOGLE_SECRET'], hd: ENV['NGX_OMNIAUTH_GOOGLE_HD']
|
66
|
+
providers << :google_oauth2
|
67
|
+
end
|
68
|
+
|
69
|
+
if dev
|
70
|
+
provider :developer
|
71
|
+
providers << :developer
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
run NginxOmniauthAdapter.app(
|
76
|
+
providers: providers,
|
77
|
+
secret: ENV['NGX_OMNIAUTH_SECRET'],
|
78
|
+
host: ENV['NGX_OMNIAUTH_HOST'],
|
79
|
+
allowed_app_callback_url: allowed_app_callback_url,
|
80
|
+
allowed_back_to_url: allowed_back_to_url,
|
81
|
+
app_refresh_interval: ENV['NGX_OMNIAUTH_APP_REFRESH_INTERVAL'] && ENV['NGX_OMNIAUTH_APP_REFRESH_INTERVAL'].to_i,
|
82
|
+
adapter_refresh_interval: ENV['NGX_OMNIAUTH_ADAPTER_REFRESH_INTERVAL'] && ENV['NGX_OMNIAUTH_APP_REFRESH_INTERVAL'].to_i,
|
83
|
+
policy_proc: proc {
|
84
|
+
if gh_teams && current_user[:provider] == 'github'
|
85
|
+
unless (current_user_data[:gh_teams] || []).any? { |team| gh_teams.include?(team) }
|
86
|
+
next false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
true
|
91
|
+
},
|
92
|
+
on_login_proc: proc {
|
93
|
+
auth = env['omniauth.auth']
|
94
|
+
case auth[:provider]
|
95
|
+
when 'github'
|
96
|
+
if gh_teams
|
97
|
+
api_host = ENV['NGX_OMNIAUTH_GITHUB_HOST'] ? "#{ENV['NGX_OMNIAUTH_GITHUB_HOST']}/api/v3" : "https://api.github.com"
|
98
|
+
current_user_data[:gh_teams] = open("#{api_host}/user/teams", 'Authorization' => "token #{auth['credentials']['token']}") { |io|
|
99
|
+
JSON.parse(io.read).map {|_| "#{_['organization']['login']}/#{_['slug']}" }.select { |team| gh_teams.include?(team) }
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
true
|
105
|
+
},
|
106
|
+
)
|
data/example/Procfile
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# vim: ft=nginx
|
2
|
+
|
3
|
+
# nginx_omniauth_adapter app
|
4
|
+
upstream auth_adapter {
|
5
|
+
server 127.0.0.1:18081 fail_timeout=0;
|
6
|
+
}
|
7
|
+
server {
|
8
|
+
listen 127.0.0.1:18080;
|
9
|
+
server_name ngx-auth.127.0.0.1.xip.io;
|
10
|
+
|
11
|
+
location / {
|
12
|
+
proxy_pass http://auth_adapter;
|
13
|
+
proxy_set_header Host $http_host;
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
# server where you want to restrict access
|
18
|
+
upstream app {
|
19
|
+
server 127.0.0.1:18082 fail_timeout=0;
|
20
|
+
}
|
21
|
+
server {
|
22
|
+
listen 127.0.0.1:18080;
|
23
|
+
server_name ngx-auth-test.127.0.0.1.xip.io;
|
24
|
+
|
25
|
+
# Restricted area
|
26
|
+
location / {
|
27
|
+
auth_request /_auth/challenge;
|
28
|
+
# Trick - do internal redirection when auth_request says "need auth".
|
29
|
+
error_page 401 = /_auth/initiate;
|
30
|
+
|
31
|
+
# Receive user info from adapter
|
32
|
+
auth_request_set $ngx_oauth_provider $upstream_http_x_ngx_oauth_provider;
|
33
|
+
auth_request_set $ngx_oauth_user $upstream_http_x_ngx_oauth_user;
|
34
|
+
auth_request_set $ngx_oauth_info $upstream_http_x_ngx_oauth_info;
|
35
|
+
proxy_set_header x-ngx-oauth-provider $ngx_oauth_provider;
|
36
|
+
proxy_set_header x-ngx-oauth-user $ngx_oauth_user;
|
37
|
+
proxy_set_header x-ngx-oauth-info $ngx_oauth_info;
|
38
|
+
|
39
|
+
# pass to backend application as usual as you do.
|
40
|
+
proxy_pass http://app;
|
41
|
+
}
|
42
|
+
|
43
|
+
# STEP 1, Internal endpoint: test still session is valid (adapter: GET /test)
|
44
|
+
location = /_auth/challenge {
|
45
|
+
internal;
|
46
|
+
|
47
|
+
proxy_pass_request_body off;
|
48
|
+
proxy_set_header Content-Length "";
|
49
|
+
proxy_set_header Host $http_host;
|
50
|
+
|
51
|
+
proxy_pass http://auth_adapter/test;
|
52
|
+
}
|
53
|
+
|
54
|
+
# STEP 2, Internal endpoint: Initiate authentication. Will redirect to adapter for omniauth sequence. (adapter: GET /initiate)
|
55
|
+
location = /_auth/initiate {
|
56
|
+
internal;
|
57
|
+
proxy_pass_request_body off;
|
58
|
+
proxy_set_header Content-Length "";
|
59
|
+
proxy_set_header Host $http_host;
|
60
|
+
proxy_set_header x-ngx-oauth-initiate-back-to http://$http_host$request_uri;
|
61
|
+
proxy_set_header x-ngx-oauth-initiate-callback http://$http_host/_auth/callback;
|
62
|
+
proxy_pass http://auth_adapter/initiate;
|
63
|
+
}
|
64
|
+
|
65
|
+
# STEP 3, adapter will back here when authentication succeeded. proxy_pass to adapter to set session cookie.
|
66
|
+
location = /_auth/callback {
|
67
|
+
auth_request off;
|
68
|
+
|
69
|
+
proxy_set_header Host $http_host;
|
70
|
+
|
71
|
+
proxy_pass http://auth_adapter/callback;
|
72
|
+
}
|
73
|
+
}
|
data/example/nginx.conf
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# vim: ft=nginx
|
2
|
+
worker_processes 1;
|
3
|
+
daemon off;
|
4
|
+
|
5
|
+
error_log stderr;
|
6
|
+
|
7
|
+
events {
|
8
|
+
worker_connections 1024;
|
9
|
+
}
|
10
|
+
|
11
|
+
http {
|
12
|
+
default_type application/octet-stream;
|
13
|
+
|
14
|
+
server_names_hash_bucket_size 128;
|
15
|
+
|
16
|
+
include nginx-site.conf;
|
17
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
get '/' do
|
5
|
+
content_type :text
|
6
|
+
|
7
|
+
{
|
8
|
+
provider: request.env['HTTP_X_NGX_OAUTH_PROVIDER'],
|
9
|
+
user: request.env['HTTP_X_NGX_OAUTH_USER'],
|
10
|
+
info: JSON.parse(request.env['HTTP_X_NGX_OAUTH_INFO'].unpack('m*')[0]),
|
11
|
+
}.to_json
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/hello' do
|
15
|
+
content_type :text
|
16
|
+
'hola'
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/unauthorized' do
|
20
|
+
[401, {'Content-Type' => 'text/plain'}, '401 :(']
|
21
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'uri'
|
3
|
+
require 'time'
|
4
|
+
require 'openssl'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module NginxOmniauthAdapter
|
8
|
+
class App < Sinatra::Base
|
9
|
+
CONTEXT_RACK_ENV_NAME = 'nginx-oauth2-adapter'.freeze
|
10
|
+
SESSION_PASS_CIPHER_ALGORITHM = 'aes-256-gcm'.freeze
|
11
|
+
|
12
|
+
set :root, File.expand_path(File.join(__dir__, '..', '..', 'app'))
|
13
|
+
|
14
|
+
def self.initialize_context(config)
|
15
|
+
{}.tap do |ctx|
|
16
|
+
ctx[:config] = config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.rack(config={})
|
21
|
+
klass = self
|
22
|
+
|
23
|
+
context = initialize_context(config)
|
24
|
+
app = lambda { |env|
|
25
|
+
env[CONTEXT_RACK_ENV_NAME] = context
|
26
|
+
klass.call(env)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
helpers do
|
31
|
+
def context
|
32
|
+
request.env[CONTEXT_RACK_ENV_NAME]
|
33
|
+
end
|
34
|
+
|
35
|
+
def adapter_config
|
36
|
+
context[:config]
|
37
|
+
end
|
38
|
+
|
39
|
+
def adapter_host
|
40
|
+
adapter_config[:host]
|
41
|
+
end
|
42
|
+
|
43
|
+
def providers
|
44
|
+
adapter_config[:providers]
|
45
|
+
end
|
46
|
+
|
47
|
+
def allowed_back_to_url
|
48
|
+
adapter_config[:allowed_back_to_url] || /./
|
49
|
+
end
|
50
|
+
|
51
|
+
def allowed_app_callback_url
|
52
|
+
adapter_config[:allowed_app_callback_url] || /./
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_login_proc
|
56
|
+
adapter_config[:on_login_proc] || proc { true }
|
57
|
+
end
|
58
|
+
|
59
|
+
def policy_proc
|
60
|
+
adapter_config[:policy_proc] || proc { true }
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_back_to
|
64
|
+
# TODO:
|
65
|
+
'/'
|
66
|
+
end
|
67
|
+
|
68
|
+
def sanitized_back_to_param
|
69
|
+
p params[:back_to]
|
70
|
+
if allowed_back_to_url === params[:back_to]
|
71
|
+
params[:back_to]
|
72
|
+
else
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def sanitized_app_callback_param
|
78
|
+
p params[:callback]
|
79
|
+
if allowed_app_callback_url === params[:callback]
|
80
|
+
params[:callback]
|
81
|
+
else
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def current_user
|
87
|
+
session[:user]
|
88
|
+
end
|
89
|
+
|
90
|
+
def current_user_data
|
91
|
+
session[:user_data] ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
def current_authorized_at
|
95
|
+
session[:authorized_at] && Time.xmlschema(session[:authorized_at])
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_logged_in_at
|
99
|
+
session[:logged_in_at] && Time.xmlschema(session[:logged_in_at])
|
100
|
+
end
|
101
|
+
|
102
|
+
def app_refresh_interval
|
103
|
+
adapter_config[:app_refresh_interval] || (60 * 60 * 24)
|
104
|
+
end
|
105
|
+
|
106
|
+
def adapter_refresh_interval
|
107
|
+
adapter_config[:adapter_refresh_interval] || (60 * 60 * 24 * 30)
|
108
|
+
end
|
109
|
+
|
110
|
+
def app_authorization_expired?
|
111
|
+
app_refresh_interval && current_user && (Time.now - current_authorized_at) > app_refresh_interval
|
112
|
+
end
|
113
|
+
|
114
|
+
def adapter_authorization_expired?
|
115
|
+
adapter_refresh_interval && current_user && (Time.now - current_logged_in_at) > adapter_refresh_interval
|
116
|
+
end
|
117
|
+
|
118
|
+
def update_session!(auth = nil)
|
119
|
+
unless session[:app_callback]
|
120
|
+
raise '[BUG] app_callback is missing'
|
121
|
+
end
|
122
|
+
|
123
|
+
common_session = {
|
124
|
+
logged_in_at: session[:logged_in_at],
|
125
|
+
user_data: current_user_data,
|
126
|
+
}
|
127
|
+
|
128
|
+
if auth
|
129
|
+
common_session[:user] = {
|
130
|
+
uid: auth[:uid],
|
131
|
+
info: auth[:info],
|
132
|
+
provider: auth[:provider],
|
133
|
+
}
|
134
|
+
else
|
135
|
+
common_session[:user] = session[:user]
|
136
|
+
end
|
137
|
+
|
138
|
+
adapter_session = common_session.merge(
|
139
|
+
side: :adapter,
|
140
|
+
)
|
141
|
+
|
142
|
+
app_session = common_session.merge(
|
143
|
+
side: :app,
|
144
|
+
back_to: session.delete(:back_to),
|
145
|
+
authorized_at: Time.now.xmlschema,
|
146
|
+
)
|
147
|
+
|
148
|
+
session.merge!(adapter_session)
|
149
|
+
|
150
|
+
session_param = encrypt_session_param(app_session)
|
151
|
+
redirect "#{session.delete(:app_callback)}?session=#{session_param}"
|
152
|
+
end
|
153
|
+
|
154
|
+
def secret_key
|
155
|
+
context[:secret_key] ||= begin
|
156
|
+
if adapter_config[:secret]
|
157
|
+
adapter_config[:secret].unpack('m*')[0]
|
158
|
+
else
|
159
|
+
cipher = OpenSSL::Cipher.new(SESSION_PASS_CIPHER_ALGORITHM)
|
160
|
+
warn "WARN: :secret not set; generating randomly."
|
161
|
+
warn " If you'd like to persist, set `openssl rand -base64 #{cipher.key_len}` . Note that you have to keep it secret."
|
162
|
+
|
163
|
+
OpenSSL::Random.random_bytes(cipher.key_len)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def encrypt_session_param(session_param)
|
169
|
+
iv = nil
|
170
|
+
cipher ||= OpenSSL::Cipher.new(SESSION_PASS_CIPHER_ALGORITHM).tap do |c|
|
171
|
+
c.encrypt
|
172
|
+
c.key = secret_key
|
173
|
+
c.iv = iv = c.random_iv
|
174
|
+
c.auth_data = ''
|
175
|
+
end
|
176
|
+
|
177
|
+
plaintext = Marshal.dump(session_param)
|
178
|
+
|
179
|
+
ciphertext = cipher.update(plaintext)
|
180
|
+
ciphertext << cipher.final
|
181
|
+
|
182
|
+
URI.encode_www_form_component([{
|
183
|
+
"iv" => [iv].pack('m*'),
|
184
|
+
"data" => [ciphertext].pack('m*'),
|
185
|
+
"tag" => [cipher.auth_tag].pack('m*'),
|
186
|
+
}.to_json].pack('m*'))
|
187
|
+
end
|
188
|
+
|
189
|
+
def decrypt_session_param(raw_data)
|
190
|
+
data = JSON.parse(raw_data.unpack('m*')[0])
|
191
|
+
|
192
|
+
cipher ||= OpenSSL::Cipher.new(SESSION_PASS_CIPHER_ALGORITHM).tap do |c|
|
193
|
+
c.decrypt
|
194
|
+
c.key = secret_key
|
195
|
+
c.iv = data['iv'].unpack('m*')[0]
|
196
|
+
c.auth_data = ''
|
197
|
+
c.auth_tag = data['tag'].unpack('m*')[0]
|
198
|
+
end
|
199
|
+
|
200
|
+
plaintext = cipher.update(data['data'].unpack('m*')[0])
|
201
|
+
plaintext << cipher.final
|
202
|
+
|
203
|
+
Marshal.load(plaintext)
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
get '/' do
|
209
|
+
content_type :text
|
210
|
+
"NginxOmniauthAdapter #{NginxOmniauthAdapter::VERSION}"
|
211
|
+
end
|
212
|
+
|
213
|
+
get '/test' do
|
214
|
+
unless current_user
|
215
|
+
halt 401
|
216
|
+
end
|
217
|
+
|
218
|
+
if app_authorization_expired?
|
219
|
+
halt 401
|
220
|
+
end
|
221
|
+
|
222
|
+
unless instance_eval(&policy_proc)
|
223
|
+
halt 403
|
224
|
+
end
|
225
|
+
|
226
|
+
headers(
|
227
|
+
'x-ngx-oauth-provider' => current_user[:provider],
|
228
|
+
'x-ngx-oauth-user' => current_user[:uid],
|
229
|
+
'x-ngx-oauth-info' => [current_user[:info].to_json].pack('m*'),
|
230
|
+
)
|
231
|
+
|
232
|
+
content_type :text
|
233
|
+
'ok'.freeze
|
234
|
+
end
|
235
|
+
|
236
|
+
get '/initiate' do
|
237
|
+
back_to = URI.encode_www_form_component(request.env['HTTP_X_NGX_OAUTH_INITIATE_BACK_TO'])
|
238
|
+
callback = URI.encode_www_form_component(request.env['HTTP_X_NGX_OAUTH_INITIATE_CALLBACK'])
|
239
|
+
|
240
|
+
if back_to == '' || callback == '' || back_to.nil? || callback.nil?
|
241
|
+
halt 400, {'Content-Type' => 'text/plain'}, 'x-ngx-oauth-initiate-back-to and x-ngx-oauth-initiate-callback header are required'
|
242
|
+
end
|
243
|
+
|
244
|
+
redirect "#{adapter_host}/auth?back_to=#{back_to}&callback=#{callback}"
|
245
|
+
end
|
246
|
+
|
247
|
+
get '/auth' do
|
248
|
+
# TODO: choose provider
|
249
|
+
session[:back_to] = sanitized_back_to_param
|
250
|
+
session[:app_callback] = sanitized_app_callback_param
|
251
|
+
p [:auth, session]
|
252
|
+
|
253
|
+
if current_user && !adapter_authorization_expired?
|
254
|
+
p [:auth, :update]
|
255
|
+
update_session!
|
256
|
+
else
|
257
|
+
p [:auth, :redirect]
|
258
|
+
redirect "#{adapter_host}/auth/#{providers[0]}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
omniauth_callback = proc do
|
263
|
+
session[:user_data] = {}
|
264
|
+
|
265
|
+
unless instance_eval(&on_login_proc)
|
266
|
+
halt 403, {'Content-Type' => 'text/plain'}, 'Forbidden (on_login_proc policy)'
|
267
|
+
end
|
268
|
+
|
269
|
+
session[:logged_in_at] = Time.now.xmlschema
|
270
|
+
update_session! env['omniauth.auth']
|
271
|
+
end
|
272
|
+
get '/auth/:provider/callback', &omniauth_callback
|
273
|
+
post '/auth/:provider/callback', &omniauth_callback
|
274
|
+
|
275
|
+
get '/callback' do # app side
|
276
|
+
app_session = decrypt_session_param(params[:session])
|
277
|
+
session.merge!(app_session)
|
278
|
+
redirect session.delete(:back_to)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nginx_omniauth_adapter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "nginx_omniauth_adapter"
|
8
|
+
spec.version = NginxOmniauthAdapter::VERSION
|
9
|
+
spec.authors = ["Shota Fukumori (sora_h)"]
|
10
|
+
spec.email = ["her@sorah.jp"]
|
11
|
+
|
12
|
+
spec.summary = %q{oauth2 adapter for ngx_http_auth_request_module}
|
13
|
+
spec.homepage = "https://github.com/sorah/nginx_omniauth_adapter"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "sinatra"
|
22
|
+
spec.add_dependency "omniauth"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "rack-test"
|
28
|
+
spec.add_development_dependency "mechanize"
|
29
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "nginx_omniauth_adapter"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/script/setup
ADDED
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nginx_omniauth_adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shota Fukumori (sora_h)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: omniauth
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rack-test
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mechanize
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- her@sorah.jp
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".travis.yml"
|
121
|
+
- Dockerfile
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- config.ru
|
127
|
+
- example/Procfile
|
128
|
+
- example/nginx-site.conf
|
129
|
+
- example/nginx.conf
|
130
|
+
- example/test_backend.rb
|
131
|
+
- lib/nginx_omniauth_adapter.rb
|
132
|
+
- lib/nginx_omniauth_adapter/app.rb
|
133
|
+
- lib/nginx_omniauth_adapter/version.rb
|
134
|
+
- nginx_omniauth_adapter.gemspec
|
135
|
+
- script/console
|
136
|
+
- script/setup
|
137
|
+
homepage: https://github.com/sorah/nginx_omniauth_adapter
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
metadata: {}
|
141
|
+
post_install_message:
|
142
|
+
rdoc_options: []
|
143
|
+
require_paths:
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.5.0
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: oauth2 adapter for ngx_http_auth_request_module
|
161
|
+
test_files: []
|