omniauth-microsoft_graph 2.0.0 → 2.1.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/.github/workflows/ruby.yml +4 -5
- data/lib/omniauth/microsoft_graph/domain_verifier.rb +17 -4
- data/lib/omniauth/microsoft_graph/version.rb +1 -1
- data/lib/omniauth/strategies/microsoft_graph.rb +19 -1
- data/omniauth-microsoft_graph.gemspec +1 -1
- data/spec/omniauth/microsoft_graph/domain_verifier_spec.rb +44 -13
- data/spec/omniauth/strategies/microsoft_graph_oauth2_spec.rb +78 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52fd8e2974f69cc1724772712dcc133fb7ac39b5043348702d8da1cc1e6cd96f
|
4
|
+
data.tar.gz: 504e046b7ca25c06306a7526403e1ae9d86f568cfd83983b07b270ca984a7726
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 359a084ad05f7d14463bc7a458dacec43d6563c3fc5a37db931dc08bf91dc652db782e1525567c8d2f921a811687e2295844b3b3d5322f4a5a436e3a020aca87
|
7
|
+
data.tar.gz: 05d76aefd01bc242ba5763fa6041156960f46835e2ddbd69e807b53a9404be558fe79a349addfcfb2c1a50b5e7e40b4b2cb5ff385439e8754e129115a123d691
|
data/.github/workflows/ruby.yml
CHANGED
@@ -8,24 +8,23 @@
|
|
8
8
|
name: Ruby
|
9
9
|
|
10
10
|
on:
|
11
|
-
push:
|
12
11
|
pull_request:
|
13
12
|
|
14
13
|
jobs:
|
15
14
|
test:
|
16
15
|
|
17
|
-
runs-on: ubuntu-latest
|
18
16
|
strategy:
|
19
17
|
matrix:
|
20
|
-
|
18
|
+
os: [ubuntu-latest, macos-latest]
|
19
|
+
ruby-version: ['3.0', '3.1', '3.2', '3.3']
|
20
|
+
runs-on: ${{ matrix.os }}
|
21
21
|
|
22
22
|
steps:
|
23
23
|
- uses: actions/checkout@v2
|
24
24
|
- name: Set up Ruby
|
25
25
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
26
26
|
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
27
|
-
|
28
|
-
uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
27
|
+
uses: ruby/setup-ruby@v1
|
29
28
|
with:
|
30
29
|
ruby-version: ${{ matrix.ruby-version }}
|
31
30
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
@@ -9,6 +9,7 @@ module OmniAuth
|
|
9
9
|
# https://www.descope.com/blog/post/noauth
|
10
10
|
# https://clerk.com/docs/authentication/social-connections/microsoft#stay-secure-against-the-n-o-auth-vulnerability
|
11
11
|
OIDC_CONFIG_URL = 'https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration'
|
12
|
+
COMMON_JWKS_URL = 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
|
12
13
|
|
13
14
|
class DomainVerificationError < OmniAuth::Error; end
|
14
15
|
|
@@ -62,13 +63,25 @@ module OmniAuth
|
|
62
63
|
def domain_verified_jwt_claim
|
63
64
|
oidc_config = access_token.get(OIDC_CONFIG_URL).parsed
|
64
65
|
algorithms = oidc_config['id_token_signing_alg_values_supported']
|
65
|
-
|
66
|
-
decoded_token = JWT.decode(id_token, nil, true, algorithms: algorithms, jwks:
|
66
|
+
jwks = get_jwks(oidc_config)
|
67
|
+
decoded_token = JWT.decode(id_token, nil, true, algorithms: algorithms, jwks: jwks)
|
68
|
+
xms_edov_valid?(decoded_token)
|
69
|
+
rescue JWT::VerificationError, ::OAuth2::Error
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
def xms_edov_valid?(decoded_token)
|
67
74
|
# https://github.com/MicrosoftDocs/azure-docs/issues/111425#issuecomment-1761043378
|
68
75
|
# Comments seemed to indicate the value is not consistent
|
69
76
|
['1', 1, 'true', true].include?(decoded_token.first['xms_edov'])
|
70
|
-
|
71
|
-
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_jwks(oidc_config)
|
80
|
+
# Depending on the tenant, the JWKS endpoint might be different. We need to
|
81
|
+
# consider both the JWKS from the OIDC configuration and the common JWKS endpoint.
|
82
|
+
oidc_config_jwk_keys = access_token.get(oidc_config['jwks_uri']).parsed[:keys]
|
83
|
+
common_jwk_keys = access_token.get(COMMON_JWKS_URL).parsed[:keys]
|
84
|
+
JWT::JWK::Set.new(oidc_config_jwk_keys + common_jwk_keys)
|
72
85
|
end
|
73
86
|
|
74
87
|
def verification_error_message
|
@@ -6,6 +6,8 @@ module OmniAuth
|
|
6
6
|
BASE_SCOPE_URL = 'https://graph.microsoft.com/'
|
7
7
|
BASE_SCOPES = %w[offline_access openid email profile].freeze
|
8
8
|
DEFAULT_SCOPE = 'offline_access openid email profile User.Read'.freeze
|
9
|
+
YAMMER_PROFILE_URL = 'https://www.yammer.com/api/v1/users/current.json'
|
10
|
+
MICROSOFT_GRAPH_PROFILE_URL = 'https://graph.microsoft.com/v1.0/me'
|
9
11
|
|
10
12
|
option :name, :microsoft_graph
|
11
13
|
|
@@ -64,7 +66,7 @@ module OmniAuth
|
|
64
66
|
end
|
65
67
|
|
66
68
|
def raw_info
|
67
|
-
@raw_info ||= access_token.get(
|
69
|
+
@raw_info ||= access_token.get(profile_endpoint).parsed
|
68
70
|
end
|
69
71
|
|
70
72
|
def callback_url
|
@@ -73,11 +75,27 @@ module OmniAuth
|
|
73
75
|
|
74
76
|
def custom_build_access_token
|
75
77
|
access_token = get_access_token(request)
|
78
|
+
# Get the profile(microsoft graph / yammer) endpoint choice based on returned bearer token
|
79
|
+
@profile_endpoint = determine_profile_endpoint(request)
|
76
80
|
access_token
|
77
81
|
end
|
78
82
|
|
79
83
|
alias build_access_token custom_build_access_token
|
80
84
|
|
85
|
+
def profile_endpoint
|
86
|
+
@profile_endpoint ||= MICROSOFT_GRAPH_PROFILE_URL
|
87
|
+
end
|
88
|
+
|
89
|
+
def determine_profile_endpoint(request)
|
90
|
+
scope = request&.env&.dig('omniauth.params', 'scope')
|
91
|
+
|
92
|
+
if scope&.include?('yammer')
|
93
|
+
YAMMER_PROFILE_URL
|
94
|
+
else
|
95
|
+
MICROSOFT_GRAPH_PROFILE_URL
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
81
99
|
private
|
82
100
|
|
83
101
|
def get_access_token(request)
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_runtime_dependency 'jwt', '~> 2.0'
|
22
22
|
spec.add_runtime_dependency 'omniauth', '~> 2.0'
|
23
23
|
spec.add_runtime_dependency 'omniauth-oauth2', '~> 1.8.0'
|
24
|
-
spec.add_development_dependency "sinatra", '~>
|
24
|
+
spec.add_development_dependency "sinatra", '~> 2.2'
|
25
25
|
spec.add_development_dependency "rake", '~> 12.3.3', '>= 12.3.3'
|
26
26
|
spec.add_development_dependency 'rspec', '~> 3.6'
|
27
27
|
spec.add_development_dependency "mocha", '~> 0'
|
@@ -41,34 +41,65 @@ RSpec.describe OmniAuth::MicrosoftGraph::DomainVerifier do
|
|
41
41
|
end
|
42
42
|
|
43
43
|
context 'when the ID token indicates domain verification' do
|
44
|
-
|
45
|
-
|
46
|
-
optional_parameters = { kid: 'mock-kid', use: 'sig', alg: 'RS256' }
|
44
|
+
let(:mock_oidc_key) do
|
45
|
+
optional_parameters = { kid: 'mock_oidc_key', use: 'sig', alg: 'RS256' }
|
47
46
|
JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
|
48
47
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
|
49
|
+
let(:mock_common_key) do
|
50
|
+
optional_parameters = { kid: 'mock_common_key', use: 'sig', alg: 'RS256' }
|
51
|
+
JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
|
52
52
|
end
|
53
53
|
|
54
|
-
# Mock the API responses to return the
|
54
|
+
# Mock the API responses to return the mock keys
|
55
55
|
before do
|
56
56
|
allow(access_token).to receive(:get)
|
57
57
|
.with(OmniAuth::MicrosoftGraph::OIDC_CONFIG_URL)
|
58
58
|
.and_return(
|
59
|
-
double(
|
60
|
-
'
|
61
|
-
|
62
|
-
|
59
|
+
double(
|
60
|
+
'OAuth2::Response',
|
61
|
+
parsed: {
|
62
|
+
'id_token_signing_alg_values_supported' => ['RS256'],
|
63
|
+
'jwks_uri' => 'https://example.com/jwks-keys',
|
64
|
+
}
|
65
|
+
)
|
63
66
|
)
|
64
67
|
allow(access_token).to receive(:get)
|
65
68
|
.with('https://example.com/jwks-keys')
|
66
69
|
.and_return(
|
67
|
-
double(
|
70
|
+
double(
|
71
|
+
'OAuth2::Response',
|
72
|
+
parsed: JWT::JWK::Set.new(mock_oidc_key).export
|
73
|
+
)
|
74
|
+
)
|
75
|
+
allow(access_token).to receive(:get)
|
76
|
+
.with(OmniAuth::MicrosoftGraph::COMMON_JWKS_URL)
|
77
|
+
.and_return(
|
78
|
+
double(
|
79
|
+
'OAuth2::Response',
|
80
|
+
parsed: JWT::JWK::Set.new(mock_common_key).export,
|
81
|
+
body: JWT::JWK::Set.new(mock_common_key).export.to_json
|
82
|
+
)
|
68
83
|
)
|
69
84
|
end
|
70
85
|
|
71
|
-
|
86
|
+
context 'when the kid exists in the oidc key' do
|
87
|
+
let(:id_token) do
|
88
|
+
payload = { email: email, xms_edov: true }
|
89
|
+
JWT.encode(payload, mock_oidc_key.signing_key, mock_oidc_key[:alg], kid: mock_oidc_key[:kid])
|
90
|
+
end
|
91
|
+
|
92
|
+
it { is_expected.to be_truthy }
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when the kid exists in the common key" do
|
96
|
+
let(:id_token) do
|
97
|
+
payload = { email: email, xms_edov: true }
|
98
|
+
JWT.encode(payload, mock_common_key.signing_key, mock_common_key[:alg], kid: mock_common_key[:kid])
|
99
|
+
end
|
100
|
+
|
101
|
+
it { is_expected.to be_truthy }
|
102
|
+
end
|
72
103
|
end
|
73
104
|
|
74
105
|
context 'when all verification strategies fail' do
|
@@ -457,4 +457,82 @@ describe OmniAuth::Strategies::MicrosoftGraph do
|
|
457
457
|
end.to raise_error(OAuth2::Error)
|
458
458
|
end
|
459
459
|
end
|
460
|
+
|
461
|
+
describe 'Yammer profile endpoint support' do
|
462
|
+
describe '#profile_endpoint' do
|
463
|
+
context 'when no profile endpoint is determined' do
|
464
|
+
it 'defaults to Microsoft Graph profile URL' do
|
465
|
+
expect(subject.profile_endpoint).to eq('https://graph.microsoft.com/v1.0/me')
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
context 'when profile endpoint is already set' do
|
470
|
+
before { subject.instance_variable_set(:@profile_endpoint, 'https://custom.endpoint.com') }
|
471
|
+
|
472
|
+
it 'returns the previously set endpoint' do
|
473
|
+
expect(subject.profile_endpoint).to eq('https://custom.endpoint.com')
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
describe '#determine_profile_endpoint' do
|
479
|
+
let(:request) { double('Request', env: request_env) }
|
480
|
+
|
481
|
+
context 'when scope includes Yammer access_as_user scope' do
|
482
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => 'https://api.yammer.com/access_as_user' } } }
|
483
|
+
|
484
|
+
it 'returns Yammer profile URL' do
|
485
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
context 'when scope includes Yammer user_impersonation scope' do
|
490
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile https://api.yammer.com/user_impersonation' } } }
|
491
|
+
|
492
|
+
it 'returns Yammer profile URL' do
|
493
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
context 'when scope includes Yammer scope among other scopes' do
|
498
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => 'offline_access openid email profile https://api.yammer.com/access_as_user User.Read' } } }
|
499
|
+
|
500
|
+
it 'returns Yammer profile URL' do
|
501
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
context 'when scope includes multiple Yammer scopes' do
|
506
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile https://api.yammer.com/access_as_user https://api.yammer.com/user_impersonation' } } }
|
507
|
+
|
508
|
+
it 'returns Yammer profile URL' do
|
509
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://www.yammer.com/api/v1/users/current.json')
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
context 'when scope does not include any Yammer scopes' do
|
514
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => 'openid profile User.Read' } } }
|
515
|
+
|
516
|
+
it 'returns Microsoft Graph profile URL' do
|
517
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
context 'when scope is nil' do
|
522
|
+
let(:request_env) { { 'omniauth.params' => { 'scope' => nil } } }
|
523
|
+
|
524
|
+
it 'returns Microsoft Graph profile URL' do
|
525
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
context 'when omniauth.params is nil' do
|
530
|
+
let(:request_env) { { 'omniauth.params' => nil } }
|
531
|
+
|
532
|
+
it 'returns Microsoft Graph profile URL' do
|
533
|
+
expect(subject.determine_profile_endpoint(request)).to eq('https://graph.microsoft.com/v1.0/me')
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
460
538
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: omniauth-microsoft_graph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Philips
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2025-07-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: jwt
|
@@ -59,14 +59,14 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '2.2'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '2.2'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rake
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -160,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
160
160
|
- !ruby/object:Gem::Version
|
161
161
|
version: '0'
|
162
162
|
requirements: []
|
163
|
-
rubygems_version: 3.
|
163
|
+
rubygems_version: 3.3.26
|
164
164
|
signing_key:
|
165
165
|
specification_version: 4
|
166
166
|
summary: omniauth provider for Microsoft Graph
|