omniauth-microsoft-identity2 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3af5a6cd9f8f7a4ad5c932febe95005653242fc9bd5b4a6a3b590bccded1c3da
4
+ data.tar.gz: 22c78044a0b051300b7dc5a26bf9d57e3389778a565ef15c84e66a645750e8e7
5
+ SHA512:
6
+ metadata.gz: 98c87218aab6c1dd088145d9974d5636f37ec0e4f3585195dc6a5af535435ea2a9b7eb3ca17c63aad90c2c09e04e2cfadb849cf0ee3a6f7c499a5dd9a7f2d928
7
+ data.tar.gz: b7a4ab85d4153062b4bbe3b4c25764e224fd6a33bf4cd5a42d66a39c00154a4076aff2a449bb6f5f4a4f1daff33f94c1c57a93047d576546365f6d6881762998
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claudio Poli
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # OmniAuth MicrosoftIdentity2 Strategy
2
+
3
+ [![Test](https://github.com/icoretech/omniauth-microsoft-identity2/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/icoretech/omniauth-microsoft-identity2/actions/workflows/test.yml?query=branch%3Amain)
4
+ [![Gem Version](https://img.shields.io/gem/v/omniauth-microsoft-identity2.svg)](https://rubygems.org/gems/omniauth-microsoft-identity2)
5
+
6
+ `omniauth-microsoft-identity2` provides a Microsoft Identity (Entra ID) OAuth2/OpenID Connect strategy for OmniAuth.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'omniauth-microsoft-identity2'
14
+ ```
15
+
16
+ Then run:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Configure OmniAuth in your Rack/Rails app:
25
+
26
+ ```ruby
27
+ Rails.application.config.middleware.use OmniAuth::Builder do
28
+ provider :microsoft_identity2,
29
+ ENV.fetch('MICROSOFT_CLIENT_ID'),
30
+ ENV.fetch('MICROSOFT_CLIENT_SECRET')
31
+ end
32
+ ```
33
+
34
+ Compatibility aliases are available if you want stable callback paths:
35
+
36
+ ```ruby
37
+ provider :microsoft_identity, ENV.fetch('MICROSOFT_CLIENT_ID'), ENV.fetch('MICROSOFT_CLIENT_SECRET')
38
+ provider :windowslive, ENV.fetch('MICROSOFT_CLIENT_ID'), ENV.fetch('MICROSOFT_CLIENT_SECRET')
39
+ ```
40
+
41
+ Microsoft app setup:
42
+ - [Microsoft identity platform OAuth 2.0 authorization code flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow)
43
+ - [OpenID Connect on the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc)
44
+
45
+ ## Options
46
+
47
+ Supported options include:
48
+ - `tenant` (default: `common`)
49
+ - `scope` (default: `openid profile email offline_access User.Read`)
50
+ - `prompt`
51
+ - `login_hint`
52
+ - `domain_hint`
53
+ - `response_mode`
54
+ - `redirect_uri`
55
+ - `nonce`
56
+ - `uid_with_tenant` (default: `true`; yields `tid:oid_or_sub` when possible)
57
+ - `skip_jwt` (default: `false`; disable id_token decode in `extra.id_info`)
58
+
59
+ Request query parameters for supported authorize options are passed through in request phase.
60
+
61
+ ## Auth Hash
62
+
63
+ Example payload from `request.env['omniauth.auth']` (realistic shape, anonymized):
64
+
65
+ ```json
66
+ {
67
+ "uid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:11111111-2222-3333-4444-555555555555",
68
+ "info": {
69
+ "name": "Sample User",
70
+ "email": "sample@example.test",
71
+ "first_name": "Sample",
72
+ "last_name": "User",
73
+ "nickname": "sample@example.test"
74
+ },
75
+ "credentials": {
76
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
77
+ "refresh_token": "0.AXwA...",
78
+ "expires_at": 1772691847,
79
+ "expires": true,
80
+ "scope": "openid profile email offline_access User.Read"
81
+ },
82
+ "extra": {
83
+ "raw_info": {
84
+ "tid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
85
+ "oid": "11111111-2222-3333-4444-555555555555",
86
+ "sub": "aaaaaaaaaaaaaaaaaaaa",
87
+ "name": "Sample User",
88
+ "given_name": "Sample",
89
+ "family_name": "User",
90
+ "preferred_username": "sample@example.test",
91
+ "email": "sample@example.test"
92
+ },
93
+ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I...redacted...",
94
+ "id_info": {
95
+ "tid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
96
+ "oid": "11111111-2222-3333-4444-555555555555",
97
+ "sub": "aaaaaaaaaaaaaaaaaaaa",
98
+ "name": "Sample User",
99
+ "preferred_username": "sample@example.test",
100
+ "iat": 1772689518,
101
+ "exp": 1772693118
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Endpoints
108
+
109
+ This gem uses Microsoft Identity v2 endpoints and Microsoft Graph user info endpoints:
110
+ - `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize`
111
+ - `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token`
112
+ - `https://graph.microsoft.com/oidc/userinfo`
113
+ - fallback: `https://graph.microsoft.com/v1.0/me`
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ bundle install
119
+ bundle exec rake
120
+ ```
121
+
122
+ Run Rails integration tests with an explicit Rails version:
123
+
124
+ ```bash
125
+ RAILS_VERSION='~> 8.1.0' bundle install
126
+ RAILS_VERSION='~> 8.1.0' bundle exec rake test_rails_integration
127
+ ```
128
+
129
+ ## Compatibility
130
+
131
+ - Ruby: `>= 3.2` (tested on `3.2`, `3.3`, `3.4`, `4.0`)
132
+ - `omniauth-oauth2`: `>= 1.8`, `< 1.9`
133
+ - Rails integration lanes: `~> 7.1.0`, `~> 7.2.0`, `~> 8.0.0`, `~> 8.1.0`
134
+
135
+ ## Release
136
+
137
+ Tag releases as `vX.Y.Z`; GitHub Actions publishes the gem to RubyGems.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAuth
4
+ module MicrosoftIdentity2
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omniauth/microsoft_identity2/version'
4
+ require 'omniauth/strategies/microsoft_identity2'
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'omniauth-oauth2'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ # OmniAuth strategy for Microsoft Identity (Entra ID) OAuth2/OpenID Connect.
9
+ class MicrosoftIdentity2 < OmniAuth::Strategies::OAuth2
10
+ BASE_URL = 'https://login.microsoftonline.com'
11
+ DEFAULT_SCOPE = 'openid profile email offline_access User.Read'
12
+ USER_INFO_URL = 'https://graph.microsoft.com/oidc/userinfo'
13
+ GRAPH_ME_URL = 'https://graph.microsoft.com/v1.0/me'
14
+
15
+ option :name, 'microsoft_identity2'
16
+ option :authorize_options, %i[scope state prompt login_hint domain_hint response_mode redirect_uri nonce]
17
+ option :tenant, 'common'
18
+ option :base_url, BASE_URL
19
+ option :scope, DEFAULT_SCOPE
20
+ option :skip_jwt, false
21
+ option :uid_with_tenant, true
22
+
23
+ option :client_options,
24
+ site: BASE_URL,
25
+ authorize_url: 'common/oauth2/v2.0/authorize',
26
+ token_url: 'common/oauth2/v2.0/token',
27
+ connection_opts: {
28
+ headers: {
29
+ user_agent: 'icoretech-omniauth-microsoft-identity2 gem',
30
+ accept: 'application/json',
31
+ content_type: 'application/x-www-form-urlencoded'
32
+ }
33
+ }
34
+
35
+ uid do
36
+ oid_or_sub = raw_info['oid'] || raw_info['sub'] || raw_info['id']
37
+ tid = raw_info['tid']
38
+
39
+ if options[:uid_with_tenant] && present?(tid) && present?(oid_or_sub)
40
+ "#{tid}:#{oid_or_sub}"
41
+ else
42
+ oid_or_sub.to_s
43
+ end
44
+ end
45
+
46
+ info do
47
+ email = raw_info['email'] || raw_info['preferred_username'] || raw_info['upn'] || raw_info['mail']
48
+ {
49
+ name: raw_info['name'],
50
+ email: email,
51
+ first_name: raw_info['given_name'],
52
+ last_name: raw_info['family_name'],
53
+ nickname: raw_info['preferred_username'] || raw_info['upn'] || email || raw_info['sub'],
54
+ image: raw_info['picture']
55
+ }.compact
56
+ end
57
+
58
+ credentials do
59
+ {
60
+ 'token' => access_token.token,
61
+ 'refresh_token' => access_token.refresh_token,
62
+ 'expires_at' => access_token.expires_at,
63
+ 'expires' => access_token.expires?,
64
+ 'scope' => token_scope
65
+ }.compact
66
+ end
67
+
68
+ extra do
69
+ data = { 'raw_info' => raw_info }
70
+ id_token = raw_id_token
71
+ if present?(id_token)
72
+ data['id_token'] = id_token
73
+ decoded = decoded_id_token
74
+ data['id_info'] = decoded if decoded
75
+ end
76
+ data
77
+ end
78
+
79
+ def client
80
+ configure_tenant_client_urls
81
+ super
82
+ end
83
+
84
+ def authorize_params
85
+ super.tap do |params|
86
+ apply_request_authorize_overrides(params)
87
+ params[:scope] = normalize_scope(params[:scope] || options[:scope])
88
+ persist_authorize_state(params)
89
+ end
90
+ end
91
+
92
+ def raw_info
93
+ @raw_info ||= begin
94
+ claims = {}
95
+ decoded = decoded_id_token
96
+ claims.merge!(decoded) if decoded
97
+ claims.merge!(fetch_user_info)
98
+ claims
99
+ end
100
+ end
101
+
102
+ # Ensure token exchange uses a stable callback URI that matches provider config.
103
+ def callback_url
104
+ options[:callback_url] || options[:redirect_uri] || super
105
+ end
106
+
107
+ # Prevent authorization response params from being appended to redirect_uri.
108
+ def query_string
109
+ return '' if request.params['code']
110
+
111
+ super
112
+ end
113
+
114
+ private
115
+
116
+ def fetch_user_info
117
+ normalize_user_info(access_token.get(USER_INFO_URL).parsed)
118
+ rescue StandardError
119
+ begin
120
+ normalize_user_info(access_token.get(GRAPH_ME_URL).parsed)
121
+ rescue StandardError
122
+ {}
123
+ end
124
+ end
125
+
126
+ def normalize_user_info(payload)
127
+ return {} unless payload.is_a?(Hash)
128
+
129
+ return payload unless graph_profile_payload?(payload)
130
+
131
+ upn = payload['userPrincipalName']
132
+ {
133
+ 'sub' => payload['id'],
134
+ 'oid' => payload['id'],
135
+ 'name' => payload['displayName'],
136
+ 'given_name' => payload['givenName'],
137
+ 'family_name' => payload['surname'],
138
+ 'email' => payload['mail'] || upn,
139
+ 'preferred_username' => upn,
140
+ 'upn' => upn
141
+ }.merge(payload).compact
142
+ end
143
+
144
+ def normalize_scope(raw_scope)
145
+ raw_scope.to_s.split(/[\s,]+/).reject(&:empty?).uniq.join(' ')
146
+ end
147
+
148
+ def apply_request_authorize_overrides(params)
149
+ options[:authorize_options].each do |key|
150
+ request_value = request.params[key.to_s]
151
+ params[key] = request_value unless blank?(request_value)
152
+ end
153
+ end
154
+
155
+ def graph_profile_payload?(payload)
156
+ payload.key?('displayName') || payload.key?('userPrincipalName')
157
+ end
158
+
159
+ def configure_tenant_client_urls
160
+ tenant = options[:tenant].to_s.strip
161
+ tenant = 'common' if tenant.empty?
162
+
163
+ base_url = options[:base_url].to_s.strip
164
+ base_url = BASE_URL if base_url.empty?
165
+
166
+ options.client_options.authorize_url = "#{base_url}/#{tenant}/oauth2/v2.0/authorize"
167
+ options.client_options.token_url = "#{base_url}/#{tenant}/oauth2/v2.0/token"
168
+ end
169
+
170
+ def persist_authorize_state(params)
171
+ session['omniauth.state'] = params[:state] if params[:state]
172
+ end
173
+
174
+ def token_scope
175
+ access_token.params['scope'] || access_token['scope']
176
+ end
177
+
178
+ def raw_id_token
179
+ params = access_token.respond_to?(:params) ? access_token.params : {}
180
+ params['id_token'] || access_token['id_token']
181
+ end
182
+
183
+ def decoded_id_token
184
+ return nil if options[:skip_jwt]
185
+
186
+ token = raw_id_token
187
+ return nil unless present?(token)
188
+
189
+ payload, = JWT.decode(token, nil, false)
190
+ payload
191
+ rescue JWT::DecodeError
192
+ nil
193
+ end
194
+
195
+ def blank?(value)
196
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
197
+ end
198
+
199
+ def present?(value)
200
+ !blank?(value)
201
+ end
202
+ end
203
+
204
+ # Backward-compatible strategy name for existing callback paths.
205
+ class MicrosoftIdentity < MicrosoftIdentity2
206
+ option :name, 'microsoft_identity'
207
+ end
208
+
209
+ # Compatibility alias for legacy windowslive callback paths.
210
+ class Windowslive < MicrosoftIdentity2
211
+ option :name, 'windowslive'
212
+ end
213
+ end
214
+ end
215
+
216
+ OmniAuth.config.add_camelization 'microsoft_identity2', 'MicrosoftIdentity2'
217
+ OmniAuth.config.add_camelization 'microsoft_identity', 'MicrosoftIdentity'
218
+ OmniAuth.config.add_camelization 'windowslive', 'Windowslive'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omniauth/microsoft_identity2'
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'omniauth/microsoft_identity2/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'omniauth-microsoft-identity2'
9
+ spec.version = OmniAuth::MicrosoftIdentity2::VERSION
10
+ spec.authors = ['Claudio Poli']
11
+ spec.email = ['masterkain@gmail.com']
12
+
13
+ spec.summary = 'OmniAuth strategy for Microsoft Identity (Entra ID) OAuth2/OpenID Connect authentication.'
14
+ spec.description =
15
+ 'OAuth2/OpenID Connect strategy for OmniAuth that authenticates users ' \
16
+ 'with Microsoft Identity and exposes profile metadata.'
17
+ spec.homepage = 'https://github.com/icoretech/omniauth-microsoft-identity2'
18
+ spec.license = 'MIT'
19
+ spec.required_ruby_version = '>= 3.2'
20
+
21
+ spec.metadata['source_code_uri'] = 'https://github.com/icoretech/omniauth-microsoft-identity2'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/icoretech/omniauth-microsoft-identity2/issues'
23
+ spec.metadata['changelog_uri'] = 'https://github.com/icoretech/omniauth-microsoft-identity2/releases'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir[
27
+ 'lib/**/*.rb',
28
+ 'README*',
29
+ 'LICENSE*',
30
+ '*.gemspec'
31
+ ]
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'cgi', '>= 0.3.6'
35
+ spec.add_dependency 'jwt', '>= 2.9.2'
36
+ spec.add_dependency 'omniauth-oauth2', '>= 1.8', '< 1.9'
37
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-microsoft-identity2
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Claudio Poli
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: cgi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.3.6
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.3.6
26
+ - !ruby/object:Gem::Dependency
27
+ name: jwt
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.9.2
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 2.9.2
40
+ - !ruby/object:Gem::Dependency
41
+ name: omniauth-oauth2
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.8'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.9'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '1.8'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.9'
60
+ description: OAuth2/OpenID Connect strategy for OmniAuth that authenticates users
61
+ with Microsoft Identity and exposes profile metadata.
62
+ email:
63
+ - masterkain@gmail.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - LICENSE.txt
69
+ - README.md
70
+ - lib/omniauth-microsoft-identity2.rb
71
+ - lib/omniauth/microsoft_identity2.rb
72
+ - lib/omniauth/microsoft_identity2/version.rb
73
+ - lib/omniauth/strategies/microsoft_identity2.rb
74
+ - omniauth-microsoft-identity2.gemspec
75
+ homepage: https://github.com/icoretech/omniauth-microsoft-identity2
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ source_code_uri: https://github.com/icoretech/omniauth-microsoft-identity2
80
+ bug_tracker_uri: https://github.com/icoretech/omniauth-microsoft-identity2/issues
81
+ changelog_uri: https://github.com/icoretech/omniauth-microsoft-identity2/releases
82
+ rubygems_mfa_required: 'true'
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '3.2'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.6.9
98
+ specification_version: 4
99
+ summary: OmniAuth strategy for Microsoft Identity (Entra ID) OAuth2/OpenID Connect
100
+ authentication.
101
+ test_files: []