omniauth-microsoft_graph 0.2.1 → 2.2.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
- SHA1:
3
- metadata.gz: bc7fe638d8adc7a7d35f5c16bdee73b203ac1692
4
- data.tar.gz: 61003e06a1329c5da156f151293af05d9b55c34c
2
+ SHA256:
3
+ metadata.gz: e1ace889e4805d8c7a520ec058b237e48ce6fc69653daa869afb3b2871cff7fe
4
+ data.tar.gz: b515974d6ecf60e4fd6450b89b6de6046db45c7e0523992b76d06165dc489b47
5
5
  SHA512:
6
- metadata.gz: 2a3d808f2b673c8400650aebf3e8ac942b34f378b4419f358ddbeaee3c2780081447118dc18555d9761c6d5e4ed34da687b79da685dc04deb4d9967549041bd4
7
- data.tar.gz: ffe7af4f24659f74743360a51b8f67bb4e4f367c9bf1f4fe32b29bd9ca5781b24e10f0046c3ba3073d68595f071d5360840ace5f377eec3c088f8f9053a57dd8
6
+ metadata.gz: eeb19611338cc3034db9cd094e7ca179f6b45da19c5b84991cbce2deb2a5ef2feb71b0445fc051af6c32e0eaa5e1b62b72a05ced3ee57b0cf4880dce6731e6fb
7
+ data.tar.gz: 8903764632bf5de5064d2652f32f6f802b8ab0cdc4c56284f3faf6ba3b2865ed538c3665b3218d9a3be7af7fb50a51a6560f174380905cfdde84e1bac57c2845
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [synth] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
@@ -0,0 +1,32 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ pull_request:
12
+
13
+ jobs:
14
+ test:
15
+
16
+ strategy:
17
+ matrix:
18
+ os: [ubuntu-latest, macos-latest]
19
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
20
+ runs-on: ${{ matrix.os }}
21
+
22
+ steps:
23
+ - uses: actions/checkout@v2
24
+ - name: Set up Ruby
25
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
26
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby-version }}
30
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31
+ - name: Run tests
32
+ run: bundle exec rake
data/.travis.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2
3
+ - 2.3
4
+ - 2.6
4
5
  - jruby
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
- # Omniauth::MicrosoftGraph [![Build Status](https://travis-ci.org/synth/omniauth-microsoft_graph.svg?branch=master)](https://travis-ci.org/synth/omniauth-microsoft_graph)
1
+ # Omniauth::MicrosoftGraph ![ruby workflow](https://github.com/synth/omniauth-microsoft_graph/actions/workflows/ruby.yml/badge.svg)
2
+
2
3
 
3
4
  Microsoft Graph OAuth2 Strategy for OmniAuth.
4
5
  Can be used to authenticate with Office365 or other MS services, and get a token for the Microsoft Graph Api, formerly the Office365 Unified Api.
@@ -21,12 +22,56 @@ Or install it yourself as:
21
22
 
22
23
  ## Usage
23
24
 
25
+ Register a new app in the [Azure Portal / App registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) to get the `AZURE_APPLICATION_CLIENT_ID` and `AZURE_APPLICATION_CLIENT_SECRET` below.
26
+
27
+ #### Configuration
24
28
  ```ruby
25
29
  Rails.application.config.middleware.use OmniAuth::Builder do
26
30
  provider :microsoft_graph, ENV['AZURE_APPLICATION_CLIENT_ID'], ENV['AZURE_APPLICATION_CLIENT_SECRET']
27
31
  end
28
32
  ```
29
33
 
34
+ #### Login Hint
35
+ Just add `{login_hint: "email@example.com"}` to your url generation to form:
36
+ ```ruby
37
+ /auth/microsoft_graph?login_hint=email@example.com
38
+ ```
39
+
40
+ #### Domain Verification
41
+ Because Microsoft allows users to set vanity emails on their accounts, the value of the user's "email" doesn't establish membership in that domain. Put another way, user malicious@hacker.biz can edit their email in Active Directory to ceo@yourcompany.com, and (depending on your auth implementation) may be able to log in automatically as that user.
42
+
43
+ To establish membership in the claimed email domain, we use two strategies:
44
+
45
+ * `email` domain matches `userPrincipalName` domain (which by definition is a verified domain)
46
+ * The user's `id_token` includes the `xms_edov` ("Email Domain Ownership Verified") claim, with a truthy value
47
+
48
+ The `xms_edov` claim is [optional](https://github.com/MicrosoftDocs/azure-docs/issues/111425), and must be configured in the Azure console before it's available in the token. Refer to [Clerk's guide](https://clerk.com/docs/authentication/social-connections/microsoft#stay-secure-against-the-n-o-auth-vulnerability) for instructions on configuring the claim.
49
+
50
+ If you're not able or don't need to support domain verification, you can bypass for an individual domain:
51
+ ```ruby
52
+ Rails.application.config.middleware.use OmniAuth::Builder do
53
+ provider :microsoft_graph,
54
+ ENV['AZURE_APPLICATION_CLIENT_ID'],
55
+ ENV['AZURE_APPLICATION_CLIENT_SECRET'],
56
+ skip_domain_verification: %w[contoso.com]
57
+ end
58
+ ```
59
+
60
+ Or, you can disable domain verification entirely. We *strongly recommend* that you do *not* disable domain verification if at all possible.
61
+ ```ruby
62
+ Rails.application.config.middleware.use OmniAuth::Builder do
63
+ provider :microsoft_graph,
64
+ ENV['AZURE_APPLICATION_CLIENT_ID'],
65
+ ENV['AZURE_APPLICATION_CLIENT_SECRET'],
66
+ skip_domain_verification: true
67
+ end
68
+ ```
69
+
70
+ [nOAuth: How Microsoft OAuth Misconfiguration Can Lead to Full Account Takeover](https://www.descope.com/blog/post/noauth) from [Descope](https://www.descope.com/)
71
+
72
+ ### Upgrading to 1.0.0
73
+ This version requires OmniAuth v2. If you are using Rails, you will need to include or upgrade `omniauth-rails_csrf_protection`. If you upgrade and get an error in your logs complaining about "authenticity error" or similiar, make sure to do `bundle update omniauth-rails_csrf_protection`
74
+
30
75
  ## Contributing
31
76
 
32
77
  1. Fork it ( https://github.com/synth/omniauth-microsoft_graph/fork )
data/Rakefile CHANGED
@@ -1,10 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ # frozen_string_literal: true
3
2
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.test_files = FileList['test/*_test.rb']
7
- end
3
+ require File.join('bundler', 'gem_tasks')
4
+ require File.join('rspec', 'core', 'rake_task')
8
5
 
9
- task :default => :test
6
+ RSpec::Core::RakeTask.new(:spec)
10
7
 
8
+ task default: :spec
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+ require 'jwt' # for token signature validation
3
+ require 'omniauth' # to inherit from OmniAuth::Error
4
+ require 'oauth2' # to rescue OAuth2::Error
5
+
6
+ module OmniAuth
7
+ module MicrosoftGraph
8
+ # Verify user email domains to mitigate the nOAuth vulnerability
9
+ # https://www.descope.com/blog/post/noauth
10
+ # https://clerk.com/docs/authentication/social-connections/microsoft#stay-secure-against-the-n-o-auth-vulnerability
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'
13
+
14
+ class DomainVerificationError < OmniAuth::Error; end
15
+
16
+ class DomainVerifier
17
+ def self.verify!(auth_hash, access_token, options)
18
+ new(auth_hash, access_token, options).verify!
19
+ end
20
+
21
+ def initialize(auth_hash, access_token, options)
22
+ @email_domain = auth_hash['info']['email']&.split('@')&.last
23
+ @upn_domain = auth_hash['extra']['raw_info']['userPrincipalName']&.split('@')&.last
24
+ @access_token = access_token
25
+ @id_token = access_token.params['id_token']
26
+ @skip_verification = options[:skip_domain_verification]
27
+ end
28
+
29
+ def verify!
30
+ # The userPrincipalName property is mutable, but must always contain a
31
+ # verified domain:
32
+ #
33
+ # "The general format is alias@domain, where domain must be present in
34
+ # the tenant's collection of verified domains."
35
+ # https://learn.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0
36
+ #
37
+ # This means while it's not suitable for consistently identifying a user
38
+ # (the domain might change), it is suitable for verifying membership in
39
+ # a given domain.
40
+ return true if email_domain.casecmp?(upn_domain) ||
41
+ skip_verification == true ||
42
+ (skip_verification.is_a?(Array) && skip_verification.include?(email_domain)) ||
43
+ domain_verified_jwt_claim
44
+ raise DomainVerificationError, verification_error_message
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :access_token,
50
+ :email_domain,
51
+ :id_token,
52
+ :permitted_domains,
53
+ :skip_verification,
54
+ :upn_domain
55
+
56
+ # https://learn.microsoft.com/en-us/entra/identity-platform/optional-claims-reference
57
+ # Microsoft offers an optional claim `xms_edov` that will indicate whether the
58
+ # user's email domain is part of the organization's verified domains. This has to be
59
+ # explicitly configured in the app registration.
60
+ #
61
+ # To get to it, we need to decode the ID token with the key material from Microsoft's
62
+ # OIDC configuration endpoint, and inspect it for the claim in question.
63
+ def domain_verified_jwt_claim
64
+ oidc_config = access_token.get(OIDC_CONFIG_URL).parsed
65
+ algorithms = oidc_config['id_token_signing_alg_values_supported']
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)
74
+ # https://github.com/MicrosoftDocs/azure-docs/issues/111425#issuecomment-1761043378
75
+ # Comments seemed to indicate the value is not consistent
76
+ ['1', 1, 'true', true].include?(decoded_token.first['xms_edov'])
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)
85
+ end
86
+
87
+ def verification_error_message
88
+ <<~MSG
89
+ The email domain '#{email_domain}' is not a verified domain for this Azure AD account.
90
+ You can either:
91
+ * Update the user's email to match the principal domain '#{upn_domain}'
92
+ * Skip verification on the '#{email_domain}' domain (not recommended)
93
+ * Disable verification with `skip_domain_verification: true` (NOT RECOMMENDED!)
94
+ Refer to the README for more details.
95
+ MSG
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,5 @@
1
- module Omniauth
1
+ module OmniAuth
2
2
  module MicrosoftGraph
3
- VERSION = "0.2.1"
3
+ VERSION = "2.2.0"
4
4
  end
5
5
  end
@@ -1,2 +1,3 @@
1
+ require "omniauth/microsoft_graph/domain_verifier"
1
2
  require "omniauth/microsoft_graph/version"
2
3
  require "omniauth/strategies/microsoft_graph"
@@ -3,64 +3,152 @@ require 'omniauth-oauth2'
3
3
  module OmniAuth
4
4
  module Strategies
5
5
  class MicrosoftGraph < OmniAuth::Strategies::OAuth2
6
+ BASE_SCOPE_URL = 'https://graph.microsoft.com/'
7
+ BASE_SCOPES = %w[offline_access openid email profile].freeze
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'
11
+
6
12
  option :name, :microsoft_graph
7
13
 
8
14
  option :client_options, {
9
- site: 'https://login.microsoftonline.com',
10
- token_url: '/common/oauth2/v2.0/token',
11
- authorize_url: '/common/oauth2/v2.0/authorize'
15
+ site: 'https://login.microsoftonline.com/',
16
+ token_url: 'common/oauth2/v2.0/token',
17
+ authorize_url: 'common/oauth2/v2.0/authorize'
12
18
  }
13
19
 
14
- option :authorize_options, %i[display score auth_type scope prompt login_hint domain_hint response_mode]
20
+ option :authorize_options, %i[state callback_url access_type display score auth_type scope prompt login_hint domain_hint response_mode]
21
+
22
+ option :token_params, {
23
+ }
24
+
25
+ option :scope, DEFAULT_SCOPE
26
+ option :authorized_client_ids, []
27
+ option :skip_domain_verification, false
15
28
 
16
29
  uid { raw_info["id"] }
17
30
 
18
31
  info do
19
32
  {
20
- email: raw_info["mail"] || raw_info["userPrincipalName"],
21
- first_name: raw_info["givenName"],
22
- last_name: raw_info["surname"],
23
- name: full_name,
24
- nickname: raw_info["userPrincipalName"],
33
+ 'email' => raw_info["mail"],
34
+ 'first_name' => raw_info["givenName"],
35
+ 'last_name' => raw_info["surname"],
36
+ 'name' => [raw_info["givenName"], raw_info["surname"]].join(' '),
37
+ 'nickname' => raw_info["displayName"],
25
38
  }
26
39
  end
27
40
 
28
41
  extra do
29
42
  {
30
43
  'raw_info' => raw_info,
31
- 'params' => access_token.params
44
+ 'params' => access_token.params,
45
+ 'aud' => options.client_id
32
46
  }
33
47
  end
34
-
35
- def callback_url
36
- options[:redirect_uri] || (full_host + script_name + callback_path)
37
- end
38
48
 
39
- def raw_info
40
- @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me').parsed
49
+ def auth_hash
50
+ super.tap do |ah|
51
+ verify_email(ah, access_token)
52
+ end
41
53
  end
42
54
 
43
55
  def authorize_params
44
56
  super.tap do |params|
45
- %w[display score auth_type].each do |v|
46
- if request.params[v]
47
- params[v.to_sym] = request.params[v]
48
- end
57
+ options[:authorize_options].each do |k|
58
+ params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
49
59
  end
60
+
61
+ params[:scope] = get_scope(params)
62
+ params[:access_type] = 'offline' if params[:access_type].nil?
63
+
64
+ session['omniauth.state'] = params[:state] if params[:state]
50
65
  end
51
66
  end
52
67
 
53
- def full_name
54
- raw_info["displayName"].presence || raw_info.values_at("givenName", "surname").compact.join(' ')
68
+ def raw_info
69
+ @raw_info ||= access_token.get(profile_endpoint).parsed
55
70
  end
56
71
 
57
- def build_access_token
58
- if request.params['access_token']
59
- ::OAuth2::AccessToken.from_hash(client, request.params.dup)
72
+ def callback_url
73
+ options[:callback_url] || full_host + script_name + callback_path
74
+ end
75
+
76
+ def custom_build_access_token
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)
80
+ access_token
81
+ end
82
+
83
+ alias build_access_token custom_build_access_token
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
60
94
  else
61
- super
95
+ MICROSOFT_GRAPH_PROFILE_URL
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def get_access_token(request)
102
+ verifier = request.params['code']
103
+ redirect_uri = request.params['redirect_uri'] || request.params['callback_url']
104
+ if verifier && request.xhr?
105
+ client_get_token(verifier, redirect_uri || '/auth/microsoft_graph/callback')
106
+ elsif verifier
107
+ client_get_token(verifier, redirect_uri || callback_url)
108
+ elsif verify_token(request.params['access_token'])
109
+ ::OAuth2::AccessToken.from_hash(client, request.params.dup)
110
+ elsif request.content_type =~ /json/i
111
+ begin
112
+ body = JSON.parse(request.body.read)
113
+ request.body.rewind # rewind request body for downstream middlewares
114
+ verifier = body && body['code']
115
+ client_get_token(verifier, '/auth/microsoft_graph/callback') if verifier
116
+ rescue JSON::ParserError => e
117
+ warn "[omniauth microsoft_graph] JSON parse error=#{e}"
118
+ end
62
119
  end
63
120
  end
121
+
122
+ def client_get_token(verifier, redirect_uri)
123
+ client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params)
124
+ end
125
+
126
+ def get_token_params
127
+ deep_symbolize(options.auth_token_params || {})
128
+ end
129
+
130
+ def get_token_options(redirect_uri = '')
131
+ { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true))
132
+ end
133
+
134
+ def get_scope(params)
135
+ raw_scope = params[:scope] || DEFAULT_SCOPE
136
+ scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten
137
+ scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" }
138
+ scope_list.join(' ')
139
+ end
140
+
141
+ def verify_token(access_token)
142
+ return false unless access_token
143
+ # access_token.get('https://graph.microsoft.com/v1.0/me').parsed
144
+ raw_response = client.request(:get, 'https://graph.microsoft.com/v1.0/me',
145
+ params: { access_token: access_token }).parsed
146
+ (raw_response['aud'] == options.client_id) || options.authorized_client_ids.include?(raw_response['aud'])
147
+ end
148
+
149
+ def verify_email(auth_hash, access_token)
150
+ OmniAuth::MicrosoftGraph::DomainVerifier.verify!(auth_hash, access_token, options)
151
+ end
64
152
  end
65
153
  end
66
154
  end
@@ -5,7 +5,7 @@ require 'omniauth/microsoft_graph/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "omniauth-microsoft_graph"
8
- spec.version = Omniauth::MicrosoftGraph::VERSION
8
+ spec.version = OmniAuth::MicrosoftGraph::VERSION
9
9
  spec.authors = ["Peter Philips", "Joel Van Horn"]
10
10
  spec.email = ["pete@p373.net", "joel@joelvanhorn.com"]
11
11
  spec.summary = %q{omniauth provider for Microsoft Graph}
@@ -18,10 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency "omniauth-oauth2"
22
-
23
- spec.add_development_dependency "sinatra"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "minitest"
26
- spec.add_development_dependency "mocha"
21
+ spec.add_runtime_dependency 'jwt', '>= 2.0', '< 4.0'
22
+ spec.add_runtime_dependency 'omniauth', '~> 2.0'
23
+ spec.add_runtime_dependency 'omniauth-oauth2', '~> 1.8.0'
24
+ spec.add_development_dependency "sinatra", '~> 4.1'
25
+ spec.add_development_dependency "rake", '~> 12.3.3', '>= 12.3.3'
26
+ spec.add_development_dependency 'rspec', '~> 3.6'
27
+ spec.add_development_dependency "mocha", '~> 0'
27
28
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'omniauth/microsoft_graph/domain_verifier'
5
+
6
+ RSpec.describe OmniAuth::MicrosoftGraph::DomainVerifier do
7
+ subject(:verifier) { described_class.new(auth_hash, access_token, options) }
8
+
9
+ let(:auth_hash) do
10
+ {
11
+ 'info' => { 'email' => email },
12
+ 'extra' => { 'raw_info' => { 'userPrincipalName' => upn } }
13
+ }
14
+ end
15
+ let(:email) { 'foo@example.com' }
16
+ let(:upn) { 'bar@hackerman.biz' }
17
+ let(:options) { { skip_domain_verification: false } }
18
+ let(:access_token) { double('OAuth2::AccessToken', params: { 'id_token' => id_token }) }
19
+ let(:id_token) { nil }
20
+
21
+ describe '#verify!' do
22
+ subject(:result) { verifier.verify! }
23
+
24
+ context 'when email domain and userPrincipalName domain match' do
25
+ let(:email) { 'foo@example.com' }
26
+ let(:upn) { 'bar@example.com' }
27
+
28
+ it { is_expected.to be_truthy }
29
+ end
30
+
31
+ context 'when email domain and userPrincipalName domain match but have different casing' do
32
+ let(:email) { 'foo@example.com' }
33
+ let(:upn) { 'bar@EXAMPLE.COM' }
34
+
35
+ it { is_expected.to be_truthy }
36
+ end
37
+
38
+ context 'when domain validation is disabled' do
39
+ let(:options) { super().merge(skip_domain_verification: true) }
40
+
41
+ it { is_expected.to be_truthy }
42
+ end
43
+
44
+ context 'when the email domain is explicitly permitted' do
45
+ let(:options) { super().merge(skip_domain_verification: ['example.com']) }
46
+
47
+ it { is_expected.to be_truthy }
48
+ end
49
+
50
+ context 'when the ID token indicates domain verification' do
51
+ let(:mock_oidc_key) do
52
+ optional_parameters = { kid: 'mock_oidc_key', use: 'sig', alg: 'RS256' }
53
+ JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
54
+ end
55
+
56
+ let(:mock_common_key) do
57
+ optional_parameters = { kid: 'mock_common_key', use: 'sig', alg: 'RS256' }
58
+ JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
59
+ end
60
+
61
+ # Mock the API responses to return the mock keys
62
+ before do
63
+ allow(access_token).to receive(:get)
64
+ .with(OmniAuth::MicrosoftGraph::OIDC_CONFIG_URL)
65
+ .and_return(
66
+ double(
67
+ 'OAuth2::Response',
68
+ parsed: {
69
+ 'id_token_signing_alg_values_supported' => ['RS256'],
70
+ 'jwks_uri' => 'https://example.com/jwks-keys',
71
+ }
72
+ )
73
+ )
74
+ allow(access_token).to receive(:get)
75
+ .with('https://example.com/jwks-keys')
76
+ .and_return(
77
+ double(
78
+ 'OAuth2::Response',
79
+ parsed: JWT::JWK::Set.new(mock_oidc_key).export
80
+ )
81
+ )
82
+ allow(access_token).to receive(:get)
83
+ .with(OmniAuth::MicrosoftGraph::COMMON_JWKS_URL)
84
+ .and_return(
85
+ double(
86
+ 'OAuth2::Response',
87
+ parsed: JWT::JWK::Set.new(mock_common_key).export,
88
+ body: JWT::JWK::Set.new(mock_common_key).export.to_json
89
+ )
90
+ )
91
+ end
92
+
93
+ context 'when the kid exists in the oidc key' do
94
+ let(:id_token) do
95
+ payload = { email: email, xms_edov: true }
96
+ JWT.encode(payload, mock_oidc_key.signing_key, mock_oidc_key[:alg], kid: mock_oidc_key[:kid])
97
+ end
98
+
99
+ it { is_expected.to be_truthy }
100
+ end
101
+
102
+ context "when the kid exists in the common key" do
103
+ let(:id_token) do
104
+ payload = { email: email, xms_edov: true }
105
+ JWT.encode(payload, mock_common_key.signing_key, mock_common_key[:alg], kid: mock_common_key[:kid])
106
+ end
107
+
108
+ it { is_expected.to be_truthy }
109
+ end
110
+ end
111
+
112
+ context 'when all verification strategies fail' do
113
+ before { allow(access_token).to receive(:get).and_raise(::OAuth2::Error.new('whoops')) }
114
+
115
+ it 'raises a DomainVerificationError' do
116
+ expect { result }.to raise_error OmniAuth::MicrosoftGraph::DomainVerificationError
117
+ end
118
+ end
119
+ end
120
+ end