oauth2-nginx-auth-backend 0.2.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/.drone.yml +15 -0
- data/.gitignore +50 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/Dockerfile +12 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/README.md +57 -0
- data/bin/ops_oauth2_server.rb +48 -0
- data/lib/ops/oauth2.rb +3 -0
- data/lib/ops/oauth2/auth.rb +90 -0
- data/lib/ops/oauth2/google.rb +142 -0
- data/lib/ops/oauth2/slack.rb +119 -0
- data/lib/ops/oauth2/version.rb +7 -0
- data/oauth2-nginx-auth-backend.gemspec +28 -0
- data/static/oauth2/images/devops-logo.png +0 -0
- data/static/oauth2/images/google-large.png +0 -0
- data/static/oauth2/images/slack-large.png +0 -0
- data/views/index.erb +81 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34843ad4d62cf09d89d2263cd138ea003034dfed
|
4
|
+
data.tar.gz: b83686fa6849f02934e0f4bb5085f2d8cab4b777
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9dbcfdc27299569cb7d7e498eee4c5be57c7e7157e49e49d2a9cbe578265738541bfc8503c1647845a709116363dc75200b0297e8a9a66ca5b71b944164a9596
|
7
|
+
data.tar.gz: e749170db36dbd8e26e5b8fa0456a97daa954c4e4ae0ef01d319577264f7408e5f917c23ca6332e5141b05b2408eb02bee1fbbb7778eca876873bd25c32ee725
|
data/.drone.yml
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/Dockerfile
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
oauth2-nginx-auth-backend (0.2.0)
|
5
|
+
httparty (~> 0.15)
|
6
|
+
sinatra (~> 2.0)
|
7
|
+
sinatra-contrib (~> 2.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
ast (2.4.0)
|
13
|
+
backports (3.11.1)
|
14
|
+
httparty (0.16.1)
|
15
|
+
multi_xml (>= 0.5.2)
|
16
|
+
multi_json (1.13.1)
|
17
|
+
multi_xml (0.6.0)
|
18
|
+
mustermann (1.0.2)
|
19
|
+
parallel (1.12.1)
|
20
|
+
parser (2.5.0.5)
|
21
|
+
ast (~> 2.4.0)
|
22
|
+
powerpack (0.1.1)
|
23
|
+
rack (2.0.4)
|
24
|
+
rack-protection (2.0.1)
|
25
|
+
rack
|
26
|
+
rainbow (3.0.0)
|
27
|
+
rubocop (0.54.0)
|
28
|
+
parallel (~> 1.10)
|
29
|
+
parser (>= 2.5)
|
30
|
+
powerpack (~> 0.1)
|
31
|
+
rainbow (>= 2.2.2, < 4.0)
|
32
|
+
ruby-progressbar (~> 1.7)
|
33
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
34
|
+
ruby-progressbar (1.9.0)
|
35
|
+
sinatra (2.0.1)
|
36
|
+
mustermann (~> 1.0)
|
37
|
+
rack (~> 2.0)
|
38
|
+
rack-protection (= 2.0.1)
|
39
|
+
tilt (~> 2.0)
|
40
|
+
sinatra-contrib (2.0.1)
|
41
|
+
backports (>= 2.0)
|
42
|
+
multi_json
|
43
|
+
mustermann (~> 1.0)
|
44
|
+
rack-protection (= 2.0.1)
|
45
|
+
sinatra (= 2.0.1)
|
46
|
+
tilt (>= 1.3, < 3)
|
47
|
+
tilt (2.0.8)
|
48
|
+
unicode-display_width (1.3.0)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
bundler (~> 1.14)
|
55
|
+
oauth2-nginx-auth-backend!
|
56
|
+
rubocop (~> 0.49)
|
57
|
+
|
58
|
+
BUNDLED WITH
|
59
|
+
1.16.1
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Nginx Configuration
|
2
|
+
|
3
|
+
### /etc/nginx/oauth2-auth.conf
|
4
|
+
```
|
5
|
+
auth_request /oauth2/verify;
|
6
|
+
error_page 401 = https://auth.example.com/oauth2/sign_in;
|
7
|
+
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
8
|
+
add_header Set-Cookie $auth_cookie;
|
9
|
+
|
10
|
+
```
|
11
|
+
|
12
|
+
### /etc/nginx/oauth2-location.conf
|
13
|
+
```
|
14
|
+
location /oauth2/ {
|
15
|
+
proxy_method GET;
|
16
|
+
proxy_pass http://127.0.0.1:3000;
|
17
|
+
proxy_set_header Content-Length "";
|
18
|
+
proxy_set_header Host $host;
|
19
|
+
proxy_set_header X-Real-IP $remote_addr;
|
20
|
+
proxy_set_header X-Scheme $scheme;
|
21
|
+
proxy_set_header X-Auth-Request-Redirect $scheme://$server_name$request_uri;
|
22
|
+
}
|
23
|
+
|
24
|
+
```
|
25
|
+
|
26
|
+
### /etc/oauth2/oauth2.conf example
|
27
|
+
```
|
28
|
+
{
|
29
|
+
"auth": {
|
30
|
+
"cookie_domain": ".devops.dance",
|
31
|
+
"cookie_name_permissions": "DDIntranetPermissions",
|
32
|
+
"cookie_name_redirect": "DDIntranetRedirect",
|
33
|
+
"cookie_name_signature": "DDIntranetSignature",
|
34
|
+
"cookie_ttl": 86400,
|
35
|
+
"default_redirect_page": "https://oauth.devops.dance/",
|
36
|
+
"oauth_shared_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
37
|
+
},
|
38
|
+
"google": {
|
39
|
+
"oauth_client_id": "XXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com",
|
40
|
+
"oauth_client_secret": "XXXXXXXXXXXXXXXXXXXXXXXX",
|
41
|
+
"oauth_redirect_url": "https://oauth.devops.dance/oauth2/google/authorize",
|
42
|
+
"oauth_server_url": "https://oauth.devops.dance/",
|
43
|
+
"whitelisted_domains": [
|
44
|
+
"smatly.com"
|
45
|
+
],
|
46
|
+
"whitelisted_emails": []
|
47
|
+
},
|
48
|
+
"slack": {
|
49
|
+
"oauth_client_id": "XXXXXXXXXXXXXXXXXXXXXXXXX",
|
50
|
+
"oauth_client_secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
51
|
+
"oauth_redirect_url": "https://oauth.devops.dance/oauth2/slack/authorize",
|
52
|
+
"whitelisted_domains": [
|
53
|
+
"devops-dance"
|
54
|
+
]
|
55
|
+
}
|
56
|
+
}
|
57
|
+
```
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'sinatra/cookies'
|
5
|
+
require 'ops/oauth2/google'
|
6
|
+
require 'ops/oauth2/slack'
|
7
|
+
require 'ops/oauth2/auth'
|
8
|
+
|
9
|
+
# Main HTTPServer class handling requests
|
10
|
+
class HTTPServer < Sinatra::Base
|
11
|
+
helpers Sinatra::Cookies
|
12
|
+
|
13
|
+
slack = Slack.new
|
14
|
+
google = Google.new
|
15
|
+
|
16
|
+
set :port, 3000
|
17
|
+
set :bind, '0.0.0.0'
|
18
|
+
set :cookie_options, domain: Auth.cookie_domain, secure: true, httponly: true
|
19
|
+
set :public_folder, 'static'
|
20
|
+
set :views, Proc.new { File.join(root, "..", "views") }
|
21
|
+
set :environment, :production
|
22
|
+
|
23
|
+
get '/oauth2/google/sign_in' do
|
24
|
+
redirect google.oauth_auth_redirect
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/oauth2/google/authorize' do
|
28
|
+
return google.authorize(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
get '/oauth2/slack/sign_in' do
|
32
|
+
redirect slack.oauth_auth_redirect
|
33
|
+
end
|
34
|
+
|
35
|
+
get '/oauth2/slack/authorize' do
|
36
|
+
return slack.authorize(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
get '/oauth2/verify' do
|
40
|
+
Auth.authorized?(cookies, request)
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/oauth2/sign_in' do
|
44
|
+
erb :index
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
HTTPServer.run!
|
data/lib/ops/oauth2.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
# Authorization
|
8
|
+
class Auth
|
9
|
+
def self.cookie_name_permissions
|
10
|
+
ENV['OAUTH_COOKIE_NAME_PERMISSIONS'] || configuration.dig('auth', 'cookie_name_permissions') || abort('Missing OAUTH_COOKIE_NAME_PERMISSIONS')
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.cookie_name_signature
|
14
|
+
'OKIntranetSignature'
|
15
|
+
ENV['OAUTH_COOKIE_NAME_SIGNATURE'] || configuration.dig('auth', 'cookie_name_signature') || abort('Missing OAUTH_COOKIE_NAME_SIGNATURE')
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.cookie_name_redirect
|
19
|
+
ENV['OAUTH_COOKIE_NAME_REDIRECT'] || configuration.dig('auth', 'cookie_name_redirect') || abort('Missing OAUTH_COOKIE_NAME_REDIRECT')
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.header_request_redirect_url
|
23
|
+
'HTTP_X_AUTH_REQUEST_REDIRECT'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.environment
|
27
|
+
ENV['OAUTH_ENVIRONMENT'] || configuration.dig('auth', 'running_environment') || abort('Missing OAUTH_ENVIRONMENT')
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.secret
|
31
|
+
ENV['OAUTH_SHARED_SECRET'] || configuration.dig('auth', 'oauth_shared_secret') || abort('Missing OAUTH_SHARED_SECRET')
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_redirect_page
|
35
|
+
ENV['DEFAULT_REDIRECT_PAGE'] || configuration.dig('auth', 'default_redirect_page') || abort('Missing DEFAULT_REDIRECT_PAGE')
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cookie_domain
|
39
|
+
ENV['OAUTH_COOKIE_DOMAIN'] || self.configuration.dig('auth', 'cookie_domain') || abort('Missing OAUTH_COOKIE_DOMAIN')
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.cookie_ttl
|
43
|
+
return ENV['OAUTH_COOKIE_TTL'].to_i if ENV['OAUTH_COOKIE_TTL']
|
44
|
+
configuration.dig('auth', 'cookie_ttl').to_i || abort('Missing OAUTH_COOKIE_TTL')
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.sign(data)
|
48
|
+
digest = OpenSSL::Digest.new('sha256')
|
49
|
+
Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data))
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.trusted?(cookies, request)
|
53
|
+
cookies[cookie_name_signature] == sign([cookies[cookie_name_permissions], request.user_agent].join)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.configuration_file
|
57
|
+
'/etc/oauth2/oauth2.conf'
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.configuration
|
61
|
+
@configuration ||= JSON.parse(File.read(configuration_file))
|
62
|
+
rescue
|
63
|
+
abort("Missing or invalid #{self.configuration_file}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.authorize(info, request)
|
67
|
+
cookies = {}
|
68
|
+
cookies[cookie_name_permissions] = Base64.encode64(info.to_json)
|
69
|
+
cookies[cookie_name_signature] = sign([Base64.encode64(info.to_json), request.user_agent].join)
|
70
|
+
cookies
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.go_to_auth(cookies, request)
|
74
|
+
cookies[cookie_name_redirect] = request.env[header_request_redirect_url]
|
75
|
+
401
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.untrusted(cookies, request)
|
79
|
+
cookies.delete(cookie_name_signature)
|
80
|
+
cookies.delete(cookie_name_permissions)
|
81
|
+
go_to_auth(cookies, request)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.authorized?(cookies, request)
|
85
|
+
return go_to_auth(cookies, request) unless cookies.key?(cookie_name_permissions)
|
86
|
+
return go_to_auth(cookies, request) unless cookies.key?(cookie_name_signature)
|
87
|
+
return untrusted(cookies, request) unless trusted?(cookies, request)
|
88
|
+
200
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'httparty'
|
5
|
+
require 'json'
|
6
|
+
require 'ops/oauth2/auth'
|
7
|
+
|
8
|
+
# Basic support of google oauth2
|
9
|
+
class Google
|
10
|
+
def oauth_client_secret
|
11
|
+
ENV['GOOGLE_OAUTH_CLIENT_SECRET'] || configuration.dig('google', 'oauth_client_secret') || abort('Missing GOOGLE_OAUTH_CLIENT_SECRET')
|
12
|
+
end
|
13
|
+
|
14
|
+
def oauth_client_id
|
15
|
+
ENV['GOOGLE_OAUTH_CLIENT_ID'] || configuration.dig('google', 'oauth_client_id') || abort('Missing GOOGLE_OAUTH_CLIENT_ID')
|
16
|
+
end
|
17
|
+
|
18
|
+
def redirect_url
|
19
|
+
ENV['GOOGLE_OAUTH_REDIRECT_URL'] || configuration.dig('google', 'oauth_redirect_url') || abort('Missing GOOGLE_OAUTH_REDIRECT_URL')
|
20
|
+
end
|
21
|
+
|
22
|
+
def state_url
|
23
|
+
ENV['OAUTH_SERVER_URL'] || configuration.dig('google', 'oauth_server_url') || abort('Missing OAUTH_SERVER_URL')
|
24
|
+
end
|
25
|
+
|
26
|
+
def google_whitelisted_domains
|
27
|
+
ENV['GOOGLE_WHITELISTED_DOMAINS'] || configuration.dig('google', 'whitelisted_domains') || abort('Missing GOOGLE_WHITELISTED_DOMAINS')
|
28
|
+
end
|
29
|
+
|
30
|
+
def google_whitelisted_emails
|
31
|
+
ENV['GOOGLE_WHITELISTED_EMAILS'] || configuration.dig('google', 'whitelisted_emails') || abort('Missing GOOGLE_WHITELISTED_EMAILS')
|
32
|
+
end
|
33
|
+
|
34
|
+
def oauth_auth_url
|
35
|
+
'https://accounts.google.com/o/oauth2/auth'
|
36
|
+
end
|
37
|
+
|
38
|
+
def oauth_token_url
|
39
|
+
'https://accounts.google.com/o/oauth2/token'
|
40
|
+
end
|
41
|
+
|
42
|
+
def oauth_userinfo_url
|
43
|
+
'https://www.googleapis.com/oauth2/v2/userinfo'
|
44
|
+
end
|
45
|
+
|
46
|
+
def configuration_file
|
47
|
+
'/etc/oauth2/oauth2.conf'
|
48
|
+
end
|
49
|
+
|
50
|
+
def configuration
|
51
|
+
@configuration ||= JSON.parse(File.read(configuration_file))
|
52
|
+
rescue
|
53
|
+
abort("Missing or invalid #{configuration_file}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def oauth_auth_url_params
|
57
|
+
[
|
58
|
+
"client_id=#{oauth_client_id}",
|
59
|
+
'scope=email',
|
60
|
+
'response_type=code',
|
61
|
+
"redirect_uri=#{CGI.escape(redirect_url)}",
|
62
|
+
"state=#{CGI.escape(state_url)}",
|
63
|
+
'login_hint='
|
64
|
+
].join('&')
|
65
|
+
end
|
66
|
+
|
67
|
+
def oauth_auth_redirect
|
68
|
+
[
|
69
|
+
oauth_auth_url,
|
70
|
+
'?',
|
71
|
+
oauth_auth_url_params
|
72
|
+
].join
|
73
|
+
end
|
74
|
+
|
75
|
+
def permitted?(user_info)
|
76
|
+
email = user_info.dig('email')
|
77
|
+
return false unless email
|
78
|
+
_, domain = email.split('@')
|
79
|
+
return true if google_whitelisted_emails.include? email
|
80
|
+
return true if google_whitelisted_domains.include? domain
|
81
|
+
false
|
82
|
+
rescue
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def user_info(authorization)
|
87
|
+
headers = {
|
88
|
+
'Authorization' => "Bearer #{authorization}"
|
89
|
+
}
|
90
|
+
HTTParty.get(oauth_userinfo_url, headers: headers)
|
91
|
+
end
|
92
|
+
|
93
|
+
def access_token(params)
|
94
|
+
response = verify(params[:code])
|
95
|
+
response.dig('access_token')
|
96
|
+
end
|
97
|
+
|
98
|
+
def authorize(s)
|
99
|
+
if s.params.key? 'code'
|
100
|
+
# Make sure we got an access token, otherwise redirect to auth page
|
101
|
+
at = access_token(s.params)
|
102
|
+
return Auth.go_to_auth(s.cookies, s.request) unless at
|
103
|
+
|
104
|
+
# Get google user info and make sure it's permitted to get auth.
|
105
|
+
ui = user_info(at)
|
106
|
+
return 403 unless permitted?(ui)
|
107
|
+
|
108
|
+
# Naive sanity check of google response
|
109
|
+
return Auth.go_to_auth(s.cookies, s.request) unless ui.key? 'email'
|
110
|
+
|
111
|
+
# Now we're safe to authorize => set cookies
|
112
|
+
Auth.authorize(ui, s.request).each do |cookie, value|
|
113
|
+
s.cookies.set(cookie, value: value, expires: Time.now + Auth.cookie_ttl)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Redirect user to the original page if redirect cookie present.
|
117
|
+
if s.cookies.key?(Auth.cookie_name_redirect)
|
118
|
+
redirect_url = s.cookies[Auth.cookie_name_redirect]
|
119
|
+
s.cookies.delete(Auth.cookie_name_redirect)
|
120
|
+
s.redirect redirect_url
|
121
|
+
end
|
122
|
+
|
123
|
+
# Redirect to default page if you don't have redirect cookie
|
124
|
+
s.redirect Auth.default_redirect_page
|
125
|
+
else
|
126
|
+
Auth.go_to_auth(s.cookies, request)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def verify(code)
|
131
|
+
options = {
|
132
|
+
body: {
|
133
|
+
client_id: oauth_client_id,
|
134
|
+
client_secret: oauth_client_secret,
|
135
|
+
code: code,
|
136
|
+
redirect_uri: redirect_url,
|
137
|
+
grant_type: 'authorization_code'
|
138
|
+
}
|
139
|
+
}
|
140
|
+
HTTParty.post(oauth_token_url, options)
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'httparty'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# Basic support of slack oauth2
|
8
|
+
class Slack
|
9
|
+
def oauth_client_secret
|
10
|
+
ENV['SLACK_OAUTH_CLIENT_SECRET'] || configuration.dig('slack', 'oauth_client_secret') || abort('Missing SLACK_OAUTH_CLIENT_SECRET')
|
11
|
+
end
|
12
|
+
|
13
|
+
def oauth_client_id
|
14
|
+
ENV['SLACK_OAUTH_CLIENT_ID'] || configuration.dig('slack', 'oauth_client_id') || abort('Missing SLACK_OAUTH_CLIENT_ID')
|
15
|
+
end
|
16
|
+
|
17
|
+
def redirect_url
|
18
|
+
ENV['SLACK_OAUTH_REDIRECT_URL'] || configuration.dig('slack', 'oauth_redirect_url') || abort('Missing SLACK_OAUTH_REDIRECT_URL')
|
19
|
+
end
|
20
|
+
|
21
|
+
def whitelisted_domains
|
22
|
+
return ENV['SLACK_WHITELISTED_DOMAINS'].split(',') if ENV['SLACK_WHITELISTED_DOMAINS']
|
23
|
+
configuration.dig('slack', 'whitelisted_domains') || abort('Missing SLACK_WHITELISTED_DOMAINS')
|
24
|
+
end
|
25
|
+
|
26
|
+
def oauth_auth_url
|
27
|
+
'https://slack.com/oauth/authorize'
|
28
|
+
end
|
29
|
+
|
30
|
+
def oauth_token_url
|
31
|
+
'https://slack.com/api/oauth.access'
|
32
|
+
end
|
33
|
+
|
34
|
+
def oauth_scopes
|
35
|
+
'identity.basic,identity.team'
|
36
|
+
end
|
37
|
+
|
38
|
+
def configuration_file
|
39
|
+
'/etc/oauth2/oauth2.conf'
|
40
|
+
end
|
41
|
+
|
42
|
+
def configuration
|
43
|
+
@configuration ||= JSON.parse(File.read(configuration_file))
|
44
|
+
rescue
|
45
|
+
abort("Missing or invalid #{configuration_file}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def oauth_auth_url_params
|
49
|
+
[
|
50
|
+
"client_id=#{oauth_client_id}",
|
51
|
+
"scope=#{oauth_scopes}",
|
52
|
+
"redirect_uri=#{CGI.escape(redirect_url)}"
|
53
|
+
].join('&')
|
54
|
+
end
|
55
|
+
|
56
|
+
def oauth_auth_redirect
|
57
|
+
[
|
58
|
+
oauth_auth_url,
|
59
|
+
'?',
|
60
|
+
oauth_auth_url_params
|
61
|
+
].join
|
62
|
+
end
|
63
|
+
|
64
|
+
def user_info(response)
|
65
|
+
payload = JSON.parse(response)
|
66
|
+
{
|
67
|
+
'user': payload['user']
|
68
|
+
}
|
69
|
+
rescue
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def domain(response)
|
74
|
+
payload = JSON.parse(response)
|
75
|
+
payload.dig('team', 'domain')
|
76
|
+
rescue
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def authorize(s)
|
81
|
+
response = verify(s.params)
|
82
|
+
return 403 unless response.dig('ok')
|
83
|
+
|
84
|
+
# get slack response domain and authorize if included in whitelisted
|
85
|
+
return 403 unless whitelisted_domains.include? domain(response.body)
|
86
|
+
|
87
|
+
# make sure we get a proper user info structure
|
88
|
+
ui = user_info(response.body)
|
89
|
+
return 403 unless ui
|
90
|
+
|
91
|
+
# build and authorize cookies
|
92
|
+
Auth.authorize(ui, s.request).each do |cookie, value|
|
93
|
+
s.cookies.set(cookie, value: value, expires: Time.now + Auth.cookie_ttl)
|
94
|
+
end
|
95
|
+
|
96
|
+
# redirect user to a proper place if needed
|
97
|
+
if s.cookies.key?(Auth.cookie_name_redirect)
|
98
|
+
redirect_url = s.cookies[Auth.cookie_name_redirect]
|
99
|
+
s.cookies.delete(Auth.cookie_name_redirect)
|
100
|
+
s.redirect redirect_url
|
101
|
+
end
|
102
|
+
|
103
|
+
# redirect to a default page
|
104
|
+
s.redirect Auth.default_redirect_page
|
105
|
+
end
|
106
|
+
|
107
|
+
def verify(params)
|
108
|
+
return { 'ok': false } unless params.dig('code')
|
109
|
+
options = {
|
110
|
+
body: {
|
111
|
+
client_id: oauth_client_id,
|
112
|
+
client_secret: oauth_client_secret,
|
113
|
+
code: params.dig('code'),
|
114
|
+
redirect_uri: redirect_url
|
115
|
+
}
|
116
|
+
}
|
117
|
+
HTTParty.post(oauth_token_url, options)
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ops/oauth2/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'oauth2-nginx-auth-backend'
|
8
|
+
spec.version = Ops::Oauth2::VERSION
|
9
|
+
spec.authors = ['Bartek Jarocki']
|
10
|
+
spec.email = ['bartek@smatly.com']
|
11
|
+
|
12
|
+
spec.summary = 'oauth2 nginx auth_request backend'
|
13
|
+
spec.homepage = 'https://github.com/bjarocki/oauth2-nginx-auth-backend'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = 'bin'
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
23
|
+
spec.add_development_dependency 'rubocop', '~> 0.49'
|
24
|
+
|
25
|
+
spec.add_runtime_dependency 'httparty', '~> 0.15'
|
26
|
+
spec.add_runtime_dependency 'sinatra', '~> 2.0'
|
27
|
+
spec.add_runtime_dependency 'sinatra-contrib', '~> 2.0'
|
28
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
data/views/index.erb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta name="generator" content=
|
5
|
+
"HTML Tidy for HTML5 for Linux version 5.4.0">
|
6
|
+
<link rel="stylesheet" href=
|
7
|
+
"https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.1/css/bulma.min.css">
|
8
|
+
|
9
|
+
<style>
|
10
|
+
.loginBtn {
|
11
|
+
box-sizing: border-box;
|
12
|
+
position: relative;
|
13
|
+
width: 100%;
|
14
|
+
margin: 18px 0px 0px 0px;
|
15
|
+
padding: 0 15px 0 52px;
|
16
|
+
text-align: left;
|
17
|
+
line-height: 42px;
|
18
|
+
white-space: nowrap;
|
19
|
+
border-radius: 0.2em;
|
20
|
+
background-color: white;
|
21
|
+
font-size: 1.3em;
|
22
|
+
color: #333;
|
23
|
+
border: 1px solid rgba(10,10,10,0.1);
|
24
|
+
}
|
25
|
+
.loginBtn:before {
|
26
|
+
content: "";
|
27
|
+
box-sizing: border-box;
|
28
|
+
position: absolute;
|
29
|
+
top: 2px;
|
30
|
+
left: 2px;
|
31
|
+
width: 38px;
|
32
|
+
height: 38px;
|
33
|
+
}
|
34
|
+
.loginBtn:focus {
|
35
|
+
outline: none;
|
36
|
+
}
|
37
|
+
.loginBtn:active {
|
38
|
+
box-shadow: inset 0 0 0 32px rgba(0,0,0,0.1);
|
39
|
+
}
|
40
|
+
.loginBtn--slack {
|
41
|
+
}
|
42
|
+
.loginBtn--slack:before {
|
43
|
+
border-right: rgba(0,0,0,0.1) 1px solid;
|
44
|
+
background: url('images/slack-large.png') 0px 0px no-repeat;
|
45
|
+
}
|
46
|
+
.loginBtn--slack:hover,
|
47
|
+
.loginBtn--slack:focus {
|
48
|
+
background: #78d4b6
|
49
|
+
}
|
50
|
+
.loginBtn--google {
|
51
|
+
background: #4285f4;
|
52
|
+
color: #fff;
|
53
|
+
}
|
54
|
+
.loginBtn--google:before {
|
55
|
+
border-right: rgba(0,0,0,0.1) 1px solid;
|
56
|
+
background: url('images/google-large.png') 0px 0px no-repeat;
|
57
|
+
}
|
58
|
+
.loginBtn--google:hover,
|
59
|
+
.loginBtn--google:focus {
|
60
|
+
background: #E74B37;
|
61
|
+
}
|
62
|
+
</style>
|
63
|
+
<title></title>
|
64
|
+
</head>
|
65
|
+
<body>
|
66
|
+
<section class="section">
|
67
|
+
<div class="columns is-mobile is-centered">
|
68
|
+
<div class="column is-one-quarter is-narrow">
|
69
|
+
<div class="box">
|
70
|
+
<p class="is-centered"></p>
|
71
|
+
<figure class="image is-256x256">
|
72
|
+
<img src="images/devops-logo.png">
|
73
|
+
</figure>
|
74
|
+
<p><button class="loginBtn loginBtn--slack" onclick="location.href='/oauth2/slack/sign_in'">Login with Slack</button></p>
|
75
|
+
<p><button class="loginBtn loginBtn--google" onclick="location.href='/oauth2/google/sign_in'">Login with Google</button></p>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</section>
|
80
|
+
</body>
|
81
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oauth2-nginx-auth-backend
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bartek Jarocki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-03-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.49'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.49'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httparty
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.15'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.15'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sinatra
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra-contrib
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- bartek@smatly.com
|
86
|
+
executables:
|
87
|
+
- ops_oauth2_server.rb
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".drone.yml"
|
92
|
+
- ".gitignore"
|
93
|
+
- ".rubocop.yml"
|
94
|
+
- ".ruby-version"
|
95
|
+
- Dockerfile
|
96
|
+
- Gemfile
|
97
|
+
- Gemfile.lock
|
98
|
+
- README.md
|
99
|
+
- bin/ops_oauth2_server.rb
|
100
|
+
- lib/ops/oauth2.rb
|
101
|
+
- lib/ops/oauth2/auth.rb
|
102
|
+
- lib/ops/oauth2/google.rb
|
103
|
+
- lib/ops/oauth2/slack.rb
|
104
|
+
- lib/ops/oauth2/version.rb
|
105
|
+
- oauth2-nginx-auth-backend.gemspec
|
106
|
+
- static/oauth2/images/devops-logo.png
|
107
|
+
- static/oauth2/images/google-large.png
|
108
|
+
- static/oauth2/images/slack-large.png
|
109
|
+
- views/index.erb
|
110
|
+
homepage: https://github.com/bjarocki/oauth2-nginx-auth-backend
|
111
|
+
licenses: []
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.6.14
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: oauth2 nginx auth_request backend
|
133
|
+
test_files: []
|