omniauth-b2c 0.1.3 → 0.1.4
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e650ab8a6baad15824677ea30e711a9b91065495
|
4
|
+
data.tar.gz: 12417baca22120dc6db605d9491016f5c6f34d44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b3de2171c58083d0a94f052d521c718c1ead27ae1deb009fb19828faf90688f00328ee1548a649352e56d986aba6ba6f22676685aa5736e07f761be721905e9
|
7
|
+
data.tar.gz: 5900271d4067f45424745d5bee9df57d6611c51173ccb5cc58e550f2bca1d057f4719428b63925638e9fa9559a3f7ccb323b8d6c33510352e0f71414300aae87
|
data/.idea/vcs.xml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'omniauth/b2c'
|
data/lib/omniauth/b2c/version.rb
CHANGED
@@ -0,0 +1,370 @@
|
|
1
|
+
#-------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2015 Micorosft Corporation
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
#-------------------------------------------------------------------------------
|
22
|
+
|
23
|
+
require 'jwt'
|
24
|
+
require 'omniauth'
|
25
|
+
require 'openssl'
|
26
|
+
require 'securerandom'
|
27
|
+
|
28
|
+
module OmniAuth
|
29
|
+
module Strategies
|
30
|
+
# A strategy for authentication against Azure Active Directory.
|
31
|
+
class AzureActiveDirectoryB2C
|
32
|
+
include OmniAuth::AzureActiveDirectory
|
33
|
+
include OmniAuth::Strategy
|
34
|
+
|
35
|
+
class OAuthError < StandardError; end
|
36
|
+
|
37
|
+
##
|
38
|
+
# The client id (key) and tenant must be configured when the OmniAuth
|
39
|
+
# middleware is installed. Example:
|
40
|
+
#
|
41
|
+
# require 'omniauth'
|
42
|
+
# require 'omniauth-azure-activedirectory'
|
43
|
+
#
|
44
|
+
# use OmniAuth::Builder do
|
45
|
+
# provider :azure_activedirectory, ENV['AAD_KEY'], ENV['AAD_TENANT']
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
args [:client_id, :tenant, :policy, :scope]
|
49
|
+
option :client_id, nil
|
50
|
+
option :tenant, nil
|
51
|
+
option :policy, nil
|
52
|
+
option :scope, nil
|
53
|
+
|
54
|
+
# Field renaming is an attempt to fit the OmniAuth recommended schema as
|
55
|
+
# best as possible.
|
56
|
+
#
|
57
|
+
# @see https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
|
58
|
+
uid { @claims['sub'] }
|
59
|
+
info do
|
60
|
+
{ name: @claims['name'],
|
61
|
+
email: @claims['email'] || @claims['upn'],
|
62
|
+
first_name: @claims['given_name'],
|
63
|
+
last_name: @claims['family_name'] }
|
64
|
+
end
|
65
|
+
credentials { { token: @id_token } }
|
66
|
+
|
67
|
+
#uid {
|
68
|
+
#
|
69
|
+
# (JWT.decode(request.params['id_token'], nil, false).first)['sub']
|
70
|
+
#}
|
71
|
+
#
|
72
|
+
#info do
|
73
|
+
# {
|
74
|
+
# #name: raw_info['name'],
|
75
|
+
# #nickname: raw_info['unique_name'],
|
76
|
+
# #first_name: raw_info['given_name'],
|
77
|
+
# #last_name: raw_info['family_name'],
|
78
|
+
# #email: raw_info['email'] || raw_info['upn'],
|
79
|
+
# #oid: raw_info['oid'],
|
80
|
+
# #tid: raw_info['tid']
|
81
|
+
# }
|
82
|
+
#end
|
83
|
+
DEFAULT_RESPONSE_TYPE = 'code id_token'
|
84
|
+
DEFAULT_RESPONSE_MODE = 'form_post'
|
85
|
+
|
86
|
+
##
|
87
|
+
# Overridden method from OmniAuth::Strategy. This is the first step in the
|
88
|
+
# authentication process.
|
89
|
+
def request_phase
|
90
|
+
redirect authorize_endpoint_url
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Overridden method from OmniAuth::Strategy. This is the second step in
|
95
|
+
# the authentication process. It is called after the user enters
|
96
|
+
# credentials at the authorization endpoint.
|
97
|
+
def callback_phase
|
98
|
+
#raise request.params.inspect
|
99
|
+
error = request.params['error_reason'] || request.params['error']
|
100
|
+
fail(OAuthError, error) if error
|
101
|
+
@id_token = request.params['id_token']
|
102
|
+
@code = request.params['code']
|
103
|
+
@claims, @header = validate_and_parse_id_token(@id_token)
|
104
|
+
validate_chash(@code, @claims, @header)
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
##
|
111
|
+
# Constructs a one-time-use authorize_endpoint. This method will use
|
112
|
+
# a new nonce on each invocation.
|
113
|
+
#
|
114
|
+
# @return String
|
115
|
+
def authorize_endpoint_url
|
116
|
+
uri = URI(openid_config['authorization_endpoint'])
|
117
|
+
uri.query = URI.encode_www_form(client_id: client_id,
|
118
|
+
redirect_uri: callback_url,
|
119
|
+
response_mode: response_mode,
|
120
|
+
response_type: response_type,
|
121
|
+
nonce: new_nonce,
|
122
|
+
p: policy,
|
123
|
+
scope: scope)
|
124
|
+
uri.to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# The client id of the calling application. This must be configured where
|
129
|
+
# AzureAD is installed as an OmniAuth strategy.
|
130
|
+
#
|
131
|
+
# @return String
|
132
|
+
def client_id
|
133
|
+
return options.client_id if options.client_id
|
134
|
+
fail StandardError, 'No client_id specified in AzureAD configuration.'
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# The expected id token issuer taken from the discovery endpoint.
|
139
|
+
#
|
140
|
+
# @return String
|
141
|
+
def issuer
|
142
|
+
openid_config['issuer']
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Fetches the current signing keys for Azure AD. Note that there should
|
147
|
+
# always two available, and that they have a 6 week rollover.
|
148
|
+
#
|
149
|
+
# Each key is a hash with the following fields:
|
150
|
+
# kty, use, kid, x5t, n, e, x5c
|
151
|
+
#
|
152
|
+
# @return Array[Hash]
|
153
|
+
def fetch_signing_keys
|
154
|
+
response = JSON.parse(Net::HTTP.get(URI(signing_keys_url)))
|
155
|
+
response['keys']
|
156
|
+
rescue JSON::ParserError
|
157
|
+
raise StandardError, 'Unable to fetch AzureAD signing keys.'
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Fetches the OpenId Connect configuration for the AzureAD tenant. This
|
162
|
+
# contains several import values, including:
|
163
|
+
#
|
164
|
+
# authorization_endpoint
|
165
|
+
# token_endpoint
|
166
|
+
# token_endpoint_auth_methods_supported
|
167
|
+
# jwks_uri
|
168
|
+
# response_types_supported
|
169
|
+
# response_modes_supported
|
170
|
+
# subject_types_supported
|
171
|
+
# id_token_signing_alg_values_supported
|
172
|
+
# scopes_supported
|
173
|
+
# issuer
|
174
|
+
# claims_supported
|
175
|
+
# microsoft_multi_refresh_token
|
176
|
+
# check_session_iframe
|
177
|
+
# end_session_endpoint
|
178
|
+
# userinfo_endpoint
|
179
|
+
#
|
180
|
+
# @return Hash
|
181
|
+
def fetch_openid_config
|
182
|
+
JSON.parse(Net::HTTP.get(URI(openid_config_url)))
|
183
|
+
rescue JSON::ParserError
|
184
|
+
raise StandardError, 'Unable to fetch OpenId configuration for ' \
|
185
|
+
'AzureAD tenant.'
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# Generates a new nonce for one time use. Stores it in the session so
|
190
|
+
# multiple users don't share nonces. All nonces should be generated by
|
191
|
+
# this method.
|
192
|
+
#
|
193
|
+
# @return String
|
194
|
+
def new_nonce
|
195
|
+
session['omniauth-azure-activedirectoryb2c.nonce'] = SecureRandom.uuid
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# A memoized version of #fetch_openid_config.
|
200
|
+
#
|
201
|
+
# @return Hash
|
202
|
+
def openid_config
|
203
|
+
@openid_config ||= fetch_openid_config
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# The location of the OpenID configuration for the tenant.
|
208
|
+
#
|
209
|
+
# @return String
|
210
|
+
def openid_config_url
|
211
|
+
"https://login.microsoftonline.com/#{tenant}/v2.0/.well-known/openid-configuration"
|
212
|
+
end
|
213
|
+
|
214
|
+
##
|
215
|
+
# Returns the most recent nonce for the session and deletes it from the
|
216
|
+
# session.
|
217
|
+
#
|
218
|
+
# @return String
|
219
|
+
def read_nonce
|
220
|
+
session.delete('omniauth-azure-activedirectory.nonce')
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# The response_type that will be set in the authorization request query
|
225
|
+
# parameters. Can be overridden by the client, but it shouldn't need to
|
226
|
+
# be.
|
227
|
+
#
|
228
|
+
# @return String
|
229
|
+
def response_type
|
230
|
+
options[:response_type] || DEFAULT_RESPONSE_TYPE
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# The response_mode that will be set in the authorization request query
|
235
|
+
# parameters. Can be overridden by the client, but it shouldn't need to
|
236
|
+
# be.
|
237
|
+
#
|
238
|
+
# @return String
|
239
|
+
def response_mode
|
240
|
+
options[:response_mode] || DEFAULT_RESPONSE_MODE
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# The keys used to sign the id token JWTs. This is just a memoized version
|
245
|
+
# of #fetch_signing_keys.
|
246
|
+
#
|
247
|
+
# @return Array[Hash]
|
248
|
+
def signing_keys
|
249
|
+
@signing_keys ||= fetch_signing_keys
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# The location of the public keys of the token signer. This is parsed from
|
254
|
+
# the OpenId config response.
|
255
|
+
#
|
256
|
+
# @return String
|
257
|
+
def signing_keys_url
|
258
|
+
return openid_config['jwks_uri'] if openid_config.include? 'jwks_uri'
|
259
|
+
fail StandardError, 'No jwks_uri in OpenId config response.'
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# The tenant of the calling application. Note that this must be
|
264
|
+
# explicitly configured when installing the AzureAD OmniAuth strategy.
|
265
|
+
#
|
266
|
+
# @return String
|
267
|
+
def tenant
|
268
|
+
return options.tenant if options.tenant
|
269
|
+
fail StandardError, 'No tenant specified in AzureAD configuration.'
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
##
|
274
|
+
# The policy of the calling application. Note that this must be
|
275
|
+
# explicitly configured when installing the AzureAD OmniAuth strategy.
|
276
|
+
#
|
277
|
+
# @return String
|
278
|
+
def policy
|
279
|
+
return options.policy if options.policy
|
280
|
+
fail StandardError, 'No policy specified in AzureAD configuration.'
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
##
|
285
|
+
# The policy of the calling application. Note that this must be
|
286
|
+
# explicitly configured when installing the AzureAD OmniAuth strategy.
|
287
|
+
#
|
288
|
+
# @return String
|
289
|
+
def scope
|
290
|
+
return options.scope if options.scope
|
291
|
+
fail StandardError, 'No scope specified in AzureAD configuration.'
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
|
296
|
+
##
|
297
|
+
# Verifies the signature of the id token as well as the exp, nbf, iat,
|
298
|
+
# iss, and aud fields.
|
299
|
+
#
|
300
|
+
# See OpenId Connect Core 3.1.3.7 and 3.2.2.11.
|
301
|
+
#
|
302
|
+
# @return Claims, Header
|
303
|
+
def validate_and_parse_id_token(id_token)
|
304
|
+
# The second parameter is the public key to verify the signature.
|
305
|
+
# However, that key is overridden by the value of the executed block
|
306
|
+
# if one is present.
|
307
|
+
#
|
308
|
+
# If you're thinking that this looks ugly with the raw nil and boolean,
|
309
|
+
# see https://github.com/jwt/ruby-jwt/issues/59.
|
310
|
+
jwt_claims, jwt_header = JWT.decode(id_token, nil, false) #do |header|
|
311
|
+
# There should always be one key from the discovery endpoint that
|
312
|
+
# matches the id in the JWT header.
|
313
|
+
# x5c = (signing_keys.find do |key|
|
314
|
+
# key['kid'] == header['kid']
|
315
|
+
# end || {})['x5c']
|
316
|
+
# if x5c.nil? || x5c.empty?
|
317
|
+
# fail JWT::VerificationError,
|
318
|
+
# 'No keys from key endpoint match the id token'
|
319
|
+
# end
|
320
|
+
# # The key also contains other fields, such as n and e, that are
|
321
|
+
# # redundant. x5c is sufficient to verify the id token.
|
322
|
+
# OpenSSL::X509::Certificate.new(JWT.base64url_decode(x5c.first)).public_key
|
323
|
+
#end
|
324
|
+
return jwt_claims, jwt_header
|
325
|
+
end
|
326
|
+
|
327
|
+
##
|
328
|
+
# Verifies that the c_hash the id token claims matches the authorization
|
329
|
+
# code. See OpenId Connect Core 3.3.2.11.
|
330
|
+
#
|
331
|
+
# @param String code
|
332
|
+
# @param Hash claims
|
333
|
+
# @param Hash header
|
334
|
+
def validate_chash(code, claims, header)
|
335
|
+
# This maps RS256 -> sha256, ES384 -> sha384, etc.
|
336
|
+
algorithm = (header['alg'] || 'RS256').sub(/RS|ES|HS/, 'sha')
|
337
|
+
full_hash = OpenSSL::Digest.new(algorithm).digest code
|
338
|
+
c_hash = JWT.base64url_encode full_hash[0..full_hash.length / 2 - 1]
|
339
|
+
return if c_hash == claims['c_hash']
|
340
|
+
fail JWT::VerificationError,
|
341
|
+
'c_hash in id token does not match auth code.'
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
#def raw_info
|
346
|
+
# # it's all here in JWT http://msdn.microsoft.com/en-us/library/azure/dn195587.aspx
|
347
|
+
# @raw_info ||= ::JWT.decode(request.params['id_token'], nil, false).first
|
348
|
+
#end
|
349
|
+
|
350
|
+
##
|
351
|
+
# The options passed to the Ruby JWT library to verify the id token.
|
352
|
+
# Note that these are not all the checks we perform. Some (like nonce)
|
353
|
+
# are not handled by the JWT API and are checked manually in
|
354
|
+
# #validate_and_parse_id_token.
|
355
|
+
#
|
356
|
+
# @return Hash
|
357
|
+
def verify_options
|
358
|
+
{ verify_expiration: true,
|
359
|
+
verify_not_before: true,
|
360
|
+
verify_iat: true,
|
361
|
+
verify_iss: true,
|
362
|
+
'iss' => issuer,
|
363
|
+
verify_aud: true,
|
364
|
+
'aud' => client_id }
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
OmniAuth.config.add_camelization 'azure_activedirectoryb2c', 'AzureActiveDirectoryB2C'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: omniauth-b2c
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Raj Ragula (Schakra Inc)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -116,14 +116,17 @@ extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
118
|
- ".gitignore"
|
119
|
+
- ".idea/vcs.xml"
|
119
120
|
- Gemfile
|
120
121
|
- LICENSE.txt
|
121
122
|
- README.md
|
122
123
|
- Rakefile
|
123
124
|
- bin/console
|
124
125
|
- bin/setup
|
126
|
+
- lib/omniauth-azure-activedirectoryb2c.rb
|
125
127
|
- lib/omniauth/b2c.rb
|
126
128
|
- lib/omniauth/b2c/version.rb
|
129
|
+
- lib/omniauth/strategies/azure_activedirectoryb2c.rb
|
127
130
|
- omniauth-b2c.gemspec
|
128
131
|
homepage: https://github.com/rajanikanthr/omniauth-adb2c
|
129
132
|
licenses:
|