omniauth-activedirectory 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +8 -0
- data/.rubocop_todo.yml +20 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/Rakefile +22 -0
- data/lib/omniauth/activedirectory/version.rb +6 -0
- data/lib/omniauth/activedirectory.rb +2 -0
- data/lib/omniauth/strategies/activedirectory.rb +307 -0
- data/lib/omniauth-activedirectory.rb +1 -0
- data/omniauth-activedirectory.gemspec +29 -0
- data/spec/fixtures/id_token.txt +1 -0
- data/spec/fixtures/id_token_bad_audience.txt +1 -0
- data/spec/fixtures/id_token_bad_chash.txt +1 -0
- data/spec/fixtures/id_token_bad_issuer.txt +1 -0
- data/spec/fixtures/id_token_bad_kid.txt +1 -0
- data/spec/fixtures/id_token_bad_nonce.txt +1 -0
- data/spec/fixtures/id_token_no_alg.txt +1 -0
- data/spec/fixtures/x5c.txt +1 -0
- data/spec/fixtures/x5c_different.txt +1 -0
- data/spec/omniauth/strategies/activedirectory_spec.rb +200 -0
- data/spec/spec_helper.rb +44 -0
- metadata +192 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9e5054c1ce0e8771f7955b5cab69b08b4ecb1287c6add06c6c3557b557df161b
|
4
|
+
data.tar.gz: b46b578255169b89254233a5351092d767c7a18e2d923d831f3f9d0c146c45ca
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3c9954a04ee129aa7f5f4a7cb6a32cc487b75e44bd50abde7e3864a56fe9cba9479b3aca3708505f7a73711e23c7392cd7b87ef02bffbd588f92afbe0b53ca0
|
7
|
+
data.tar.gz: 3b40be7f0410ff9f79a303aa2614e12314973d5efe2fb90ce116905b1d03a3003b6eddb0cb8a24670cf579e3d9f6823919c577de8a4cb6c4991d0397d3ec098e
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# This configuration was generated by `rubocop --auto-gen-config`
|
2
|
+
# on 2015-08-06 14:09:24 -0700 using RuboCop version 0.32.1.
|
3
|
+
# The point is for the user to remove these configuration records
|
4
|
+
# one by one as the offenses are removed from the code base.
|
5
|
+
# Note that changes in the inspected code, or installation of new
|
6
|
+
# versions of RuboCop, may require this file to be generated again.
|
7
|
+
|
8
|
+
# Offense count: 2
|
9
|
+
Metrics/AbcSize:
|
10
|
+
Max: 19
|
11
|
+
|
12
|
+
# Offense count: 1
|
13
|
+
# Configuration parameters: CountComments.
|
14
|
+
Metrics/ClassLength:
|
15
|
+
Max: 118
|
16
|
+
|
17
|
+
# Offense count: 1
|
18
|
+
# Configuration parameters: Exclude.
|
19
|
+
Style/FileName:
|
20
|
+
Enabled: false
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Dave Van Fleet
|
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,94 @@
|
|
1
|
+
# OmniAuth Azure Active Directory
|
2
|
+
|
3
|
+
This is a fork of Microsoft's official ruby gem for Azure Active Directory as an omniauth provider. Most of this README is taken from Microsoft's original gem, [found here](https://github.com/AzureAD/omniauth-azure-activedirectory), with additions to make it compatible with other providers.
|
4
|
+
|
5
|
+
OmniAuth strategy to authenticate to Azure Active Directory via OpenId Connect.
|
6
|
+
|
7
|
+
Before starting, set up a tenant and register a Web Application at [https://manage.windowsazure.com](https://manage.windowsazure.com). Note your client id and tenant for later.
|
8
|
+
|
9
|
+
# Why this Gem?
|
10
|
+
|
11
|
+
While Microsoft's original gem still works, when Azure AD is being used as the only provider for omniauth, it hasn't been updated in a significant amount of time. When trying to implement this gem in my own projects, I faced the issue of having many runtime dependency conflicts, meaning I either needed to use older versions of other provider gems (not always possible), or update this gem for myself. This repo is the result of deciding on the latter.
|
12
|
+
|
13
|
+
## Samples and Documentation
|
14
|
+
|
15
|
+
[Find Microsoft's examples here](https://github.com/AzureADSamples) to help you get started with learning the Azure Identity system. This includes tutorials for native clients such as Windows, Windows Phone, iOS, OSX, Android, and Linux. There you can also find full walkthroughs for authentication flows such as OAuth2, OpenID Connect, Graph API, and other features provided by Microsoft.
|
16
|
+
|
17
|
+
## How to use this SDK
|
18
|
+
|
19
|
+
#### Installation
|
20
|
+
|
21
|
+
Add to your Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'omniauth-activedirectory'
|
25
|
+
```
|
26
|
+
|
27
|
+
### Usage
|
28
|
+
|
29
|
+
If you are already using OmniAuth, adding AzureAD is as simple as adding a new provider to your `OmniAuth::Builder`. The provider requires your AzureAD client id and your AzureAD tenant.
|
30
|
+
|
31
|
+
For example, in Rails you would add this in `config/initializers/omniauth.rb`:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
35
|
+
provider :activedirectory, ENV['AAD_CLIENT_ID'], ENV['AAD_TENANT']
|
36
|
+
# other providers here
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
When you want to authenticate the user, simply redirect them to `/auth/activedirectory`. From there, OmniAuth will takeover. Once the user authenticates (or fails to authenticate), they will be redirected to `/auth/activedirectory/callback`. The authentication result is available in `request.env['omniauth.auth']`.
|
41
|
+
|
42
|
+
#### CSRF Concerns
|
43
|
+
|
44
|
+
Rails' built-in CSRF protection can cause some hassles with omniauth, since a `POST` request is being made to your application by the provider, without an anti forgery token from you application. We need to tells Rails not to perform this check on the specific action being used for authentication with omniauth. Typically, your callback will be routed to the `#create` action in the `Sessions` controller. If this is the case for your app, app the following line in the beggining of the sessions controller:
|
45
|
+
```ruby
|
46
|
+
skip_before_action :verify_authenticity_token, only: :create
|
47
|
+
```
|
48
|
+
Make sure that nothing sensitive is happening in this action that could expose your app to CSRF attacks. It should pretty much just get the data in the `POST` request and use it to create a new session.
|
49
|
+
|
50
|
+
|
51
|
+
### Auth Hash
|
52
|
+
|
53
|
+
OmniAuth AzureAD tries to be consistent with the auth hash schema recommended by OmniAuth. [https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema).
|
54
|
+
|
55
|
+
Here's an example of an authentication hash available in the callback. You can access this hash as `request.env['omniauth.auth']`.
|
56
|
+
|
57
|
+
```
|
58
|
+
:provider => "azureactivedirectory",
|
59
|
+
:uid => "123456abcdef",
|
60
|
+
:info => {
|
61
|
+
:name => "John Smith",
|
62
|
+
:email => "jsmith@contoso.net",
|
63
|
+
:first_name => "John",
|
64
|
+
:last_name => "Smith"
|
65
|
+
},
|
66
|
+
:credentials => {
|
67
|
+
:code => "ffdsjap9fdjw893-rt2wj8r9r32jnkdsflaofdsa9"
|
68
|
+
},
|
69
|
+
:extra => {
|
70
|
+
:session_state => '532fgdsgtfera32',
|
71
|
+
:raw_info => {
|
72
|
+
:id_token => "fjeri9wqrfe98r23.fdsaf121435rt.f42qfdsaf",
|
73
|
+
:id_token_claims => {
|
74
|
+
"aud" => "fdsafdsa-fdsafd-fdsa-sfdasfds",
|
75
|
+
"iss" => "https://sts.windows.net/fdsafdsa-fdsafdsa/",
|
76
|
+
"iat" => 53315113,
|
77
|
+
"nbf" => 53143215,
|
78
|
+
"exp" => 53425123,
|
79
|
+
"ver" => "1.0",
|
80
|
+
"tid" => "5ffdsa2f-dsafds-sda-sds",
|
81
|
+
"oid" => "fdsafdsaafdsa",
|
82
|
+
"upn" => "jsmith@contoso.com",
|
83
|
+
"sub" => "123456abcdef",
|
84
|
+
"nonce" => "fdsaf342rfdsafdsafsads"
|
85
|
+
},
|
86
|
+
:id_token_header => {
|
87
|
+
"typ" => "JWT",
|
88
|
+
"alg" => "RS256",
|
89
|
+
"x5t" => "fdsafdsafdsafdsa4t4er32",
|
90
|
+
"kid" => "tjiofpjd8ap9fgdsa44"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
7
|
+
# This can be run with `bundle exec rake spec`.
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
9
|
+
t.pattern = `git ls-files`.split("\n").select { |f| f.end_with? 'spec.rb' }
|
10
|
+
t.rspec_opts = '--format documentation'
|
11
|
+
end
|
12
|
+
|
13
|
+
# This can be run with `bundle exec rake rubocop`.
|
14
|
+
RuboCop::RakeTask.new(:rubocop) do |t|
|
15
|
+
t.patterns = `git ls-files`.split("\n").select do |f|
|
16
|
+
f.end_with?('.rb') && !f.start_with?('examples')
|
17
|
+
|
18
|
+
end
|
19
|
+
t.fail_on_error = false
|
20
|
+
end
|
21
|
+
|
22
|
+
task default: :spec
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require 'omniauth'
|
3
|
+
require 'openssl'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module OmniAuth
|
7
|
+
module Strategies
|
8
|
+
# A strategy for authentication against Azure Active Directory.
|
9
|
+
class ActiveDirectory
|
10
|
+
include OmniAuth::ActiveDirectory
|
11
|
+
include OmniAuth::Strategy
|
12
|
+
|
13
|
+
class OAuthError < StandardError; end
|
14
|
+
|
15
|
+
##
|
16
|
+
# The client id (key) and tenant must be configured when the OmniAuth
|
17
|
+
# middleware is installed. Example:
|
18
|
+
#
|
19
|
+
# require 'omniauth'
|
20
|
+
# require 'omniauth-activedirectory'
|
21
|
+
#
|
22
|
+
# use OmniAuth::Builder do
|
23
|
+
# provider :activedirectory, ENV['AAD_KEY'], ENV['AAD_TENANT']
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
args [:client_id, :tenant]
|
27
|
+
option :client_id, nil
|
28
|
+
option :tenant, nil
|
29
|
+
|
30
|
+
# Field renaming is an attempt to fit the OmniAuth recommended schema as
|
31
|
+
# best as possible.
|
32
|
+
#
|
33
|
+
# @see https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
|
34
|
+
uid { @claims['sub'] }
|
35
|
+
info do
|
36
|
+
{ name: @claims['name'],
|
37
|
+
email: @claims['email'] || @claims['upn'],
|
38
|
+
first_name: @claims['given_name'],
|
39
|
+
last_name: @claims['family_name'] }
|
40
|
+
end
|
41
|
+
credentials { { code: @code } }
|
42
|
+
extra do
|
43
|
+
{ session_state: @session_state,
|
44
|
+
raw_info:
|
45
|
+
{ id_token: @id_token,
|
46
|
+
id_token_claims: @claims,
|
47
|
+
id_token_header: @header } }
|
48
|
+
end
|
49
|
+
|
50
|
+
DEFAULT_RESPONSE_TYPE = 'code id_token'
|
51
|
+
DEFAULT_RESPONSE_MODE = 'form_post'
|
52
|
+
|
53
|
+
##
|
54
|
+
# Overridden method from OmniAuth::Strategy. This is the first step in the
|
55
|
+
# authentication process.
|
56
|
+
def request_phase
|
57
|
+
redirect authorize_endpoint_url
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Overridden method from OmniAuth::Strategy. This is the second step in
|
62
|
+
# the authentication process. It is called after the user enters
|
63
|
+
# credentials at the authorization endpoint.
|
64
|
+
def callback_phase
|
65
|
+
error = request.params['error_reason'] || request.params['error']
|
66
|
+
fail(OAuthError, error) if error
|
67
|
+
@session_state = request.params['session_state']
|
68
|
+
@id_token = request.params['id_token']
|
69
|
+
@code = request.params['code']
|
70
|
+
@claims, @header = validate_and_parse_id_token(@id_token)
|
71
|
+
# validate_chash(@code, @claims, @header)
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
##
|
78
|
+
# Constructs a one-time-use authorize_endpoint. This method will use
|
79
|
+
# a new nonce on each invocation.
|
80
|
+
#
|
81
|
+
# @return String
|
82
|
+
def authorize_endpoint_url
|
83
|
+
uri = URI(openid_config['authorization_endpoint'])
|
84
|
+
uri.query = URI.encode_www_form(client_id: client_id,
|
85
|
+
redirect_uri: callback_url,
|
86
|
+
response_mode: response_mode,
|
87
|
+
response_type: response_type,
|
88
|
+
nonce: new_nonce)
|
89
|
+
uri.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# The client id of the calling application. This must be configured where
|
94
|
+
# AD is installed as an OmniAuth strategy.
|
95
|
+
#
|
96
|
+
# @return String
|
97
|
+
def client_id
|
98
|
+
return options.client_id if options.client_id
|
99
|
+
fail StandardError, 'No client_id specified in AzureAD configuration.'
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# The expected id token issuer taken from the discovery endpoint.
|
104
|
+
#
|
105
|
+
# @return String
|
106
|
+
def issuer
|
107
|
+
openid_config['issuer']
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Fetches the current signing keys for Azure AD. Note that there should
|
112
|
+
# always two available, and that they have a 6 week rollover.
|
113
|
+
#
|
114
|
+
# Each key is a hash with the following fields:
|
115
|
+
# kty, use, kid, x5t, n, e, x5c
|
116
|
+
#
|
117
|
+
# @return Array[Hash]
|
118
|
+
def fetch_signing_keys
|
119
|
+
response = JSON.parse(Net::HTTP.get(URI(signing_keys_url)))
|
120
|
+
response['keys']
|
121
|
+
rescue JSON::ParserError
|
122
|
+
raise StandardError, 'Unable to fetch AzureAD signing keys.'
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Fetches the OpenId Connect configuration for the AzureAD tenant. This
|
127
|
+
# contains several import values, including:
|
128
|
+
#
|
129
|
+
# authorization_endpoint
|
130
|
+
# token_endpoint
|
131
|
+
# token_endpoint_auth_methods_supported
|
132
|
+
# jwks_uri
|
133
|
+
# response_types_supported
|
134
|
+
# response_modes_supported
|
135
|
+
# subject_types_supported
|
136
|
+
# id_token_signing_alg_values_supported
|
137
|
+
# scopes_supported
|
138
|
+
# issuer
|
139
|
+
# claims_supported
|
140
|
+
# microsoft_multi_refresh_token
|
141
|
+
# check_session_iframe
|
142
|
+
# end_session_endpoint
|
143
|
+
# userinfo_endpoint
|
144
|
+
#
|
145
|
+
# @return Hash
|
146
|
+
def fetch_openid_config
|
147
|
+
JSON.parse(Net::HTTP.get(URI(openid_config_url)))
|
148
|
+
rescue JSON::ParserError
|
149
|
+
raise StandardError, 'Unable to fetch OpenId configuration for ' \
|
150
|
+
'AzureAD tenant.'
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Generates a new nonce for one time use. Stores it in the session so
|
155
|
+
# multiple users don't share nonces. All nonces should be generated by
|
156
|
+
# this method.
|
157
|
+
#
|
158
|
+
# @return String
|
159
|
+
def new_nonce
|
160
|
+
session['omniauth-azure-activedirectory.nonce'] = SecureRandom.uuid
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# A memoized version of #fetch_openid_config.
|
165
|
+
#
|
166
|
+
# @return Hash
|
167
|
+
def openid_config
|
168
|
+
@openid_config ||= fetch_openid_config
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# The location of the OpenID configuration for the tenant.
|
173
|
+
#
|
174
|
+
# @return String
|
175
|
+
def openid_config_url
|
176
|
+
"https://login.windows.net/#{tenant}/.well-known/openid-configuration"
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Returns the most recent nonce for the session and deletes it from the
|
181
|
+
# session.
|
182
|
+
#
|
183
|
+
# @return String
|
184
|
+
def read_nonce
|
185
|
+
session.delete('omniauth-azure-activedirectory.nonce')
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# The response_type that will be set in the authorization request query
|
190
|
+
# parameters. Can be overridden by the client, but it shouldn't need to
|
191
|
+
# be.
|
192
|
+
#
|
193
|
+
# @return String
|
194
|
+
def response_type
|
195
|
+
options[:response_type] || DEFAULT_RESPONSE_TYPE
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# The response_mode that will be set in the authorization request query
|
200
|
+
# parameters. Can be overridden by the client, but it shouldn't need to
|
201
|
+
# be.
|
202
|
+
#
|
203
|
+
# @return String
|
204
|
+
def response_mode
|
205
|
+
options[:response_mode] || DEFAULT_RESPONSE_MODE
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# The keys used to sign the id token JWTs. This is just a memoized version
|
210
|
+
# of #fetch_signing_keys.
|
211
|
+
#
|
212
|
+
# @return Array[Hash]
|
213
|
+
def signing_keys
|
214
|
+
@signing_keys ||= fetch_signing_keys
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# The location of the public keys of the token signer. This is parsed from
|
219
|
+
# the OpenId config response.
|
220
|
+
#
|
221
|
+
# @return String
|
222
|
+
def signing_keys_url
|
223
|
+
return openid_config['jwks_uri'] if openid_config.include? 'jwks_uri'
|
224
|
+
fail StandardError, 'No jwks_uri in OpenId config response.'
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# The tenant of the calling application. Note that this must be
|
229
|
+
# explicitly configured when installing the AzureAD OmniAuth strategy.
|
230
|
+
#
|
231
|
+
# @return String
|
232
|
+
def tenant
|
233
|
+
return options.tenant if options.tenant
|
234
|
+
fail StandardError, 'No tenant specified in AzureAD configuration.'
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Verifies the signature of the id token as well as the exp, nbf, iat,
|
239
|
+
# iss, and aud fields.
|
240
|
+
#
|
241
|
+
# See OpenId Connect Core 3.1.3.7 and 3.2.2.11.
|
242
|
+
#
|
243
|
+
# @return Claims, Header
|
244
|
+
def validate_and_parse_id_token(id_token)
|
245
|
+
# The second parameter is the public key to verify the signature.
|
246
|
+
# However, that key is overridden by the value of the executed block
|
247
|
+
# if one is present.
|
248
|
+
#
|
249
|
+
# If you're thinking that this looks ugly with the raw nil and boolean,
|
250
|
+
# see https://github.com/jwt/ruby-jwt/issues/59.
|
251
|
+
jwt_claims, jwt_header =
|
252
|
+
JWT.decode(id_token, nil, false, verify_options) do |header|
|
253
|
+
# There should always be one key from the discovery endpoint that
|
254
|
+
# matches the id in the JWT header.
|
255
|
+
x5c = (signing_keys.find do |key|
|
256
|
+
key['kid'] == header['kid']
|
257
|
+
end || {})['x5c']
|
258
|
+
if x5c.nil? || x5c.empty?
|
259
|
+
fail JWT::VerificationError,
|
260
|
+
'No keys from key endpoint match the id token'
|
261
|
+
end
|
262
|
+
# The key also contains other fields, such as n and e, that are
|
263
|
+
# redundant. x5c is sufficient to verify the id token.
|
264
|
+
OpenSSL::X509::Certificate.new(JWT.base64url_decode(x5c.first)).public_key
|
265
|
+
end
|
266
|
+
return jwt_claims, jwt_header if jwt_claims['nonce'] == read_nonce
|
267
|
+
fail JWT::DecodeError, 'Returned nonce did not match.'
|
268
|
+
end
|
269
|
+
|
270
|
+
##
|
271
|
+
# Verifies that the c_hash the id token claims matches the authorization
|
272
|
+
# code. See OpenId Connect Core 3.3.2.11.
|
273
|
+
#
|
274
|
+
# @param String code
|
275
|
+
# @param Hash claims
|
276
|
+
# @param Hash header
|
277
|
+
# def validate_chash(code, claims, header)
|
278
|
+
# # This maps RS256 -> sha256, ES384 -> sha384, etc.
|
279
|
+
# algorithm = (header['alg'] || 'RS256').sub(/RS|ES|HS/, 'sha')
|
280
|
+
# full_hash = OpenSSL::Digest.new(algorithm).digest code
|
281
|
+
# c_hash = JWT.base64url_encode full_hash[0..full_hash.length / 2 - 1]
|
282
|
+
# return if c_hash == claims['c_hash']
|
283
|
+
# fail JWT::VerificationError,
|
284
|
+
# 'c_hash in id token does not match auth code.'
|
285
|
+
# end
|
286
|
+
|
287
|
+
##
|
288
|
+
# The options passed to the Ruby JWT library to verify the id token.
|
289
|
+
# Note that these are not all the checks we perform. Some (like nonce)
|
290
|
+
# are not handled by the JWT API and are checked manually in
|
291
|
+
# #validate_and_parse_id_token.
|
292
|
+
#
|
293
|
+
# @return Hash
|
294
|
+
def verify_options
|
295
|
+
{ verify_expiration: true,
|
296
|
+
verify_not_before: true,
|
297
|
+
verify_iat: true,
|
298
|
+
verify_iss: true,
|
299
|
+
'iss' => issuer,
|
300
|
+
verify_aud: true,
|
301
|
+
'aud' => client_id }
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
OmniAuth.config.add_camelization 'activedirectory', 'ActiveDirectory'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'omniauth/activedirectory'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'omniauth/activedirectory/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'omniauth-activedirectory'
|
6
|
+
s.version = OmniAuth::ActiveDirectory::VERSION
|
7
|
+
s.authors = ['Microsoft Corporation', 'Dave Van Fleet']
|
8
|
+
s.email = 'vanfleet@arsome.com'
|
9
|
+
s.summary = 'Azure Active Directory strategy for OmniAuth'
|
10
|
+
s.description = 'Allows developers to authenticate to Azure AD'
|
11
|
+
s.homepage = 'https://github.com/davevanfleet/omniauth-activedirectory'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.require_paths = ['lib']
|
16
|
+
|
17
|
+
s.required_ruby_version = '>= 2.2'
|
18
|
+
|
19
|
+
s.add_runtime_dependency 'jwt', '>= 2.0'
|
20
|
+
s.add_runtime_dependency 'oauth2', '~> 1.1'
|
21
|
+
s.add_runtime_dependency 'omniauth', '~> 2.0'
|
22
|
+
s.add_runtime_dependency 'omniauth-oauth2', '~> 1.7.1'
|
23
|
+
|
24
|
+
s.add_development_dependency 'rake', '~> 12.0'
|
25
|
+
s.add_development_dependency 'rspec', '~> 3.6'
|
26
|
+
s.add_development_dependency 'rubocop', '~> 0.49'
|
27
|
+
s.add_development_dependency 'simplecov', '~> 0.10'
|
28
|
+
s.add_development_dependency 'webmock', '~> 1.21'
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiJWcFRRaWk1VF84cmd3eEEtV3RiMkJ3In0.Xz9SL1dm9xeJ3YtBIwSpL7SHEMr5lsL32mkJIugoAt7rNhj2ZauN77_N3skU9FIRTVb_XBFHrLo1AXion7RWoOGAMk8xnuQRlhamGoWsjttWE9oO6J6kuQPSDBvLTA88UqLoGNezDwFNfgUFQq-66m33ZWdkiNOFoFZ_In6DtAwxHZZUys-KoYD3iCbviUoBzU57aV-SBsWyComq39pDGpw03qZoa_xgRfujdVHG1DKlO5VG79kUE3ySYWJyBYVKUdAzjH1iotjpPA1svtqytn4CUldAMi4nnf7iq5SCJMb4ucu0mN6AhJH9ktY--fGY6_OidyiDe4F57ZzLw-3jOQ
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6Im5vdCB0aGUgY2xpZW50IGlkIiwibm9uY2UiOiJteSBub25jZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJzbWl0aCIsImVtYWlsIjoianNtaXRoQGNvbnRvc28uY29tIn0.gGCFTcGDBb2P5tPRj2ojUUiwOJoSslQlxEVTElZY6FCzCZzcypsnrYOB9Adkztp2AWF4wW9fT58lqXwaahKquXtK4wyK4KoNBxXBhS4sDMeNpVkK4914tT_6gecvyUsI_tlJaS0epd5c0mN9-1QjgvNEirY1L-XYaT290LmLYVYIFTEJXoSlnwvv081k0txdJZKr14JXd_bSLUbhGSd-NcGZkJuVmg2F_C65zd1wUsQOkV2iVdJ-ycaDJklv8-DFfDFIHQio8S9yqyieHwArRmTW9HzcoHhWBh2MIItszoTSbmQEF062NtNBPW8gyk1OSot5X9klJUhgPAAFqJ0TJQ
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiI1WEhDdk9qcUgtbnJBXzNXNUJYaWJnIn0.EHPFrxMbr2EH1IZ15F3oP1yvrukadY4ggZSOxm625qPoMxfhv-QzFm0YMhkAG0cLM_LSHi_RgedDwTGuzOtIWqmheYzydnhtbIBeKmx0RSgdob6WeiTZwJ93VRiV4q82Qda41JaaIl_wdWd4lVyVstd9o9jPYMtKEVLgaNDrtHt6pPxEGCVraiaM6tVyKc5XIHu3wNaqle5UZZREU6oirwTCUhXDZkz1g5qY2-aWUdfFVsElSarJuMcxGDPD20hvx7T3D7SCHAF8WhKw3AwQyrodKWCo3cqDOz6vHymb_-ELkJc14GMbtJiyrf6XYySZZoseoI_WBDmLfKcK637xCw
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy5pbXBvc3Rlci5uZXQvYnVuY2gtb2YtcmFuZG9tLWNoYXJzIiwibmFtZSI6IkpvaG4gU21pdGgiLCJhdWQiOiJ0aGUgY2xpZW50IGlkIiwibm9uY2UiOiJteSBub25jZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJzbWl0aCIsImVtYWlsIjoianNtaXRoQGNvbnRvc28uY29tIn0.fwnJuRsif_Td3MfXoyADHidYJyPFdWSBBoLbVAu4Lz3-pmSln9Vgxl5KowqEKq2LX5n0aqyWVGLcoT-_G_PNXuizuNmssv5vreKLvDMpsFXt2irdwGYDCRki7KQPBk3bn12YjBzE2EqiRy7dTEG_0vWoh1RqoNP72BBL8xYQUlIOFleZhT5KGNYbh7rvcmDq7aA-xdaXT3QJfHCHpitW_zVzZ5Gok_awcdx_v3r3eFbG2IT0PfmT40Ljia0aP2i60KgsOLLHarYO51KFNDEfr1pUDf4IweaEzstbVLwk-_5ulJ5QgByhNJrmWDfrGeCQRk0SO2XX-EUsVn5ySVJ5CQ
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMzQifQ.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiI1WEhDdk9qcUgtbnJBXzNXNUJYaWJnIn0.gWGBc9rH30SN17Ikm1CjqIYAyzFHX0yeRQu85sVYLE3r5k26bjS3R6rTJcCQlYqHPRdoPcnkUgT1QVbdThw34ICrODoavs7I5QYEn_jKP9zM4UJEKQaCLBAtitzrk1KDEf0GLcNKif-MYu7MiQfoOzwCGWfIs-vgqk4lv0JUs9OlSLp5LHru7G3jKy6Qswbrpxpjzm9I8BnKEdhUfhz4P6wIf9KLMmhgeGQdQ6wBuxPmOf9r6EKIij2AENhFp1qP90m8kXq9tIt5FZFjwIs_G6spLl0gQXyx0qC8rP5JTkqwrBUieWU-BRqVdax8YxA0iDKzZyfsMV92yVcZT6S_NQ
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im5vdCBteSBub25jZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJzbWl0aCIsImVtYWlsIjoianNtaXRoQGNvbnRvc28uY29tIn0.KBSOCiy0sUF33akept9nPNFgmMWxiPWDBVA0dZqQaF6gQkhQu82irAQzB2Ygkh9KDeIEf0MSZWLDq0X3W3Fke4wzjrbzL-2QM4l-KgPFbJixqtJYHSPOidHSCQ8vA0v8kES3H_zqU6QisygwwXLh2ozqKXMnsrBPIAtiZz_a0vPbHtrYbb-WIrtTruMemcTt5OkbfDIttzi5EarakQg93vraIb0jK0szuAqLkXFOzcIIGPgyAvVpHZveqm99GR04tbKyTRIyJP1vZwtIpu89PKFM3soWcWd_kjAWcS81ZTzEn5_P-1QL7YTInK53acNXZHh08Fba3Um4J_KlV2KRjw
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiJWcFRRaWk1VF84cmd3eEEtV3RiMkJ3In0.Xz9SL1dm9xeJ3YtBIwSpL7SHEMr5lsL32mkJIugoAt7rNhj2ZauN77_N3skU9FIRTVb_XBFHrLo1AXion7RWoOGAMk8xnuQRlhamGoWsjttWE9oO6J6kuQPSDBvLTA88UqLoGNezDwFNfgUFQq-66m33ZWdkiNOFoFZ_In6DtAwxHZZUys-KoYD3iCbviUoBzU57aV-SBsWyComq39pDGpw03qZoa_xgRfujdVHG1DKlO5VG79kUE3ySYWJyBYVKUdAzjH1iotjpPA1svtqytn4CUldAMi4nnf7iq5SCJMb4ucu0mN6AhJH9ktY--fGY6_OidyiDe4F57ZzLw-3jOQ
|
@@ -0,0 +1 @@
|
|
1
|
+
MIIBwzCCAbegAwIBAgIBATADBgEAMDAxEzARBgoJkiaJk_IsZAEZFgNvcmcxGTAXBgoJkiaJk_IsZAEZFglydWJ5LWxhbmcwHhcNMTUwODA2MjMyOTE4WhcNMjUwODAzMjMyOTE4WjAwMRMwEQYKCZImiZPyLGQBGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxyunSConbK4z1T47vukPa0OaNcY_R6l8Z0TILR1w5O_YkMbJGRXpZORFPVs83xLoxs7RcPWnibQhQ4G6m6fwEA4rutvOm-3xWyey-OOZdVqpNmxqd2VcCCI6AoUtl8m4w0UFuu-oiELj7BF8cGS5wvHYATEBEY72n_84uu-LF423Ffe8HeMEY00nO3ZVcV9MjFBVps8RAwL62ooXPZyly6fQt4728wlZPs3EMijS-Bj4auokx6ssBooaF9XiZJKKCAe16epbBB9S4XVlNXo6EhZ-PBpMFRJknDwWFFYPVOX3NlBp8chi8VaXmDXqsFJiwkkeIqg4I6WFAM4BsnDInwIDAQABMAMGAQADAQA
|
@@ -0,0 +1 @@
|
|
1
|
+
MIIBjTCCAYGgAwIBAgIBATADBgEAMBUxEzARBgoJkiaJk_IsZAEZFgNvcmcwHhcNMTUwODA3MjAyMDU1WhcNMjUwODA0MjAyMDU1WjAVMRMwEQYKCZImiZPyLGQBGRYDb3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy8VyE4KHx0GDV6RMYqscBR56lal9uQNYXm_Du8O6-F8Da9Xc0h7O8JWomqFLUEE-0ogMcjzOAiQzTc8X6VhkIBbMCKYA6OQ-hiC3aaYFLNAnxaVThnUJWXxA7spbnKFG1xZK01G8bAx7s7DbbWMHlchebjWprCMkYEjnECXVuDyfkZW-8aHDCtq62JAd-WQL1LN-UkOBCgodTNW2x7e-_KBvPzPSSzYygAh7kCl727QeHwplgA83mQrdecNvYoiEBYOjSKz8bdiKRYjmogGoDv3_W4cY76a9XBZpSoNjBWdWo_w7ce4KbOsi3V0g8EZm9ccKjBPKe9pZYF4aCNQJ6QIDAQABMAMGAQADAQA
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'omniauth-activedirectory'
|
3
|
+
|
4
|
+
# This was fairly awkward to test. I've stubbed every endpoint and am simulating
|
5
|
+
# the state of the request. Especially large strings are stored in fixtures.
|
6
|
+
describe OmniAuth::Strategies::ActiveDirectory do
|
7
|
+
let(:app) { -> _ { [200, {}, ['Hello world.']] } }
|
8
|
+
let(:x5c) { File.read(File.expand_path('../../../fixtures/x5c.txt', __FILE__)) }
|
9
|
+
|
10
|
+
# These values were used to create the "successful" id_token JWT.
|
11
|
+
let(:client_id) { 'the client id' }
|
12
|
+
let(:code) { 'code' }
|
13
|
+
let(:email) { 'jsmith@contoso.com' }
|
14
|
+
let(:family_name) { 'smith' }
|
15
|
+
let(:given_name) { 'John' }
|
16
|
+
let(:issuer) { 'https://sts.windows.net/bunch-of-random-chars' }
|
17
|
+
let(:kid) { 'abc123' }
|
18
|
+
let(:name) { 'John Smith' }
|
19
|
+
let(:nonce) { 'my nonce' }
|
20
|
+
let(:session_state) { 'session state' }
|
21
|
+
let(:auth_endpoint_host) { 'authorize.com' }
|
22
|
+
|
23
|
+
let(:hybrid_flow_params) do
|
24
|
+
{ 'id_token' => id_token,
|
25
|
+
'session_state' => session_state,
|
26
|
+
'code' => code }
|
27
|
+
end
|
28
|
+
|
29
|
+
let(:tenant) { 'tenant' }
|
30
|
+
let(:openid_config_response) { "{\"issuer\":\"#{issuer}\",\"authorization_endpoint\":\"http://#{auth_endpoint_host}\",\"jwks_uri\":\"https://login.windows.net/common/discovery/keys\"}" }
|
31
|
+
let(:keys_response) { "{\"keys\":[{\"kid\":\"#{kid}\",\"x5c\":[\"#{x5c}\"]}]}" }
|
32
|
+
|
33
|
+
let(:env) { { 'rack.session' => { 'omniauth-azure-activedirectory.nonce' => nonce } } }
|
34
|
+
|
35
|
+
before(:each) do
|
36
|
+
stub_request(:get, "https://login.windows.net/#{tenant}/.well-known/openid-configuration")
|
37
|
+
.to_return(status: 200, body: openid_config_response)
|
38
|
+
stub_request(:get, 'https://login.windows.net/common/discovery/keys')
|
39
|
+
.to_return(status: 200, body: keys_response)
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#callback_phase' do
|
43
|
+
let(:request) { double('Request', params: hybrid_flow_params, path_info: 'path') }
|
44
|
+
let(:strategy) do
|
45
|
+
described_class.new(app, client_id, tenant).tap do |s|
|
46
|
+
allow(s).to receive(:request) { request }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
subject { -> { strategy.callback_phase } }
|
51
|
+
before(:each) { strategy.call!(env) }
|
52
|
+
|
53
|
+
context 'with a successful response' do
|
54
|
+
# payload:
|
55
|
+
# { 'iss' => 'https://sts.windows.net/bunch-of-random-chars',
|
56
|
+
# 'name' => 'John Smith',
|
57
|
+
# 'aud' => 'the client id',
|
58
|
+
# 'nonce' => 'my nonce',
|
59
|
+
# 'email' => 'jsmith@contoso.com',
|
60
|
+
# 'given_name' => 'John',
|
61
|
+
# 'family_name' => 'Smith' }
|
62
|
+
# headers:
|
63
|
+
# { 'typ' => 'JWT',
|
64
|
+
# 'alg' => 'RS256',
|
65
|
+
# 'kid' => 'abc123' }
|
66
|
+
#
|
67
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token.txt', __FILE__)) }
|
68
|
+
|
69
|
+
# If it passes this test, then the id was successfully validated.
|
70
|
+
it { is_expected.to_not raise_error }
|
71
|
+
|
72
|
+
describe 'the auth hash' do
|
73
|
+
before(:each) { strategy.callback_phase }
|
74
|
+
|
75
|
+
subject { env['omniauth.auth'] }
|
76
|
+
|
77
|
+
it 'should contain the name' do
|
78
|
+
expect(subject.info['name']).to eq name
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should contain the first name' do
|
82
|
+
expect(subject.info['first_name']).to eq given_name
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should contain the last name' do
|
86
|
+
expect(subject.info['last_name']).to eq family_name
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should contain the email' do
|
90
|
+
expect(subject.info['email']).to eq email
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should contain the auth code' do
|
94
|
+
expect(subject.credentials['code']).to eq code
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should contain the session state' do
|
98
|
+
expect(subject.extra['session_state']).to eq session_state
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with an invalid issuer' do
|
104
|
+
# payload:
|
105
|
+
# { 'iss' => 'https://sts.imposter.net/bunch-of-random-chars', ... }
|
106
|
+
#
|
107
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_bad_issuer.txt', __FILE__)) }
|
108
|
+
it { is_expected.to raise_error JWT::InvalidIssuerError }
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'with an invalid audience' do
|
112
|
+
# payload:
|
113
|
+
# { 'aud' => 'not the client id', ... }
|
114
|
+
#
|
115
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_bad_audience.txt', __FILE__)) }
|
116
|
+
it { is_expected.to raise_error JWT::InvalidAudError }
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'with a non-matching nonce' do
|
120
|
+
# payload:
|
121
|
+
# { 'nonce' => 'not my nonce', ... }
|
122
|
+
#
|
123
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_bad_nonce.txt', __FILE__)) }
|
124
|
+
it { is_expected.to raise_error JWT::DecodeError }
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'with the wrong x5c' do
|
128
|
+
let(:x5c) { File.read(File.expand_path('../../../fixtures/x5c_different.txt', __FILE__)) }
|
129
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token.txt', __FILE__)) }
|
130
|
+
it { is_expected.to raise_error JWT::VerificationError }
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with a non-matching c_hash' do
|
134
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_bad_chash.txt', __FILE__)) }
|
135
|
+
it { is_expected.to raise_error JWT::VerificationError }
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with a non-matching kid' do
|
139
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_bad_kid.txt', __FILE__)) }
|
140
|
+
it { is_expected.to raise_error JWT::VerificationError }
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'with no alg header' do
|
144
|
+
let(:id_token) { File.read(File.expand_path('../../../fixtures/id_token_no_alg.txt', __FILE__)) }
|
145
|
+
|
146
|
+
it 'should correctly parse using default RS256' do
|
147
|
+
expect(subject).to_not raise_error
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'the auth hash' do
|
151
|
+
subject { env['omniauth.auth'] }
|
152
|
+
before(:each) { strategy.callback_phase }
|
153
|
+
|
154
|
+
it 'should default to RS256' do
|
155
|
+
expect(subject.info['name']).to eq name
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#request_phase' do
|
162
|
+
let(:strategy) { described_class.new(app, client_id, tenant) }
|
163
|
+
subject { strategy.request_phase }
|
164
|
+
before(:each) { strategy.call!(env) }
|
165
|
+
|
166
|
+
it 'should make a redirect' do
|
167
|
+
expect(subject.first).to eq 302
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should redirect to the correct endpoint' do
|
171
|
+
expect(URI(subject[1]['Location']).host).to eq auth_endpoint_host
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe '#read_nonce' do
|
176
|
+
let(:strategy) { described_class.new(app, client_id, tenant) }
|
177
|
+
let(:env) { { 'rack.session' => {} } }
|
178
|
+
before(:each) { strategy.call!(env) }
|
179
|
+
subject { strategy.send(:read_nonce) }
|
180
|
+
|
181
|
+
context 'before a nonce is set' do
|
182
|
+
it { is_expected.to be nil }
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'after a nonce is set' do
|
186
|
+
before(:each) { @nonce = strategy.send(:new_nonce) }
|
187
|
+
it 'should match' do
|
188
|
+
expect(subject).to eq @nonce
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context 'twice in a row' do
|
193
|
+
before(:each) do
|
194
|
+
strategy.send(:new_nonce)
|
195
|
+
strategy.send(:read_nonce)
|
196
|
+
end
|
197
|
+
it { is_expected.to be nil }
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
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 'webmock/rspec'
|
24
|
+
require 'simplecov'
|
25
|
+
|
26
|
+
SimpleCov.start do
|
27
|
+
# Don't measure coverage on test files.
|
28
|
+
add_filter 'spec'
|
29
|
+
end
|
30
|
+
|
31
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
32
|
+
|
33
|
+
RSpec.configure do |config|
|
34
|
+
config.expect_with :rspec do |expectations|
|
35
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
36
|
+
end
|
37
|
+
|
38
|
+
config.mock_with :rspec do |mocks|
|
39
|
+
mocks.verify_partial_doubles = true
|
40
|
+
end
|
41
|
+
|
42
|
+
config.warnings = true
|
43
|
+
config.order = :random
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omniauth-activedirectory
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Microsoft Corporation
|
8
|
+
- Dave Van Fleet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2021-03-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: jwt
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '2.0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '2.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: oauth2
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.1'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.1'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: omniauth
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '2.0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '2.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: omniauth-oauth2
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.7.1
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.7.1
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '12.0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '12.0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3.6'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.6'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rubocop
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0.49'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0.49'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: simplecov
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0.10'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0.10'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: webmock
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '1.21'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '1.21'
|
140
|
+
description: Allows developers to authenticate to Azure AD
|
141
|
+
email: vanfleet@arsome.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".rubocop.yml"
|
148
|
+
- ".rubocop_todo.yml"
|
149
|
+
- Gemfile
|
150
|
+
- LICENSE.txt
|
151
|
+
- README.md
|
152
|
+
- Rakefile
|
153
|
+
- lib/omniauth-activedirectory.rb
|
154
|
+
- lib/omniauth/activedirectory.rb
|
155
|
+
- lib/omniauth/activedirectory/version.rb
|
156
|
+
- lib/omniauth/strategies/activedirectory.rb
|
157
|
+
- omniauth-activedirectory.gemspec
|
158
|
+
- spec/fixtures/id_token.txt
|
159
|
+
- spec/fixtures/id_token_bad_audience.txt
|
160
|
+
- spec/fixtures/id_token_bad_chash.txt
|
161
|
+
- spec/fixtures/id_token_bad_issuer.txt
|
162
|
+
- spec/fixtures/id_token_bad_kid.txt
|
163
|
+
- spec/fixtures/id_token_bad_nonce.txt
|
164
|
+
- spec/fixtures/id_token_no_alg.txt
|
165
|
+
- spec/fixtures/x5c.txt
|
166
|
+
- spec/fixtures/x5c_different.txt
|
167
|
+
- spec/omniauth/strategies/activedirectory_spec.rb
|
168
|
+
- spec/spec_helper.rb
|
169
|
+
homepage: https://github.com/davevanfleet/omniauth-activedirectory
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '2.2'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubygems_version: 3.2.3
|
189
|
+
signing_key:
|
190
|
+
specification_version: 4
|
191
|
+
summary: Azure Active Directory strategy for OmniAuth
|
192
|
+
test_files: []
|