omniauth-entra-id 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12d51e6e7781fc58e98afb3631c673ffe033f6b606ddb549df20e8baaae1c5af
4
+ data.tar.gz: 8b507bae0a8bbf2e72601e3e3be9c9d0513977528988ee04f9f70b5a8ce1672d
5
+ SHA512:
6
+ metadata.gz: 3595a1f659a7429216de6f00a39949b4a70655e4505bf6d126e9ea158569b977f0a7af8ebddb3df7791f89e85ad1ca35b53d926fc8bc6727d5477a06b20fc18a
7
+ data.tar.gz: c090da890e3c2c2edd5a4f36e922f5eadae2caf498c5cd9ba398eaba6c62c2ef055ff4cf4da487dc353cf79438abd8004c2c6cb09e9fe8024184c51b2e77642d
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Change Log
2
+
3
+ ## v3.0.0 (2024-10-22)
4
+
5
+ * To upgrade from the Azure ActiveDirectory V2 gem, please see [`UPGRADING.md`](UPGRADING.md)
6
+ * Branched from `omniauth-azure-activedirectory-v2` version 2.4.0 and renamed to `omniauth-entra-id`
7
+ * Can specify `tenant_name` in options via #31 (thanks to @Jureamer) for B2C login
8
+ * Supports authenticating with a certificate instead of client secret via #32 (thanks to @juliaducey)
9
+ * ID token extraction and validation is improved; long-standing fault with UID generation from OIDs (see #33) addressed via #34 (thanks to @tom-brouwer-bex)
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at dev@ripaglobal.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in omniauth-entra-id.gemspec
4
+ #
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 RIPA Global
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,229 @@
1
+ # OmniAuth::Entra::Id
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/omniauth-entra-id.svg)](https://rubygems.org/gems/omniauth-entra-id)
4
+ [![Build Status](https://github.com/RIPAGlobal/omniauth-entra-id/actions/workflows/master.yml/badge.svg)](https://github.com/RIPAGlobal/omniauth-entra-id/actions)
5
+ [![License](https://img.shields.io/github/license/RIPAGlobal/omniauth-entra-id.svg)](LICENSE.txt)
6
+
7
+ OAuth 2 authentication with [Entra ID API](https://learn.microsoft.com/en-us/entra/identity-platform/v2-overview). Rationale:
8
+
9
+ * https://github.com/marknadig/omniauth-azure-oauth2 is no longer maintained.
10
+ * https://github.com/marknadig/omniauth-azure-oauth2/pull/29 contains important additions.
11
+
12
+ This gem combines the two and makes some changes to support the Entra API. The old ActiveDirectory V1 API used OpenID Connect. If you need this, a gem from Microsoft [is available here](https://github.com/AzureAD/omniauth-azure-activedirectory), but seems to be abandoned.
13
+
14
+ **If upgrading from older versions of this gem under its old name of "Azure ActiveDirectory V2", please follow the instructions in [`UPGRADING.md`](UPGRADING.md).**
15
+
16
+
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'omniauth-entra-id'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ ```shell
29
+ $ bundle install
30
+ ```
31
+
32
+ Or install it yourself as:
33
+
34
+ ```shell
35
+ $ gem install omniauth-entra-id
36
+ ```
37
+
38
+
39
+
40
+ ## Usage
41
+
42
+ Please start by reading https://github.com/marknadig/omniauth-azure-oauth2 for basic configuration and background information. Note that with this gem, you must use strategy name `entra_id` rather than `azure_oauth2`. Additional configuration information is given below.
43
+
44
+ ### Entra ID server configuration
45
+
46
+ In most cases, you only want to receive 'verified' email addresses in your application. For older app registrations in the Entra portal, this may need to be [enabled explicitly](https://learn.microsoft.com/en-us/graph/applications-authenticationbehaviors?tabs=http#prevent-the-issuance-of-email-claims-with-unverified-domain-owners). It's [enabled by default](https://learn.microsoft.com/en-us/entra/identity-platform/migrate-off-email-claim-authorization#how-do-i-protect-my-application-immediately) for new multi-tenant app registrations made after June 2023.
47
+
48
+ ### Implementation
49
+ #### With `OmniAuth::Builder`
50
+
51
+ You can do something like this for a static / fixed configuration:
52
+
53
+ ```ruby
54
+ Rails.application.config.middleware.use OmniAuth::Builder do
55
+ provider(
56
+ :entra_id,
57
+ {
58
+ client_id: ENV['ENTRA_CLIENT_ID'],
59
+ client_secret: ENV['ENTRA_CLIENT_SECRET']
60
+ }
61
+ )
62
+ end
63
+ ```
64
+
65
+ ...or, if using a custom provider class (called `YouTenantProvider` in this example, described in more detail later):
66
+
67
+ ```ruby
68
+ Rails.application.config.middleware.use OmniAuth::Builder do
69
+ provider(
70
+ :entra_id,
71
+ YouTenantProvider
72
+ )
73
+ end
74
+ ```
75
+
76
+ #### With Devise
77
+
78
+ In your `config/initializers/devise.rb` file you can do something like this for a static / fixed configuration:
79
+
80
+ ```ruby
81
+ config.omniauth(
82
+ :entra_id,
83
+ {
84
+ client_id: ENV['ENTRA_CLIENT_ID'],
85
+ client_secret: ENV['ENTRA_CLIENT_SECRET']
86
+ }
87
+ )
88
+ ```
89
+
90
+ ...or, if using a custom provider class (called `YouTenantProvider` in this example, described in more detail later):
91
+
92
+ ```ruby
93
+ config.omniauth(
94
+ :entra_id,
95
+ YouTenantProvider
96
+ )
97
+ ```
98
+
99
+ ### Configuration options
100
+
101
+ All of the items listed below are optional, unless noted otherwise. They can be provided either in a static configuration Hash as shown in examples above, or via *read accessor instance methods* in a provider class (more on this later).
102
+
103
+ To have your application authenticate with Entra via a client secret, specify `client_secret`. If you instead want to use certificate-based authentication via client assertion, give the `certificate_path` and `tenant_id` instead. You should provide only `client_secret` or `certificate_path`, not both.
104
+
105
+ If you're using the client assertion flow, you need to register your certificate in the Entra portal. For more information, please see [the documentation](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials).
106
+
107
+ | Option | Use |
108
+ | ------ | --- |
109
+ | `client_id` | **Mandatory.** Client ID for the 'application' (integration) configured on the Entra side. Found via the Entra UI. |
110
+ | `client_secret` | **Mandatory for client secret flow.** Client secret for the 'application' (integration) configured on the Entra side. Found via the Entra UI. Don't give this if using client assertion flow. |
111
+ | `certificate_path` | **Mandatory for client assertion flow.** Don't give this if using a client secret instead of client assertion. This should be the filepath to a PKCS#12 file. |
112
+ | `tenant_id` | **Mandatory for client assertion flow.** Entra Tenant ID for multi-tenanted use. Default is `common`. Forms part of the Entra OAuth URL - `{base}/{tenant_id}/oauth2/v2.0/...` |
113
+ | `base_url` | Location of Entra login page, for specialised requirements; default is `OmniAuth::Strategies::EntraId::BASE_URL` (at the time of writing, this is `https://login.microsoftonline.com`). |
114
+ | `tenant_name` | For what is currently known by its old name of "Azure ActiveDirectory B2C" (and only active if `custom_policy` is also provided - see below), set the tenancy name to constructs the correct B2C endpoint of `{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{custom_policy>}" and uses that for auth calls. This is a convenience feature; the `base_entra_url` option could also be manually built up in the same way. |
115
+ | `custom_policy` | Custom policy. Default is nil. Used in conjunction with `tenant_name`- see above. |
116
+ | `authorize_params` | Additional parameters passed as URL query data in the initial OAuth redirection to Microsoft. See below for more. Empty Hash default. |
117
+ | `domain_hint` | If defined, sets (overwriting, if already present) `domain_hint` inside `authorize_params`. Default `nil` / none. |
118
+ | `scope` | If defined, sets (overwriting, if already present) `scope` inside `authorize_params`. Default is `OmniAuth::Strategies::EntraId::DEFAULT_SCOPE` (at the time of writing, this is `'openid profile email'`). |
119
+ | `adfs` | If defined, modifies the URLs so they work with an on premise ADFS server. In order to use this you also need to set the `base_url` correctly and fill the `tenant_id` with `'adfs'`. |
120
+
121
+ In addition, as a special case, if the request URL contains a query parameter `prompt`, then this will be written into `authorize_params` under that key, overwriting if present any other value there. Note that this comes from the current request URL at the time OAuth flow is commencing, _not_ via static options Hash data or via a custom provider class - but you _could_ just as easily set `scope` inside a custom `authorize_params` returned from a provider class, as shown in an example later; the request URL query mechanism is just another way of doing the same thing.
122
+
123
+ #### Explaining `custom_policy` and `tenant_name`
124
+
125
+ When using Azure ActiveDirectory B2C - which seems to be distinct from Entra ID and not renamed as of October 2024 - tenants can define custom policies. With normal OAuth use cases, when the underlying `oauth2` gem creates the request for getting a token via POST, it places all `params` (which would include anything you've provided in the normal configuration to name your custom policy) in the `body` of the request. This would not work. Microsoft's documentation indicates that when [requesting a token](https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#request-a-token), they want the name of custom policies to be given in the URL rather than in the body of the request. They ignore a custom policy specified in the body.
126
+
127
+ Solve this for B2C use cases by giving your tenant name and custom policy name in the relevant configuration options. This causes a base URL to be constructed as follows:
128
+
129
+ ```
130
+ <tenant-name>.b2clogin.com/<tenant-name>.onmicrosoft.com/<policy-name>/oauth2/v2.0/...
131
+ ```
132
+
133
+ #### Explaining `authorize_params`
134
+
135
+ The `authorize_params` hash-like object contains key-value pairs which are added to existing standard OAuth data in the initial `POST` request made by this gem from your web site, to the Microsoft Entra login page, at the start of OAuth flow. You can find these listed some way down the table just below an OAuth URL example at:
136
+
137
+ * https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-authorization-code
138
+
139
+ ...looking for in particular items from `prompt` onwards. For example, Microsoft say that a prompt option of `select_account` will always lead to the account selection UI to be shown at login, whether or not the user is currently signed into a Microsoft Entra ID account in that browser session. You would active it using options that look something like this in your OmniAuth Builder or Devise setup code:
140
+
141
+ ```ruby
142
+ {
143
+ client_id: ENV['ENTRA_CLIENT_ID'],
144
+ client_secret: ENV['ENTRA_CLIENT_SECRET']
145
+ authorize_params: {
146
+ prompt: 'select_account'
147
+ }
148
+ }
149
+ ```
150
+
151
+
152
+
153
+ #### Dynamic options via a custom provider class
154
+
155
+ Documentation mentioned earlier at https://github.com/marknadig/omniauth-azure-oauth2#usage gives an example of setting tenant ID dynamically via a custom provider class. We can also use that class in other ways. For example, let's rewrite it thus:
156
+
157
+ ```ruby
158
+ class YouTenantProvider
159
+ def initialize(strategy)
160
+ @strategy = strategy
161
+ end
162
+
163
+ def client_id
164
+ ENV['ENTRA_CLIENT_ID']
165
+ end
166
+
167
+ def client_secret
168
+ ENV['ENTRA_CLIENT_SECRET']
169
+ end
170
+
171
+ def authorize_params
172
+ ap = {}
173
+
174
+ if @strategy.request && @strategy.request.params['login_hint']
175
+ ap['login_hint'] = @strategy.request.params['login_hint']
176
+ end
177
+
178
+ # (...and/or set other options such as 'prompt' here...)
179
+
180
+ return ap
181
+ end
182
+ end
183
+ ```
184
+
185
+ In this example, we're providing custom `authorize_params`. You can just return a standard Ruby Hash here, using lower case String or Symbol keys. The `strategy` value given to the initializer is an instance of [`OmniAuth::Strategies::EntraId`](https://github.com/RIPAGlobal/omniauth-entra-id/blob/master/lib/omniauth/strategies/entra_id.rb) which is a subclass of [`OmniAuth::Strategies::OAuth2`](https://www.rubydoc.info/gems/omniauth-oauth2/1.8.0/OmniAuth/Strategies/OAuth2), but that's not all that helpful! What's more useful is to know that **the Rails `request` object is available via `@strategy.request` and, likewise, the session store via `@strategy.session`**. This gives you a lot of flexibility for responding to an inbound request or user session, varying the parameters used for the Entra OAuth flow.
186
+
187
+ In method `#authorize_params` above, the request object is used to look for a `login_hint` query string entry, set in whichever view(s) is/are presented by your application for use when your users need to be redirected to the OmniAuth controller in order to kick off OAuth with Entra. The value is copied into the `authorize_params` Hash. Earlier, it was mentioned that there was a special case of `prompt` being pulled from the request URL query data, but that this could also be done via a custom provider - here, you can see how; just check `@strategy.request.params['prompt']` and copy that into `authorize_params` if preset.
188
+
189
+ > **NB:** Naming things is hard! The predecessor gem used the name `YouTenantProvider` since it was focused on custom tenant provision, but if using this in a more generic way, perhaps consider a more generic name such as, say, `CustomOmniAuthEntraProvider`.
190
+
191
+ #### Special case scope override
192
+
193
+ If required and more convenient, you can specify a custom `scope` value via generation of an authorisation URL including that required `scope`, rather than by using a custom provider class with `def scope...end` method. Include the `scope` value in your call to generate the URL thus:
194
+
195
+ ```ruby
196
+ omniauth_authorize_url('resource_name_eg_user', 'entra_id', scope: '...')
197
+ ```
198
+
199
+
200
+
201
+ ## Contributing
202
+
203
+ Bug reports and pull requests are welcome on GitHub at https://github.com/RIPAGlobal/omniauth-entra-id. This project is intended to be a safe, welcoming space for collaboration so contributors must adhere to the [code of conduct](https://github.com/RIPAGlobal/omniauth-entra-id/blob/master/CODE_OF_CONDUCT.md).
204
+
205
+ ### Getting running
206
+
207
+ * Fork the repository
208
+ * Check out your fork
209
+ * `cd` into the repository
210
+ * `bin/setup`
211
+ * `bundle exec rspec` to make sure all the tests run
212
+
213
+ ### Making changes
214
+
215
+ * Make your change
216
+ * Add tests and check that `bundle exec rspec` still runs successfully
217
+ * For new features (rather than bug fixes), update `README.md` with details
218
+
219
+
220
+
221
+ ## License
222
+
223
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
224
+
225
+
226
+
227
+ ## Code of Conduct
228
+
229
+ Everyone interacting in this project's codebases, issue trackers, chat rooms and mailing lists must follow the [code of conduct](https://github.com/RIPAGlobal/omniauth-entra-id/blob/master/CODE_OF_CONDUCT.md).
data/UPGRADING.md ADDED
@@ -0,0 +1,97 @@
1
+ # Upgrading from `omniauth-azure-activedirectory-v2`
2
+
3
+ This guide assumes you were on v2.3 or v2.4 of the old-named gem. The basic steps are:
4
+
5
+ * Update your code to account for the rename
6
+ * Update your code to account for breaking changes
7
+
8
+
9
+
10
+ ## Updates due to the gem rename
11
+
12
+ All gem users will likely need to follow these steps.
13
+
14
+ * In general, searching project-wide for `azure_activedirectory_v2` and replacing with `entra_id` and, likewise, for the hyphenated `azure-activedirectory-v2` and replacing with `entra-id`, will cover a lot of use cases
15
+ * `README.md` always included examples with environment variables that were named as illustrations only; these have changed from e.g. `AZURE_CLIENT_ID` to `ENTRA_CLIENT_ID` just for internal consistency, but while renaming your own related environment variables or constants (should you use any) may help with code base understanding and consistency, it's not essential. Those names are part of _your_ code base, not part of code in this gem.
16
+
17
+ ### Configuration
18
+
19
+ Rename the strategy in your configuration block:
20
+
21
+ ```ruby
22
+ config.omniauth(
23
+ :azure_activedirectory_v2,
24
+ # ...
25
+ )
26
+ ```
27
+
28
+ ...becomes:
29
+
30
+ ```ruby
31
+ config.omniauth(
32
+ :entra_id,
33
+ # ...
34
+ )
35
+ ```
36
+
37
+ ### Callback routes
38
+
39
+ Depending on how you handle callbacks from OmniAuth, you might need to update routes or controllers handling shared routes to account for the name change. The old callback URL of:
40
+
41
+ ```
42
+ https://example.com/v1/auth/azure_activedirectory_v2/callback
43
+ ```
44
+
45
+ ...is now:
46
+
47
+ ```
48
+ https://example.com/v1/auth/entra/callback
49
+ ```
50
+
51
+ ### URL generation
52
+
53
+ Change things like this:
54
+
55
+ ```
56
+ omniauth_authorize_url('resource_name_eg_user', 'azure_activedirectory_v2', scope: '...')
57
+ ```
58
+
59
+ ...to this:
60
+
61
+ ```
62
+ omniauth_authorize_url('resource_name_eg_user', 'entra_id', scope: '...')
63
+ ```
64
+
65
+
66
+
67
+ ## Updates due to other breaking changes
68
+
69
+ ### Critical breaking change for all gem users
70
+
71
+ This change is for UIDs and is the main reason for creating a V3 gem, whether or not it included the Entra name change.
72
+
73
+ * The UID returned by OmniAuth for a user previously depended upon the `oid` (object ID) returned by Microsoft. As noted in #33 and fixed in #34, this _might not be unique_ and tenant ID (`tid`) is supposed to be considered too.
74
+ * Out-of-box, Entra ID will do this. If you were an Azure ActiveDirectory V2 (old-name gem, version 2.x) user, then you will have been receiving different UIDs based only on the `oid` from Microsoft.
75
+ * **The change of OID might break the connection between a previously-registered and logged in user and a new login** as usually, you need to store the OmniAuth UID somewhere alongside or within your User records when a user is "connected to" an external OAuth service such as Entra ID.
76
+
77
+ You have two options, should the issue affect you (and it almost certainly will).
78
+
79
+ * If you can determine the tenant IDs for all users in your database, you can just migrate the UIDs. The new UID is just a simple concatenation of tenant ID and object ID, so treating the UID as a string, add the tenant ID as a prefix without any other changes in your migration and things should work fine thereafter.
80
+ * Otherwise, you should lazy-migrate:
81
+ - As usual, in your OAuth callback handler, `request.env['omniauth.auth'].uid` gives the UID - but now that's the "new" Entra gem's value which includes tenant ID.
82
+ - If you can find a user with that ID, then all good - they've been migrated already or got connected to Entra *after* you started using the updated gem
83
+ - Otherwise, check `request.env['omniauth.auth'].extra.dig('raw_info', 'oid')` - this gives the value that the *old Azure ActiveDirectory V2 gem* used as UID
84
+ - Look up the user with this ID. If you find them, great; remember to migrate their record by updating their stored auth ID to the new `request.env['omniauth.auth'].uid` value.
85
+ - For better security add something like an indexed boolean column indicating whether or not the user has been thus migrated and only perform old OID lookups on users which have not yet been migrated.
86
+ - If the user can't be found by either means, then they've not been connected to your system yet. Your existing handling path for such a condition applies.
87
+
88
+ ### Applications that handle multiple OAuth providers
89
+
90
+ If your user records contain users that have 'connected' to more than one kind of OAuth provider, then as well as the third party's UID being stored for future logins, you'll most likely have stored the OmniAuth provider name too so that the UID can be looked up in a provider's context (there's no guarantee, of course, that UIDs are unique *between providers* since they're entirely independent entities with their own strategies for allocating unique IDs).
91
+
92
+ In that case, you will need to migrate records from the old `azure_activedirectory_v2` name to `entra_id`. **Zero-downtime deployment of this change would be very hard since your codebase would need to update from the Azure ActiveDirectory V2 gem to the Entra ID gem with the migration running simultaneously**, so if you need to do such a migration, then you probably should plan for a small maintenance window. At the scheduled time, go into maintenance mode, migrate, deploy, and restore normal service. Even without this, though, the 'worst that can happen' (in theory!) would be temporary user login failures. Either the Entra gem will be causing you to look for a user with an `entra_id` provider but the migration to set this hasn't run yet, or the other way round, with the old gem looking for the old provider name but it's already updated.
93
+
94
+ ### Breaking changes that depend on whether or not you use a certain feature
95
+
96
+ * If you refer to `OmniAuth::Strategies::AzureActivedirectoryV2` at all, then this becomes `OmniAuth::Strategies::EntraId` (note lower case "d").
97
+ * `base_azure_url` option renamed to just `base_url` with corresponding rename of `OmniAuth::Strategies::AzureActivedirectoryV2::BASE_AZURE_URL` to `OmniAuth::Strategies::EntraId::BASE_URL`.
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "omniauth/entra/id"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ module OmniAuth
2
+ module Entra
3
+ module Id
4
+ VERSION = "3.0.0"
5
+ DATE = "2024-10-22"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ require File.join('omniauth', 'entra_id', 'version')
2
+ require File.join('omniauth', 'strategies', 'entra_id')
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omniauth-oauth2'
4
+
5
+ module OmniAuth
6
+ module Strategies
7
+ class EntraId < OmniAuth::Strategies::OAuth2
8
+ BASE_URL = 'https://login.microsoftonline.com'
9
+
10
+ option :name, 'entra_id'
11
+ option :tenant_provider, nil
12
+ option :jwt_leeway, 60
13
+
14
+ DEFAULT_SCOPE = 'openid profile email'
15
+ COMMON_TENANT_ID = 'common'
16
+
17
+ # The tenant_provider must return client_id, client_secret and,
18
+ # optionally, tenant_id and base_url.
19
+ #
20
+ args [:tenant_provider]
21
+
22
+ def client
23
+ provider = if options.tenant_provider
24
+ options.tenant_provider.new(self)
25
+ else
26
+ options
27
+ end
28
+
29
+ options.client_id = provider.client_id
30
+
31
+ if provider.respond_to?(:client_secret) && provider.client_secret
32
+ options.client_secret = provider.client_secret
33
+ elsif provider.respond_to?(:certificate_path) && provider.respond_to?(:tenant_id) && provider.certificate_path && provider.tenant_id
34
+ options.token_params = {
35
+ tenant: provider.tenant_id,
36
+ client_id: provider.client_id,
37
+ client_assertion: client_assertion(provider.tenant_id, provider.client_id, provider.certificate_path),
38
+ client_assertion_type: client_assertion_type
39
+ }
40
+ else
41
+ raise ArgumentError, "You must provide either client_secret or certificate_path and tenant_id"
42
+ end
43
+
44
+ options.tenant_id = if provider.respond_to?(:tenant_id)
45
+ provider.tenant_id
46
+ else
47
+ COMMON_TENANT_ID
48
+ end
49
+
50
+ options.base_url = if provider.respond_to?(:base_url )
51
+ provider.base_url
52
+ else
53
+ BASE_URL
54
+ end
55
+
56
+ options.tenant_name = provider.tenant_name if provider.respond_to?(:tenant_name)
57
+ options.custom_policy = provider.custom_policy if provider.respond_to?(:custom_policy)
58
+ options.authorize_params = provider.authorize_params if provider.respond_to?(:authorize_params)
59
+ options.authorize_params.domain_hint = provider.domain_hint if provider.respond_to?(:domain_hint) && provider.domain_hint
60
+ options.authorize_params.prompt = request.params['prompt'] if defined?(request) && request.params['prompt']
61
+
62
+ options.authorize_params.scope = if defined?(request) && request.params['scope']
63
+ request.params['scope']
64
+ elsif provider.respond_to?(:scope) && provider.scope
65
+ provider.scope
66
+ else
67
+ DEFAULT_SCOPE
68
+ end
69
+
70
+ oauth2 = if provider.respond_to?(:adfs?) && provider.adfs?
71
+ 'oauth2'
72
+ else
73
+ 'oauth2/v2.0'
74
+ end
75
+
76
+ tenanted_endpoint_base_url = if options.custom_policy && options.tenant_name
77
+ "https://#{options.tenant_name}.b2clogin.com/#{options.tenant_name}.onmicrosoft.com/#{options.custom_policy}"
78
+ else
79
+ "#{options.base_url}/#{options.tenant_id}"
80
+ end
81
+
82
+ options.client_options.authorize_url = "#{tenanted_endpoint_base_url}/#{oauth2}/authorize"
83
+ options.client_options.token_url = "#{tenanted_endpoint_base_url}/#{oauth2}/token"
84
+
85
+ super
86
+ end
87
+
88
+ uid do
89
+ #
90
+ # https://learn.microsoft.com/en-us/entra/identity-platform/migrate-off-email-claim-authorization
91
+ #
92
+ # OID alone might not be unique; TID must be included. An alternative
93
+ # would be to use 'sub' but this is only unique in client/app
94
+ # registration context. If a different app registration is used, the
95
+ # 'sub' values can be different too.
96
+ #
97
+ raw_info['tid'] + raw_info['oid']
98
+ end
99
+
100
+ info do
101
+ {
102
+ name: raw_info['name'],
103
+ email: raw_info['email'],
104
+ nickname: raw_info['unique_name'],
105
+ first_name: raw_info['given_name'],
106
+ last_name: raw_info['family_name']
107
+ }
108
+ end
109
+
110
+ extra do
111
+ { raw_info: raw_info }
112
+ end
113
+
114
+ def callback_url
115
+ full_host + callback_path
116
+ end
117
+
118
+ # https://learn.microsoft.com/en-us/entra/identity-platform/id-tokens
119
+ #
120
+ # Some account types from Microsoft seem to only have a decodable ID token,
121
+ # with JWT unable to decode the access token. Information is limited in those
122
+ # cases. Other account types provide an expanded set of data inside the auth
123
+ # token, which does decode as a JWT.
124
+ #
125
+ # Merge the two, allowing the expanded auth token data to overwrite the ID
126
+ # token data if keys collide, and use this as raw info.
127
+ #
128
+ def raw_info
129
+ if @raw_info.nil?
130
+ id_token_data = begin
131
+ ::JWT.decode(access_token.params['id_token'], nil, false).first
132
+ rescue StandardError
133
+ {}
134
+ end
135
+
136
+ # For multi-tenant apps (the 'common' tenant_id) it doesn't make any
137
+ # sense to verify the token issuer, because the value of 'iss' in the
138
+ # token depends on the 'tid' in the token itself.
139
+ #
140
+ issuer = if options.tenant_id.nil? || options.tenant_id == COMMON_TENANT_ID
141
+ nil
142
+ else
143
+ "#{options.base_url || BASE_URL}/#{options.tenant_id}/v2.0"
144
+ end
145
+
146
+ # https://learn.microsoft.com/en-us/entra/identity-platform/id-tokens#validate-tokens
147
+ #
148
+ JWT::Verify.verify_claims(
149
+ id_token_data,
150
+ verify_iss: !issuer.nil?,
151
+ iss: issuer,
152
+ verify_aud: true,
153
+ aud: options.client_id,
154
+ verify_expiration: true,
155
+ verify_not_before: true,
156
+ leeway: options[:jwt_leeway]
157
+ )
158
+
159
+ auth_token_data = begin
160
+ ::JWT.decode(access_token.token, nil, false).first
161
+ rescue StandardError
162
+ {}
163
+ end
164
+
165
+ id_token_data.merge!(auth_token_data)
166
+ @raw_info = id_token_data
167
+ end
168
+
169
+ @raw_info
170
+ end
171
+
172
+ # https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential
173
+ #
174
+ # The below methods support the flow for using certificate-based client
175
+ # assertion authentication.
176
+ #
177
+ def client_assertion_type
178
+ 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
179
+ end
180
+
181
+ def client_assertion_claims(tenant_id, client_id)
182
+ {
183
+ 'aud' => "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token",
184
+ 'exp' => Time.now.to_i + 300,
185
+ 'iss' => client_id,
186
+ 'jti' => SecureRandom.uuid,
187
+ 'nbf' => Time.now.to_i,
188
+ 'sub' => client_id,
189
+ 'iat' => Time.now.to_i
190
+ }
191
+ end
192
+
193
+ def client_assertion(tenant_id, client_id, certificate_path)
194
+ certificate_file = OpenSSL::PKCS12.new(File.read(certificate_path))
195
+ certificate_thumbprint ||= Digest::SHA1.digest(certificate_file.certificate.to_der)
196
+ private_key = OpenSSL::PKey::RSA.new(certificate_file.key)
197
+
198
+ claims = client_assertion_claims(tenant_id, client_id)
199
+ x5c = Base64.strict_encode64(certificate_file.certificate.to_der)
200
+ x5t = Base64.strict_encode64(certificate_thumbprint)
201
+
202
+ JWT.encode(claims, private_key, 'RS256', { 'x5c': [x5c], 'x5t': x5t })
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1 @@
1
+ require File.join('omniauth', 'entra_id')
@@ -0,0 +1,53 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ $:.push File.expand_path( '../lib', __FILE__ )
5
+ require 'omniauth/entra_id/version'
6
+
7
+ # https://guides.rubygems.org/specification-reference/
8
+ #
9
+ Gem::Specification.new do |s|
10
+ s.name = 'omniauth-entra-id'
11
+ s.version = OmniAuth::Entra::Id::VERSION
12
+ s.date = OmniAuth::Entra::Id::DATE
13
+ s.summary = 'OAuth 2 authentication with the Entra ID API.'
14
+ s.authors = [ 'RIPA Global' ]
15
+ s.email = [ 'dev@ripaglobal.com' ]
16
+ s.licenses = [ 'MIT' ]
17
+ s.homepage = 'https://github.com/RIPAGlobal/omniauth-entra-id'
18
+
19
+ s.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
20
+ s.require_paths = ['lib']
21
+ s.bindir = 'exe'
22
+ s.files = %w{
23
+ README.md
24
+ CHANGELOG.md
25
+ CODE_OF_CONDUCT.md
26
+ UPGRADING.md
27
+ LICENSE.txt
28
+
29
+ Gemfile
30
+ bin/console
31
+ bin/setup
32
+
33
+ lib/omniauth-entra-id.rb
34
+ lib/omniauth/entra_id.rb
35
+ lib/omniauth/entra_id/version.rb
36
+ lib/omniauth/strategies/entra_id.rb
37
+
38
+ omniauth-entra-id.gemspec
39
+ }
40
+
41
+ s.metadata = {
42
+ 'homepage_uri' => 'https://www.ripaglobal.com/',
43
+ 'bug_tracker_uri' => 'https://github.com/RIPAGlobal/omniauth-entra-id/issues/',
44
+ 'changelog_uri' => 'https://github.com/RIPAGlobal/omniauth-entra-id/blob/master/CHANGELOG.md',
45
+ 'source_code_uri' => 'https://github.com/RIPAGlobal/omniauth-entra-id'
46
+ }
47
+
48
+ s.add_runtime_dependency('omniauth-oauth2', '~> 1.8')
49
+
50
+ s.add_development_dependency('debug', '~> 1.9 ')
51
+ s.add_development_dependency('rake', '~> 13.2 ')
52
+ s.add_development_dependency('rspec', '~> 3.13')
53
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-entra-id
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ platform: ruby
6
+ authors:
7
+ - RIPA Global
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: omniauth-oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: debug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.9'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.13'
69
+ description:
70
+ email:
71
+ - dev@ripaglobal.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - CODE_OF_CONDUCT.md
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - UPGRADING.md
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/omniauth-entra-id.rb
85
+ - lib/omniauth/entra_id.rb
86
+ - lib/omniauth/entra_id/version.rb
87
+ - lib/omniauth/strategies/entra_id.rb
88
+ - omniauth-entra-id.gemspec
89
+ homepage: https://github.com/RIPAGlobal/omniauth-entra-id
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ homepage_uri: https://www.ripaglobal.com/
94
+ bug_tracker_uri: https://github.com/RIPAGlobal/omniauth-entra-id/issues/
95
+ changelog_uri: https://github.com/RIPAGlobal/omniauth-entra-id/blob/master/CHANGELOG.md
96
+ source_code_uri: https://github.com/RIPAGlobal/omniauth-entra-id
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 3.0.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 3.5.21
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: OAuth 2 authentication with the Entra ID API.
116
+ test_files: []