-
Notifications
You must be signed in to change notification settings - Fork 5
/
OAuth.coffee
193 lines (180 loc) · 9.35 KB
/
OAuth.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# General approach to interact with OAuth 1.0a & 2.0
#
# This is meant to be an example how to use scribe with different services. Generalizing OAuth dance
# it can greatly simplify authorization routines on node.js/coffeescript apps, but one could also use it as
# a starting point to own handling.
#
# - class / widget supports certain api on hood, but with optional services parameter any web service / API can be used.
# on that case provider class must extend scribe DefaultApi10a or DefaultApi20 to work right way
# - default services can be configured with environment variables
# - storage could be any form of storage, session, redis db and so forth that just has "on set" and "on get" event
# handlers. in case of hubot you can pass robot.brain.data to store oauth tokens
# - public methods by OAuth class are:
# 1. get_authorization_url which takes callback function to return url / false
# 2. set_verification_code which takes code and callback function to return true/false
# 3. set_access_token_code which is used by OAuth 2.0 schemes only which takes code and return true/false
# 4. refresh_access_token which is used by OAuth 2.0 schemes only and which takes access_token and
# callback function to return true/false
# 5. get_request_token, get_access_token and get_verifier methods to retrieve tokens as per method name
#
# TODO:
# - signatureType on default service configurations is not used yet, but its possible and maybe
# required on some services because default will be "Header" type
# - its unclear how expired_in should be used
# - at the moment callbacks return only true/false or authorization url. on some cases more information
# could be served so returned object could be associated array of values. but so far I try to keep it silly simple
root = exports ? this
# require main library and apis
scribe = require('../scribe').load(['GoogleApi', 'GoogleApi2', 'TwitterApi', 'FacebookApi', 'LinkedInApi'])
# hubot enviroment variables
env = process.env
# set up services. this list should be updated when new widgets comes in and has been tested
default_services = {}
default_services['analytics'] = {'provider': scribe.GoogleApi, 'key': env.GOOGLE_OAUTH_API_KEY, 'secret': env.GOOGLE_OAUTH_API_SECRET, 'scope': 'https://www.google.com/analytics/feeds/', 'callback': 'oob'}
default_services['analytics2'] = {'provider': scribe.GoogleApi2, 'key': env.GOOGLE_OAUTH2_API_KEY, 'secret': env.GOOGLE_OAUTH2_API_SECRET, 'scope': 'https://www.googleapis.com/auth/analytics.readonly', 'callback': 'urn:ietf:wg:oauth:2.0:oob'}
default_services['twitter'] = {'provider': scribe.TwitterApi, 'key': env.TWITTER_OAUTH_API_KEY, 'secret': env.TWITTER_OAUTH_API_SECRET, 'scope': '', 'callback': 'oob'}
default_services['facebook'] = {'provider': scribe.FacebookApi, 'key': env.FACEBOOK_OAUTH_API_KEY, 'secret': env.FACEBOOK_OAUTH_API_SECRET, 'scope': 'email,read_stream,read_insights', 'callback': 'https://www.facebook.com/connect/login_success.html'}
default_services['linkedin'] = {'provider': scribe.LinkedInApi, 'key': env.LINKEDIN_OAUTH_API_KEY, 'secret': env.LINKEDIN_OAUTH_API_SECRET, 'scope': '', 'callback': 'oob'}
class root.OAuth
# TODO: its possible to provide own services here, but then those service providers should be implemented by own classes
# which is of course possible and suitable, so lets keep this possibility on hood
constructor: (@storage, @api, @services = null) ->
if @services and @services[@api]
@config = @services[@api]
else
@config = default_services[@api]
@service = null
create_service: () ->
if @config
if not @service
@service = new scribe.ServiceBuilder()
.provider(@config['provider'])
.apiKey(@config['key'])
.apiSecret(@config['secret'])
._callback(@config['callback'])
._scope(@config['scope'])
.build()
return @service
else
console.log "Service / API not found: " + @api
return false
# private method
_init_storage: () ->
if not @storage.oauth
@storage.oauth = {}
@storage.oauth[@api] = {}
else if not @storage.oauth[@api]
@storage.oauth[@api] = {}
return @storage.oauth[@api]
get_authorization_url: (callback) ->
if service = @create_service()
storage = @_init_storage()
# OAuth v2.0 is this much simpler on retrieving url
if service.getVersion() == "2.0"
callback service.getAuthorizationUrl()
else
request_token_extract = (response) ->
#console.log 'Response: ' + response.data
token = service.api.getRequestTokenExtractor() response.data
storage['request_token'] = token.getToken()
storage['request_secret'] = token.getSecret()
console.log "Request token set: " + storage['request_token']
callback service.getAuthorizationUrl token
service.getRequestToken request_token_extract
else callback false
remove_authorization: () ->
if not service = @create_service()
return false
storage = @_init_storage()
if service.getVersion() == "2.0"
delete storage['expires_in']
delete storage['token_type']
delete storage['refresh_token']
else
delete storage['request_token']
delete storage['request_secret']
delete storage['code']
delete storage['access_token']
delete storage['access_secret']
return true
get_request_token: () ->
new scribe.Token @storage.oauth[@api]['request_token'], @storage.oauth[@api]['request_secret']
get_verifier: () ->
new scribe.Verifier @storage.oauth[@api]['code']
# special case for facebook desktop apps for example in that case you have access token instead of verification code
# and you will set it directly to the scribe. note because its OAuth2.0 protocol, no need for access secret at all
# TODO: should prevent accidental usage?
set_access_token_code: (code) ->
if service = @create_service()
storage = @_init_storage()
storage['access_token'] = code
console.log 'Access token set: ' + storage['access_token']
return true
return false
# private method. is called after set verification code
_set_access_token: (service, callback) ->
# this (@) references cant be used inside callback functions, but variables (local) can be used
storage = @_init_storage()
access_token_extract = (response) ->
#console.log 'Response: ' + response.data
token = service.api.getAccessTokenExtractor() response.data
if storage['access_token'] = token.getToken()
console.log 'Access token set: ' + storage['access_token']
# access secret is not really needed on OAuth 2.0 scheme. expires, type and refresh tokens are instead
if service.getVersion() == "2.0"
storage['expires_in'] = token.getExpires()
storage['token_type'] = token.getType()
storage['refresh_token'] = token.getRefresh()
else
storage['access_secret'] = token.getSecret()
callback true
else
console.log "Access token could not be set. Please try to refresh or get a new authorization by url."
callback false
# request token is not used on OAuth 2.0
if service.getVersion() == "2.0"
service.getAccessToken @get_verifier(), access_token_extract
else
service.getAccessToken @get_request_token(), @get_verifier(), access_token_extract
set_verification_code: (code, callback) ->
storage = @_init_storage()
if service = @create_service()
if service.getVersion() == "1.0" and not storage['request_token']
console.log "Please get authorization url and request token first"
else if code
storage['code'] = code
console.log 'Verification code set: ' + storage['code']
@_set_access_token service, callback
else
console.log "Verification code not found"
else callback false
get_access_token: () ->
storage = @_init_storage()
if storage['access_token'] and service = @create_service()
# OAuth v2.0 has some more fields on token
if service.getVersion() == "2.0"
return new scribe.Token storage['access_token'], "", "", storage['expires_in'], storage['token_type'], storage['refresh_token']
else
return new scribe.Token storage['access_token'], storage['access_secret']
else
console.log "Access token not set for api: " + @api + ". Please set verification code or get authorization url."
return false
# for OAuth v2.0 only
refresh_access_token: (access_token, callback) ->
if access_token and service = @create_service()
if service.getVersion() == "2.0"
storage = @_init_storage()
refresh_token_extract = (response) ->
#console.log 'Response: ' + response.data
refresh_token = service.api.getAccessTokenExtractor() response.data
access_token.updateToken refresh_token
storage['access_token'] = access_token.getToken()
# TODO: How expired_in should be used?
# time + expires_in -> look up value. if current is more than stored, then refresh?
storage['expires_in'] = access_token.getExpires()
console.log "Refreshed token: " + storage['access_token']
callback true
service.getRefreshToken access_token, refresh_token_extract
else
console.log "Only OAuth 2.0 tokens can be refreshed."
else callback false