ginjo-omniauth-slack 2.4.1 → 2.5.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 +4 -4
- data/.gitignore +6 -5
- data/.rdoc_options +33 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +18 -1
- data/Gemfile +5 -1
- data/README.md +498 -169
- data/Rakefile +1 -0
- data/lib/omniauth-slack/debug.rb +57 -0
- data/lib/omniauth-slack/oauth2/access_token.rb +382 -0
- data/lib/omniauth-slack/oauth2/client.rb +95 -0
- data/lib/omniauth-slack/omniauth/auth_hash.rb +10 -0
- data/lib/omniauth-slack/refinements.rb +68 -0
- data/lib/omniauth-slack/slack.rb +121 -0
- data/lib/omniauth-slack/version.rb +1 -1
- data/lib/omniauth/strategies/slack.rb +212 -343
- data/test/access_token_test.rb +123 -0
- data/test/helper.rb +40 -5
- data/test/refinements_test.rb +83 -0
- data/test/strategy_test.rb +249 -0
- data/test/support/oauth_user_token_response_v2.json +16 -0
- data/test/support/scope_base.yml +25 -0
- data/test/support/shared_examples.rb +0 -10
- data/test/test.rb +1 -249
- metadata +20 -2
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'omniauth/strategies/oauth2'
|
3
|
+
require 'omniauth/auth_hash'
|
4
|
+
require 'oauth2'
|
5
|
+
require 'omniauth-slack/omniauth/auth_hash'
|
6
|
+
|
7
|
+
|
8
|
+
# Refinements will work as long as the call to the refined method is lexically scoped with the 'using'.
|
9
|
+
|
10
|
+
module OmniAuth
|
11
|
+
module Slack
|
12
|
+
|
13
|
+
module OAuth2Refinements
|
14
|
+
refine ::OAuth2::Response do
|
15
|
+
def to_auth_hash
|
16
|
+
#Module.const_get('::OmniAuth::Slack::AuthHash').new(parsed)
|
17
|
+
::OmniAuth::Slack::AuthHash.new(parsed)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ArrayRefinements
|
23
|
+
refine ::Array do
|
24
|
+
# Sort this array according to other-array's current order.
|
25
|
+
# See https://stackoverflow.com/questions/44536537/sort-the-array-with-reference-to-another-array
|
26
|
+
# This also handles items not in the reference_array.
|
27
|
+
# Pass :beginning or :ending as the 2nd arg to specify where to put unmatched source items.
|
28
|
+
# Pass a block to specify exactly which part of source value is being used for sort.
|
29
|
+
# Example: sources.sort_with(dependencies){|v| v.name.to_s}
|
30
|
+
def sort_with(reference_array, unmatched = :beginning)
|
31
|
+
ref_index = reference_array.to_a.each_with_index.to_h
|
32
|
+
unmatched_destination = case unmatched
|
33
|
+
when /begin/; -1
|
34
|
+
when /end/; 1
|
35
|
+
when Integer; unmatched
|
36
|
+
else -1
|
37
|
+
end
|
38
|
+
#puts "Sorting array #{self} with unmatched_destination '#{unmatched_destination}' and reference index #{ref_index}"
|
39
|
+
sort_by do |v|
|
40
|
+
val = block_given? ? yield(v) : v
|
41
|
+
[ref_index[val] || (unmatched_destination * reference_array.size), val]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module StringRefinements
|
48
|
+
refine String do
|
49
|
+
def words
|
50
|
+
split(/[,\s]+/)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module CallerMethodName
|
56
|
+
def caller_method_name
|
57
|
+
#caller[0][/`([^']*)'/, 1] # This gets the method name only 1 level up.
|
58
|
+
caller[1][/`([^']*)'/, 1] # This gets the method name 2 levels up.
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.included(other)
|
62
|
+
other.send(:extend, CallerMethodName)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end # Slack
|
67
|
+
end # OmniAuth
|
68
|
+
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'omniauth-slack/refinements'
|
3
|
+
require 'omniauth-slack/oauth2/client'
|
4
|
+
require 'omniauth-slack/oauth2/access_token'
|
5
|
+
|
6
|
+
require 'rack'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module OmniAuth
|
10
|
+
module Slack
|
11
|
+
|
12
|
+
# Build an access token from access-token-hash or from token-string.
|
13
|
+
def self.build_access_token(client_id, client_key, token_string_or_hash)
|
14
|
+
client = OmniAuth::Slack::OAuth2::Client.new(
|
15
|
+
client_id,
|
16
|
+
client_key,
|
17
|
+
OmniAuth::Strategies::Slack.default_options.client_options.to_h.map{|k,v| [k.to_sym, v]}.to_h
|
18
|
+
)
|
19
|
+
|
20
|
+
access_token = case
|
21
|
+
when token_string_or_hash.is_a?(String)
|
22
|
+
OmniAuth::Slack::OAuth2::AccessToken.new(client, token_string_or_hash)
|
23
|
+
when token_string_or_hash.is_a?(Hash)
|
24
|
+
OmniAuth::Slack::OAuth2::AccessToken.from_hash(client, token_string_or_hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
access_token
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Rack middleware to verify incoming slack request signature.
|
32
|
+
#
|
33
|
+
# use OmniAuth::Slack::VerifySlackSignature
|
34
|
+
#
|
35
|
+
# ENV ... TODO: Complete this section of required env variables.
|
36
|
+
# or consider having accepting a config block in the 'use' call.
|
37
|
+
#
|
38
|
+
class VerifySlackSignature
|
39
|
+
include OmniAuth::Slack::Debug
|
40
|
+
|
41
|
+
attr_accessor :app_id, :signing_secret
|
42
|
+
|
43
|
+
def initialize(app)
|
44
|
+
@app = app
|
45
|
+
@app_id = nil
|
46
|
+
@signing_secret = nil
|
47
|
+
|
48
|
+
middleware_instance = self
|
49
|
+
|
50
|
+
if block_given?
|
51
|
+
# Can set app_id and signing_secret from here.
|
52
|
+
yield(middleware_instance)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(env)
|
57
|
+
@env = env
|
58
|
+
@logger = logger = OmniAuth.logger
|
59
|
+
|
60
|
+
debug{"calling middleware"}
|
61
|
+
|
62
|
+
env['rack.input'].rewind
|
63
|
+
body_string = env['rack.input'].read
|
64
|
+
env['rack.input'].rewind
|
65
|
+
|
66
|
+
debug{"VerifySlackSignature body_string: #{body_string}"}
|
67
|
+
|
68
|
+
body_hash =
|
69
|
+
begin
|
70
|
+
body_string && JSON.load(body_string)
|
71
|
+
rescue
|
72
|
+
{}
|
73
|
+
end
|
74
|
+
|
75
|
+
if body_hash.to_a.size == 0
|
76
|
+
debug{"not detecting JSON body"}
|
77
|
+
pass
|
78
|
+
|
79
|
+
else
|
80
|
+
api_app_id = body_hash['api_app_id']
|
81
|
+
slack_signature = env['HTTP_X_SLACK_SIGNATURE']
|
82
|
+
slack_timestamp = env['HTTP_X_SLACK_REQUEST_TIMESTAMP']
|
83
|
+
|
84
|
+
if ! [api_app_id, slack_signature, slack_timestamp].all?
|
85
|
+
logger.debug("(slack) VerifySlackSignature not detecting incoming Slack request")
|
86
|
+
pass
|
87
|
+
|
88
|
+
elsif signing_secret.to_s.empty?
|
89
|
+
logger.info("(slack) VerifySlackSignature missing signing_secret")
|
90
|
+
pass
|
91
|
+
|
92
|
+
elsif app_id && app_id.to_s != api_app_id.to_s
|
93
|
+
logger.info("(slack) VerifySlackSignature app_id mismatch")
|
94
|
+
pass
|
95
|
+
|
96
|
+
else
|
97
|
+
computed_signature = 'v0=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), signing_secret, "v0:#{slack_timestamp}:#{body_string}").to_s
|
98
|
+
rslt = (slack_signature == computed_signature)
|
99
|
+
|
100
|
+
if rslt
|
101
|
+
logger.info("(slack) VerifySlackSignature: #{rslt}")
|
102
|
+
else
|
103
|
+
logger.info("(slack) VerifySlackSignature: #{rslt} (slack: #{slack_signature}, computed: #{computed_signature})")
|
104
|
+
end
|
105
|
+
|
106
|
+
pass rslt
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def pass(result = nil)
|
112
|
+
@env['omniauth.slack.verification'] = result
|
113
|
+
debug{"set env omniauth.slack.verification to: #{result}"}
|
114
|
+
@app.call(@env)
|
115
|
+
end
|
116
|
+
|
117
|
+
end # VerifySlackSignature
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
@@ -1,411 +1,280 @@
|
|
1
|
+
# :markup: tomdoc
|
2
|
+
|
1
3
|
require 'omniauth/strategies/oauth2'
|
4
|
+
require 'omniauth-slack/refinements'
|
5
|
+
require 'omniauth-slack/slack'
|
6
|
+
require 'omniauth-slack/omniauth/auth_hash'
|
2
7
|
require 'thread'
|
3
8
|
require 'uri'
|
4
9
|
|
5
10
|
module OmniAuth
|
11
|
+
using Slack::OAuth2Refinements
|
12
|
+
|
6
13
|
module Strategies
|
7
|
-
|
14
|
+
|
15
|
+
# This is the OmniAuth strategy for Slack.
|
16
|
+
# It is used as Rack middleware.
|
17
|
+
#
|
18
|
+
# use OmniAuth::Builder do
|
19
|
+
# provider :slack, OAUTH_KEY, OAUTH_SECRET, options...
|
20
|
+
# end
|
21
|
+
#
|
8
22
|
class Slack < OmniAuth::Strategies::OAuth2
|
9
|
-
|
10
|
-
|
11
|
-
|
23
|
+
include OmniAuth::Slack::Debug
|
24
|
+
|
25
|
+
|
26
|
+
### Options ###
|
12
27
|
|
28
|
+
# Master list of authorization options handled by omniauth-slack.
|
29
|
+
# See below for redirect_uri.
|
30
|
+
#
|
31
|
+
AUTH_OPTIONS = %i(scope user_scope team team_domain)
|
32
|
+
|
33
|
+
debug{"#{self} setting up default options"}
|
34
|
+
|
35
|
+
# Default strategy name
|
36
|
+
option :name, 'slack'
|
37
|
+
|
38
|
+
# Options that can be passed with provider authorization URL.
|
39
|
+
option :authorize_options, AUTH_OPTIONS - %i(team_domain)
|
40
|
+
|
41
|
+
# OAuth2::Client options.
|
13
42
|
option :client_options, {
|
14
43
|
site: 'https://slack.com',
|
15
|
-
|
16
|
-
|
44
|
+
authorize_url: '/oauth/v2/authorize',
|
45
|
+
token_url: '/api/oauth.v2.access',
|
46
|
+
auth_scheme: :basic_auth,
|
47
|
+
raise_errors: false, # MUST be false to allow Slack's get-token response from v2 API.
|
48
|
+
history: Array.new,
|
17
49
|
}
|
18
50
|
|
51
|
+
# Authorization token-exchange API call options.
|
19
52
|
option :auth_token_params, {
|
20
53
|
mode: :query,
|
21
54
|
param_name: 'token'
|
22
55
|
}
|
56
|
+
|
57
|
+
|
58
|
+
### Omniauth Slack custom options ###
|
23
59
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
option :
|
60
|
+
# redirect_uri does not need to be in authorize_options,
|
61
|
+
# since it inserted anyway by omniauth-oauth2 during both
|
62
|
+
# the request (authorization) phase and the callback (get-token) phase.
|
63
|
+
# The magic of redirect_uri actually happens in the callback_url method.
|
64
|
+
option :redirect_uri
|
29
65
|
|
30
|
-
|
66
|
+
# Options allowed to pass from omniauth /auth/<provider> URL
|
67
|
+
# to provider authorization URL.
|
68
|
+
option :pass_through_params, %i(team)
|
69
|
+
|
70
|
+
|
71
|
+
### Data ###
|
31
72
|
|
32
73
|
# User ID is not guaranteed to be globally unique across all Slack users.
|
33
74
|
# The combination of user ID and team ID, on the other hand, is guaranteed
|
34
75
|
# to be globally unique.
|
35
|
-
|
36
|
-
|
76
|
+
#
|
77
|
+
uid { access_token&.uid }
|
78
|
+
|
79
|
+
|
80
|
+
# Gathers access_token and awarded scopes for :credentials section of AuthHash.
|
81
|
+
#
|
37
82
|
credentials do
|
38
83
|
{
|
39
|
-
|
40
|
-
scope:
|
41
|
-
|
84
|
+
token_type: access_or_user_token&.token_type,
|
85
|
+
scope: access_or_user_token&.scope,
|
86
|
+
scopes: access_or_user_token&.all_scopes,
|
87
|
+
token: access_or_user_token&.token
|
42
88
|
}
|
43
89
|
end
|
44
90
|
|
91
|
+
# Gathers a myriad of possible data returned from omniauth-slack /api/oauth.access call,
|
92
|
+
# for `:info` section of AuthHash.
|
93
|
+
# :markup: markdown
|
94
|
+
#
|
95
|
+
# You an modify the info hash from your application.
|
96
|
+
# This example adds a users_info API request and response.
|
97
|
+
# Note that this will automatically store Client request history,
|
98
|
+
# if enabled. You do not need to link the auth-hash raw-info to
|
99
|
+
# the Client history array (See notes in OmniAuth::Slack::OAuth2::Client).
|
100
|
+
#
|
101
|
+
# Example:
|
102
|
+
#
|
103
|
+
# class OmniAuth::Strategies::Slack
|
104
|
+
# original_info = info.dup
|
105
|
+
# info do
|
106
|
+
# {
|
107
|
+
# access_token: instance_exec(&original_info),
|
108
|
+
# users_info: access_token.get('/api/users.info', params: {user: access_token.user_id}, headers: {'X-Slack-User' => (access_token.user_id)}).parsed
|
109
|
+
# }
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
45
113
|
info do
|
46
|
-
|
47
|
-
|
48
|
-
define_additional_data
|
49
|
-
semaphore
|
50
|
-
end
|
51
|
-
|
52
|
-
num_threads = options.preload_data_with_threads.to_i
|
53
|
-
|
54
|
-
if num_threads > 0 && !skip_info?
|
55
|
-
preload_data_with_threads(num_threads)
|
56
|
-
end
|
57
|
-
|
58
|
-
# Start with only what we can glean from the authorization response.
|
59
|
-
hash = {
|
60
|
-
name: auth['user'].to_h['name'],
|
61
|
-
email: auth['user'].to_h['email'],
|
62
|
-
user_id: user_id,
|
63
|
-
team_name: auth['team_name'] || auth['team'].to_h['name'],
|
64
|
-
team_id: team_id,
|
65
|
-
image: auth['team'].to_h['image_48']
|
114
|
+
{
|
115
|
+
name: access_token.user_name
|
66
116
|
}
|
67
|
-
|
68
|
-
# Now add everything else, using further calls to the api, if necessary.
|
69
|
-
unless skip_info?
|
70
|
-
%w(first_name last_name phone skype avatar_hash real_name real_name_normalized).each do |key|
|
71
|
-
hash[key.to_sym] = (
|
72
|
-
user_info['user'].to_h['profile'] ||
|
73
|
-
user_profile['profile']
|
74
|
-
).to_h[key]
|
75
|
-
end
|
76
|
-
|
77
|
-
%w(deleted status color tz tz_label tz_offset is_admin is_owner is_primary_owner is_restricted is_ultra_restricted is_bot has_2fa).each do |key|
|
78
|
-
hash[key.to_sym] = user_info['user'].to_h[key]
|
79
|
-
end
|
80
|
-
|
81
|
-
more_info = {
|
82
|
-
image: (
|
83
|
-
hash[:image] ||
|
84
|
-
user_identity.to_h['image_48'] ||
|
85
|
-
user_info['user'].to_h['profile'].to_h['image_48'] ||
|
86
|
-
user_profile['profile'].to_h['image_48']
|
87
|
-
),
|
88
|
-
name:(
|
89
|
-
hash[:name] ||
|
90
|
-
user_identity['name'] ||
|
91
|
-
user_info['user'].to_h['real_name'] ||
|
92
|
-
user_profile['profile'].to_h['real_name']
|
93
|
-
),
|
94
|
-
email:(
|
95
|
-
hash[:email] ||
|
96
|
-
user_identity.to_h['email'] ||
|
97
|
-
user_info['user'].to_h['profile'].to_h['email'] ||
|
98
|
-
user_profile['profile'].to_h['email']
|
99
|
-
),
|
100
|
-
team_name:(
|
101
|
-
hash[:team_name] ||
|
102
|
-
team_identity.to_h['name'] ||
|
103
|
-
team_info['team'].to_h['name']
|
104
|
-
),
|
105
|
-
team_domain:(
|
106
|
-
auth['team'].to_h['domain'] ||
|
107
|
-
team_identity.to_h['domain'] ||
|
108
|
-
team_info['team'].to_h['domain']
|
109
|
-
),
|
110
|
-
team_image:(
|
111
|
-
auth['team'].to_h['image_44'] ||
|
112
|
-
team_identity.to_h['image_44'] ||
|
113
|
-
team_info['team'].to_h['icon'].to_h['image_44']
|
114
|
-
),
|
115
|
-
team_email_domain:(
|
116
|
-
team_info['team'].to_h['email_domain']
|
117
|
-
),
|
118
|
-
nickname:(
|
119
|
-
user_info.to_h['user'].to_h['name'] ||
|
120
|
-
auth['user'].to_h['name'] ||
|
121
|
-
user_identity.to_h['name']
|
122
|
-
),
|
123
|
-
}
|
124
|
-
|
125
|
-
hash.merge!(more_info)
|
126
|
-
end
|
127
|
-
hash
|
117
|
+
.merge access_token.to_hash
|
128
118
|
end
|
129
119
|
|
120
|
+
# Defines a section for all additional data to be
|
121
|
+
# included with the AuthHash instance.
|
122
|
+
#
|
123
|
+
# for :extra section of AuthHash.
|
124
|
+
#
|
130
125
|
extra do
|
131
126
|
{
|
132
|
-
scopes_requested:
|
133
|
-
|
134
|
-
web_hook_info: web_hook_info,
|
135
|
-
bot_info: auth['bot'] || bot_info['bot'],
|
136
|
-
auth: auth,
|
137
|
-
identity: identity,
|
138
|
-
user_info: user_info,
|
139
|
-
user_profile: user_profile,
|
140
|
-
team_info: team_info,
|
141
|
-
apps_permissions_users_list: apps_permissions_users_list,
|
142
|
-
additional_data: get_additional_data,
|
143
|
-
raw_info: @raw_info
|
127
|
+
scopes_requested: scopes_requested,
|
128
|
+
raw_info: raw_info
|
144
129
|
}
|
145
130
|
end
|
146
131
|
|
147
|
-
|
148
|
-
|
132
|
+
|
133
|
+
### Instance Methods ###
|
134
|
+
|
135
|
+
# Wraps OmniAuth::Oauth2#authorize_params so that specified params
|
136
|
+
# can be passed on to Slack authorization GET request.
|
149
137
|
# See https://github.com/omniauth/omniauth/issues/390
|
138
|
+
#
|
150
139
|
def authorize_params
|
151
|
-
super.tap do |
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
140
|
+
super.tap do |prms|
|
141
|
+
params_digest = prms.hash
|
142
|
+
debug{"Using omniauth authorize_params #{prms}"}
|
143
|
+
debug{"Considering request.params #{request.params}"}
|
144
|
+
debug{"Considering pass_through_params #{pass_through_params}"}
|
145
|
+
filtered_ptp = pass_through_params.reject{|o| o.to_s == 'team_domain'}
|
146
|
+
filtered_rp = request.params.reject{|k,v| !filtered_ptp.any?{|ptp| ptp.to_s == k.to_s}}
|
147
|
+
debug{"Filtered request params #{filtered_rp}"}
|
148
|
+
prms.merge! filtered_rp
|
149
|
+
log(:debug, "Using modified authorize_params #{prms}") if prms.hash != params_digest
|
150
|
+
session['omniauth.authorize_params'] = prms
|
158
151
|
end
|
159
152
|
end
|
160
153
|
|
161
|
-
#
|
162
|
-
# * overrides previous omniauth-strategies-oauth2 :client definition.
|
154
|
+
# Pre-sets env vars for super.
|
163
155
|
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
# * Set auth site uri with custom subdomain (if provided).
|
156
|
+
# OmniAuth callback phase to extract session var for
|
157
|
+
# omniauth.authorize_params into env (this is how omniauth does this).
|
167
158
|
#
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
if !team_domain.to_s.empty?
|
173
|
-
site_uri = URI.parse(options[:client_options]['site'])
|
174
|
-
site_uri.host = "#{team_domain}.slack.com"
|
175
|
-
new_client.site = site_uri.to_s
|
176
|
-
log(:debug, "Oauth site uri with custom team_domain #{site_uri}")
|
177
|
-
end
|
178
|
-
|
179
|
-
st_raw_info = raw_info
|
180
|
-
new_client.define_singleton_method(:request) do |*args|
|
181
|
-
OmniAuth.logger.send(:debug, "(slack) API request #{args[0..1]}; in thread #{Thread.current.object_id}.")
|
182
|
-
request_output = super(*args)
|
183
|
-
uri = args[1].to_s.gsub(/^.*\/([^\/]+)/, '\1') # use single-quote or double-back-slash for replacement.
|
184
|
-
st_raw_info[uri.to_s]= request_output
|
185
|
-
request_output
|
186
|
-
end
|
187
|
-
|
188
|
-
new_client
|
189
|
-
end
|
190
|
-
|
191
|
-
# Dropping query_string from callback_url prevents some errors in call to /api/oauth.access.
|
192
|
-
def callback_url
|
193
|
-
full_host + script_name + callback_path
|
194
|
-
end
|
195
|
-
|
196
|
-
def identity
|
197
|
-
return {} unless !skip_info? && has_scope?(identity: ['identity.basic','identity:read:user']) && is_not_excluded?
|
198
|
-
semaphore.synchronize {
|
199
|
-
@identity_raw ||= access_token.get('/api/users.identity', headers: {'X-Slack-User' => user_id})
|
200
|
-
@identity ||= @identity_raw.parsed
|
201
|
-
}
|
202
|
-
end
|
203
|
-
|
204
|
-
|
205
|
-
private
|
206
|
-
|
207
|
-
def initialize(*args)
|
159
|
+
def callback_phase #(*args)
|
160
|
+
# This technique copied from OmniAuth::Strategy,
|
161
|
+
# (this is how they do it for other omniauth objects).
|
162
|
+
env['omniauth.authorize_params'] = session.delete('omniauth.authorize_params')
|
208
163
|
super
|
209
|
-
@main_semaphore = Mutex.new
|
210
|
-
@semaphores = {}
|
211
164
|
end
|
212
165
|
|
213
|
-
#
|
214
|
-
#
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
166
|
+
# Returns OmniAuth::Slack::AuthHash
|
167
|
+
#
|
168
|
+
# Super result is converted to plain hash first,
|
169
|
+
# so AuthHash can do its recursive build magic.
|
170
|
+
#
|
171
|
+
def auth_hash
|
172
|
+
OmniAuth::Slack::AuthHash.new super.to_hash
|
219
173
|
end
|
220
174
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
175
|
+
# Uses `OmniAuth::Slack::OAuth2::Client` to handle Slack-specific features.
|
176
|
+
#
|
177
|
+
# * Logs API requests with OmniAuth.logger.
|
178
|
+
# * Allows passthrough of Slack team_domain.
|
179
|
+
# * Enables/disables Client instance history.
|
180
|
+
# * Allows use of OmniAuth::Slack::OAuth2::AccessToken.
|
181
|
+
#
|
182
|
+
# Returns instance of OmniAuth::Slack::OAuth2::Client.
|
183
|
+
#
|
184
|
+
def client
|
185
|
+
@client ||= (
|
186
|
+
team_domain = (pass_through_params.include?('team_domain') && request.params['team_domain']) ? request.params['team_domain'] : options.team_domain
|
187
|
+
new_client = OmniAuth::Slack::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options.merge({subdomain:team_domain})))
|
188
|
+
|
189
|
+
debug{"Strategy #{self} using Client #{new_client} with callback_url #{callback_url}"}
|
190
|
+
|
191
|
+
new_client
|
234
192
|
)
|
235
193
|
end
|
236
|
-
|
237
|
-
def is_not_excluded?(method_name = caller[0][/`([^']*)'/, 1])
|
238
|
-
active_methods.include?(method_name.to_s) || active_methods.include?(method_name.to_s.to_sym)
|
239
|
-
end
|
240
|
-
|
241
|
-
# Preload additional api calls with a pool of threads.
|
242
|
-
def preload_data_with_threads(num_threads)
|
243
|
-
return unless num_threads > 0
|
244
|
-
preload_methods = active_methods.concat(options[:additional_data].keys)
|
245
|
-
log :info, "Preloading (#{preload_methods.size}) data requests using (#{num_threads}) threads."
|
246
|
-
work_q = Queue.new
|
247
|
-
preload_methods.each{|x| work_q.push x }
|
248
|
-
workers = num_threads.to_i.times.map do
|
249
|
-
Thread.new do
|
250
|
-
begin
|
251
|
-
while x = work_q.pop(true)
|
252
|
-
log :debug, "Preloading #{x}."
|
253
|
-
send x
|
254
|
-
end
|
255
|
-
rescue ThreadError
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
259
|
-
workers.map(&:join); "ok"
|
260
|
-
end
|
261
|
-
|
262
|
-
# Define methods for addional data from :additional_data option
|
263
|
-
def define_additional_data
|
264
|
-
hash = options[:additional_data]
|
265
|
-
if !hash.to_h.empty?
|
266
|
-
hash.each do |k,v|
|
267
|
-
define_singleton_method(k) do
|
268
|
-
instance_variable_get(:"@#{k}") ||
|
269
|
-
instance_variable_set(:"@#{k}", v.respond_to?(:call) ? v.call(env) : v)
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def get_additional_data
|
276
|
-
if skip_info?
|
277
|
-
{}
|
278
|
-
else
|
279
|
-
options[:additional_data].inject({}) do |hash,tupple|
|
280
|
-
hash[tupple[0].to_s] = send(tupple[0].to_s)
|
281
|
-
hash
|
282
|
-
end
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
# Parsed data returned from /slack/oauth.access api call.
|
287
|
-
def auth
|
288
|
-
@auth ||= access_token.params.to_h.merge({'token' => access_token.token})
|
289
|
-
end
|
290
194
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
@team_identity ||= identity['team'].to_h
|
297
|
-
end
|
298
|
-
|
299
|
-
def user_info
|
300
|
-
return {} unless !skip_info? && has_scope?(identity: 'users:read', team: 'users:read') && is_not_excluded?
|
301
|
-
semaphore.synchronize {
|
302
|
-
@user_info_raw ||= access_token.get('/api/users.info', params: {user: user_id}, headers: {'X-Slack-User' => user_id})
|
303
|
-
@user_info ||= @user_info_raw.parsed
|
304
|
-
}
|
195
|
+
# Dropping query_string from the default OmniAuth callback_url prevents
|
196
|
+
# some errors in call to /api/oauth.[v2.]access.
|
197
|
+
#
|
198
|
+
def callback_url
|
199
|
+
options.redirect_uri || full_host + script_name + callback_path
|
305
200
|
end
|
306
201
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
202
|
+
### Possibly obsolete
|
203
|
+
#
|
204
|
+
# def user_id
|
205
|
+
# # access_token['user_id'] || access_token['user'].to_h['id'] || access_token['authorizing_user'].to_h['user_id']
|
206
|
+
# access_or_user_token&.user_id
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
# def team_id
|
210
|
+
# access_token&.team_id
|
211
|
+
# end
|
212
|
+
|
213
|
+
# Gets and decodes :pass_through_params option.
|
214
|
+
#
|
215
|
+
def pass_through_params
|
216
|
+
ptp = [options.pass_through_params].flatten.compact
|
217
|
+
case
|
218
|
+
when ptp[0].to_s == 'all'
|
219
|
+
options.pass_through_params = AUTH_OPTIONS
|
220
|
+
when ptp[0].to_s == 'none'
|
221
|
+
[]
|
222
|
+
else
|
223
|
+
ptp
|
224
|
+
end
|
313
225
|
end
|
314
226
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
227
|
+
# Parsed data returned from /slack/oauth.[v2.]access api call.
|
228
|
+
#
|
229
|
+
# Where does this actually go? Where is it used?
|
230
|
+
#
|
231
|
+
# Simplifying this to just 'access_token.to_hash' does not appear to
|
232
|
+
# have any noticeable negative effect.
|
233
|
+
#
|
234
|
+
# Possibly obsolete
|
235
|
+
#
|
236
|
+
# def auth
|
237
|
+
# @auth ||= access_token.to_hash
|
238
|
+
# end
|
322
239
|
|
323
|
-
|
324
|
-
return {} unless auth.key? 'incoming_webhook'
|
325
|
-
auth['incoming_webhook']
|
326
|
-
end
|
327
|
-
|
328
|
-
def bot_info
|
329
|
-
return {} unless !skip_info? && has_scope?(identity: 'users:read') && is_not_excluded?
|
330
|
-
semaphore.synchronize {
|
331
|
-
@bot_info_raw ||= access_token.get('/api/bots.info')
|
332
|
-
@bot_info ||= @bot_info_raw.parsed
|
333
|
-
}
|
334
|
-
end
|
335
|
-
|
336
|
-
def user_id
|
337
|
-
auth['user_id'] || auth['user'].to_h['id'] || auth['authorizing_user'].to_h['user_id']
|
338
|
-
end
|
339
|
-
|
340
|
-
def team_id
|
341
|
-
auth['team_id'] || auth['team'].to_h['id']
|
342
|
-
end
|
343
|
-
|
344
|
-
# API call to get user permissions for workspace token.
|
345
|
-
# This is needed because workspace token 'sign-in-with-slack' is missing scopes
|
346
|
-
# in the :scope field (acknowledged issue in developer preview).
|
240
|
+
# Points to client @history, which is filled with API response objects.
|
347
241
|
#
|
348
|
-
# Returns [<id>: <resource>]
|
349
|
-
def apps_permissions_users_list
|
350
|
-
return {} unless !skip_info? && is_app_token? && is_not_excluded?
|
351
|
-
semaphore.synchronize {
|
352
|
-
@apps_permissions_users_list_raw ||= access_token.get('/api/apps.permissions.users.list')
|
353
|
-
@apps_permissions_users_list ||= @apps_permissions_users_list_raw.parsed['resources'].inject({}){|h,i| h[i['id']] = i; h}
|
354
|
-
}
|
355
|
-
end
|
356
|
-
|
357
242
|
def raw_info
|
358
|
-
@raw_info ||=
|
359
|
-
|
360
|
-
|
361
|
-
# Is this a workspace app token?
|
362
|
-
def is_app_token?
|
363
|
-
auth['token_type'].to_s == 'app'
|
243
|
+
@raw_info ||= access_token.client.history
|
244
|
+
debug{"Retrieved raw_info (size #{@raw_info.size}) (object_id #{@raw_info.object_id})"}
|
245
|
+
@raw_info
|
364
246
|
end
|
365
247
|
|
366
|
-
#
|
367
|
-
# * The classic :scope field (string)
|
368
|
-
# * New workshop token :scopes field (hash)
|
369
|
-
# * Separate call to apps.permissions.users.list (array)
|
248
|
+
# Gets 'authed_user' sub-token from main access token.
|
370
249
|
#
|
371
|
-
|
372
|
-
|
373
|
-
def all_scopes
|
374
|
-
@all_scopes ||=
|
375
|
-
{'identity' => (auth['scope'] || apps_permissions_users_list[user_id].to_h['scopes'].to_a.join(',')).to_s.split(',')}
|
376
|
-
.merge(auth['scopes'].to_h)
|
250
|
+
def user_token
|
251
|
+
access_token&.user_token
|
377
252
|
end
|
378
253
|
|
379
|
-
#
|
380
|
-
#
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
else raise "Scope must be a string or array"
|
389
|
-
end
|
390
|
-
test_scopes.detect do |scope|
|
391
|
-
all_scopes[section.to_s].to_a.include?(scope.to_s)
|
392
|
-
end
|
254
|
+
# Gets main access_token, if valid, otherwise gets user_token, if valid.
|
255
|
+
# Handles Slack v1 and v2 API (v2 is non-conformant with OAUTH2 spec).
|
256
|
+
def access_or_user_token
|
257
|
+
if access_token&.token
|
258
|
+
access_token
|
259
|
+
elsif user_token
|
260
|
+
user_token
|
261
|
+
else
|
262
|
+
access_token
|
393
263
|
end
|
394
264
|
end
|
395
265
|
|
396
|
-
def
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
)
|
266
|
+
def scopes_requested
|
267
|
+
# omniauth.authorize_params is an enhancement to omniauth functionality for omniauth-slack.
|
268
|
+
out = {
|
269
|
+
scope: env['omniauth.authorize_params'].to_h['scope'],
|
270
|
+
user_scope: env['omniauth.authorize_params'].to_h['user_scope']
|
271
|
+
}
|
272
|
+
|
273
|
+
debug{"scopes_requested: #{out}"}
|
274
|
+
return out
|
406
275
|
end
|
407
|
-
|
408
|
-
end
|
409
|
-
end
|
410
|
-
end
|
276
|
+
|
277
|
+
end # Slack
|
278
|
+
end # Strategies
|
279
|
+
end # OmniAuth
|
411
280
|
|