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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 340d76dc549fc5e3710217599a247901f775a1ae26a4ea62eb3916928bc6afac
4
- data.tar.gz: 6319931d11bb4ff224c573a2fc16633b771cbd407115c1cca644d0226c05baae
3
+ metadata.gz: 52fd8e2974f69cc1724772712dcc133fb7ac39b5043348702d8da1cc1e6cd96f
4
+ data.tar.gz: 504e046b7ca25c06306a7526403e1ae9d86f568cfd83983b07b270ca984a7726
5
5
  SHA512:
6
- metadata.gz: 8bc6c22cf81b3996abe32d83f0e13ca88a156e69f53465e2421fe93f96193499743d37ffe9147ded38b877964aec637314ffc178839d06c5b52330ef46010984
7
- data.tar.gz: 62acd37d43f7b2e79171c328898a696c0f4c4d0583dfd817a0be54544e2b3adfd7239f9128a919b742b7131ba68ca74670059ece7e3c57424f834b6817173e70
6
+ metadata.gz: 359a084ad05f7d14463bc7a458dacec43d6563c3fc5a37db931dc08bf91dc652db782e1525567c8d2f921a811687e2295844b3b3d5322f4a5a436e3a020aca87
7
+ data.tar.gz: 05d76aefd01bc242ba5763fa6041156960f46835e2ddbd69e807b53a9404be558fe79a349addfcfb2c1a50b5e7e40b4b2cb5ff385439e8754e129115a123d691
@@ -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
- ruby-version: ['3.0']
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
- # uses: ruby/setup-ruby@v1
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
- keys = JWT::JWK::Set.new(access_token.get(oidc_config['jwks_uri']).parsed)
66
- decoded_token = JWT.decode(id_token, nil, true, algorithms: algorithms, jwks: keys)
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
- rescue JWT::VerificationError, ::OAuth2::Error
71
- false
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
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module MicrosoftGraph
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
  end
@@ -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('https://graph.microsoft.com/v1.0/me').parsed
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", '~> 0'
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
- # Sign a fake ID token with our own local key
45
- let(:mock_key) do
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
- let(:id_token) do
50
- payload = { email: email, xms_edov: true }
51
- JWT.encode(payload, mock_key.signing_key, mock_key[:alg], kid: mock_key[:kid])
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 local key
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('OAuth2::Response', parsed: {
60
- 'id_token_signing_alg_values_supported' => ['RS256'],
61
- 'jwks_uri' => 'https://example.com/jwks-keys'
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('OAuth2::Response', parsed: JWT::JWK::Set.new(mock_key).export)
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
- it { is_expected.to be_truthy }
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.0.0
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: 2023-12-30 00:00:00.000000000 Z
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: '0'
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: '0'
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.4.22
163
+ rubygems_version: 3.3.26
164
164
  signing_key:
165
165
  specification_version: 4
166
166
  summary: omniauth provider for Microsoft Graph