rack_entra_id_auth 1.3.3 → 1.4.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: bbee29e061f4499a9a8ddfe96f7cb76a0c57c25fa98a10d2edbe830ba832dccd
4
- data.tar.gz: dd246584193ba03782c92e41a7b72e32824c8ecd4515678b4ed39e6e5ec35c8a
3
+ metadata.gz: d4de08ceb491ee5f4fc94eafde1b0a76710a9db7fcc0a0cae198ec66ef993dd0
4
+ data.tar.gz: e9e3ef35102d6f5d88fe13abd5f812c7e18c930391e0432b061934e742ac6bbf
5
5
  SHA512:
6
- metadata.gz: e4c3cee936287beec22c6593a36c18e4d82f4b0d99ba9a4d768bddd7108b661f24cbe32b5082697ebb3a166f44fc9230c0d6419bd8865705e2f8e0a1934f4a03
7
- data.tar.gz: c88a785fea49772f49920d7b09dfbeeb116675b4111d652ecf237bc0fbcd9d6fc9c331c0e0d04af1a0295d6c6eee19946589a16ae502672c63d9d9d8c423352a
6
+ metadata.gz: 0fe33f7129a61e27ef94042734c5110ac756e74e7969012a607fc06a5171fad01e459f763ea790fab85e26c296fd8670da9fce9ec2f879d787605d12a424929a
7
+ data.tar.gz: 5cdf8ed28ed3208bdc06fc70b580f30dc366f6722fd73938fd01c6c7e882c8fd405543b09a9c000fad4f8c32fc80336a795e2a5a7fc0a3377938e077b49c235a
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2024 David Susco
1
+ Copyright (c) 2026 David Susco
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,35 +1,128 @@
1
1
  # RackEntraIdAuth
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Rails-aware Rack middleware for Microsoft Entra ID (SAML) authentication, built on [ruby-saml](https://github.com/SAML-Toolkits/ruby-saml).
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rack_entra_id_auth`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ The middleware handles single sign-on and single logout with Entra ID. It stores the user's SAML attributes in the session and redirects any `401 Unauthorized` response from your app to the Entra ID login. In development it can run a mock login server instead, so you don't need a real IdP.
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 3.2+
10
+ - Rails 7.2+
11
+ - Rack 3.2+
6
12
 
7
13
  ## Installation
8
14
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
15
+ Add the gem to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'rack_entra_id_auth'
19
+ ```
20
+
21
+ Or install it straight from GitHub:
22
+
23
+ ```ruby
24
+ gem 'rack_entra_id_auth', github: 'dsusco/rack_entra_id_auth'
25
+ ```
26
+
27
+ Then run `bundle install`. In a Rails app the middleware is added automatically by the gem's Railtie.
28
+
29
+ ## Setup
30
+
31
+ Generate the initializer, then fill in your Entra ID (IdP) and app (SP) settings:
32
+
33
+ ```bash
34
+ bin/rails generate rack_entra_id_auth:initializer
35
+ ```
36
+
37
+ This creates `config/initializers/rack_entra_id_auth.rb`, which documents every available setting. A minimal configuration looks like this:
38
+
39
+ ```ruby
40
+ RackEntraIdAuth.configure do |config|
41
+ config.idp_sso_service_url = 'https://login.microsoftonline.com/<tenant>/saml2'
42
+ config.idp_slo_service_url = 'https://login.microsoftonline.com/<tenant>/saml2'
43
+ config.idp_cert = '<Entra ID X.509 certificate>'
44
+
45
+ config.sp_entity_id = 'https://your.app/'
46
+ config.assertion_consumer_service_url = 'https://your.app/saml/login'
47
+ config.single_logout_service_url = 'https://your.app/saml/logout'
48
+ end
49
+ ```
50
+
51
+ Instead of setting the IdP values by hand, you can load them from your Entra ID app's federation metadata URL:
52
+
53
+ ```ruby
54
+ config.metadata_url = 'https://login.microsoftonline.com/<tenant>/federationmetadata/2007-06/federationmetadata.xml?appid=<app id>'
55
+ ```
10
56
 
11
- Install the gem and add to the application's Gemfile by executing:
57
+ The middleware intercepts requests to the assertion consumer service and single logout URLs, so make sure they don't clash with routes in your app.
12
58
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
59
+ Optionally, generate `login` and `logout` routes so you can use `login_path` and `logout_path` helpers in your views:
14
60
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
61
+ ```bash
62
+ bin/rails generate rack_entra_id_auth:routes
63
+ ```
16
64
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
65
+ ### Mock server
66
+
67
+ The mock server is **on by default**. It shows a simple login page where you pick one of the users defined in `config.mock_attributes`. Turn it off wherever you use Entra ID.
68
+
69
+ This setting must go in `config/application.rb` or an environment file, not the initializer, because the middleware is chosen before initializers run:
70
+
71
+ ```ruby
72
+ # config/environments/production.rb
73
+ config.rack_entra_id_auth.mock_server = false
74
+ ```
18
75
 
19
76
  ## Usage
20
77
 
21
- TODO: Write usage instructions here
78
+ To require a login, return a `401`. The middleware turns it into a redirect to Entra ID (or the mock login page), and sends the user back to the original URL afterwards.
22
79
 
23
- ## Development
80
+ ```ruby
81
+ class ApplicationController < ActionController::Base
82
+ before_action :authenticate!
83
+
84
+ private
85
+
86
+ def authenticate!
87
+ head :unauthorized if current_user.blank?
88
+ end
89
+
90
+ def current_user
91
+ session[:entra_id]
92
+ end
93
+ end
94
+ ```
24
95
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
96
+ After login, `session[:entra_id]` holds the user's SAML attributes, for example `displayname`, `emailaddress`, `name` and `groups`. You can change the session key with `config.session_key`, and change how attributes are stored with `config.session_value_proc`.
26
97
 
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
98
+ Linking to `/login` or `/logout` starts a login or logout. Add a `relay_state` query parameter to choose where the user lands afterwards:
28
99
 
29
- ## Contributing
100
+ ```erb
101
+ <%= link_to 'Log out', logout_path(relay_state: root_url) %>
102
+ ```
103
+
104
+ To leave paths alone entirely (health checks, public assets), list them as strings or regular expressions:
105
+
106
+ ```ruby
107
+ config.exclude_paths = ['/up', %r{\A/assets/}]
108
+ ```
109
+
110
+ ### Single logout
111
+
112
+ Single logout is disabled by default, so `/logout` only clears the local session. To also sign the user out of Entra ID, switch to a session store that can be cleared server-side, such as [entra_id_active_record_session_store](https://github.com/dsusco/entra_id_active_record_session_store), then set:
113
+
114
+ ```ruby
115
+ config.skip_single_logout = false
116
+ ```
117
+
118
+ ## Development
30
119
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rack_entra_id_auth.
120
+ ```bash
121
+ bundle install
122
+ bundle exec rake test
123
+ bundle exec rubocop
124
+ ```
32
125
 
33
126
  ## License
34
127
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
128
+ Available as open source under the terms of the [MIT License](MIT-LICENSE).
@@ -94,8 +94,8 @@ RackEntraIdAuth.configure do |config|
94
94
  # requests and should be set below as well.
95
95
 
96
96
  # config.idp_entity_id = ''
97
- config.idp_sso_service_url = "IdP SINGLE SIGN-ON SERVICE URL"
98
- config.idp_slo_service_url = "IdP SINGLE LOGOUT SERVICE URL"
97
+ config.idp_sso_service_url = 'IdP SINGLE SIGN-ON SERVICE URL'
98
+ config.idp_slo_service_url = 'IdP SINGLE LOGOUT SERVICE URL'
99
99
  # config.idp_slo_response_service_url = ''
100
100
  config.idp_cert = 'IdP X509CERTIFICATE'
101
101
  # config.idp_cert_fingerprint = ''
@@ -1,11 +1,9 @@
1
- require 'active_support/configurable'
1
+ require 'active_support/core_ext/object/blank'
2
2
  require 'ruby-saml'
3
3
 
4
4
  module RackEntraIdAuth
5
5
  class Configuration
6
- include ActiveSupport::Configurable
7
-
8
- RUBY_SAML_SETTINGS = %i(
6
+ RUBY_SAML_SETTINGS = %i[
9
7
  idp_entity_id
10
8
  idp_sso_service_url
11
9
  idp_slo_service_url
@@ -40,54 +38,53 @@ module RackEntraIdAuth
40
38
  authn_context_decl_ref
41
39
  security
42
40
  soft
43
- )
41
+ ].freeze
44
42
 
45
- RUBY_SAML_SETTINGS.each { |ruby_saml_setting| config_accessor ruby_saml_setting }
43
+ DEFAULTS = {
44
+ exclude_paths: [],
45
+ login_path: '/login',
46
+ logout_path: '/logout',
47
+ # mock_server must be set in `config/application.rb` or an environment-
48
+ # specific configuration file. I.e. it must happen before initializers as
49
+ # it's used in the initializer created in the Railtie.
50
+ mock_server: true,
51
+ mock_attributes: {},
52
+ session_key: :entra_id,
53
+ session_value_proc: proc do |attributes|
54
+ attributes.each_with_object({}) do |(key, value), memo|
55
+ key = key.split('/').last
56
+ value = value.first if value.is_a?(Array) and value.length.eql?(1) and !key.eql?('groups')
57
+ memo[key] = value
58
+ end
59
+ end,
60
+ skip_single_logout: true
61
+ }.freeze
46
62
 
47
- config_accessor :exclude_paths, default: []
48
- config_accessor :login_path, default: '/login'
49
- config_accessor :login_relay_state_url
50
- config_accessor :logout_path, default: '/logout'
51
- config_accessor :logout_relay_state_url
52
- # mock_server must be set in `config/application.rb` or an environment-
53
- # specific configuration file. I.e. it must happen before initializers as
54
- # it's used in the initializer created in the Railtie.
55
- config_accessor :mock_server, default: true
56
- config_accessor :mock_attributes, default: {}
57
- config_accessor :session_key, default: :entra_id
58
- config_accessor :session_value_proc, default: Proc.new { |attributes|
59
- attributes.inject({}) do |memo, (key, value)|
60
- key = key.split('/').last
61
- value = value.first if value.kind_of?(Array) and value.length.eql?(1) and !key.eql?('groups')
62
- memo[key] = value
63
- memo
64
- end
65
- }
66
- config_accessor :skip_single_logout, default: true
63
+ attr_accessor(*RUBY_SAML_SETTINGS, *DEFAULTS.keys, :login_relay_state_url, :logout_relay_state_url)
64
+ attr_reader :metadata_url
65
+
66
+ def initialize
67
+ DEFAULTS.each { |key, value| public_send("#{key}=", value.dup) }
68
+ end
67
69
 
68
70
  def configuration_options (configuration_options = {})
69
71
  configuration_options.slice(:metadata_url, *RUBY_SAML_SETTINGS).each do |key, value|
70
- self.send("#{key}=", value) unless value.nil?
72
+ send("#{key}=", value) unless value.nil?
71
73
  end
72
74
  end
73
75
 
74
- def metadata_url
75
- @metadata_url
76
- end
77
-
78
76
  def metadata_url= (metadata_url)
79
77
  @metadata_url = metadata_url
80
78
 
81
- OneLogin::RubySaml::IdpMetadataParser.new.parse_remote_to_hash(metadata_url)
82
- .slice(*RUBY_SAML_SETTINGS).each do |key, value|
83
- self.send("#{key}=", value) unless value.nil?
84
- end
79
+ idp_settings = OneLogin::RubySaml::IdpMetadataParser.new.parse_remote_to_hash(metadata_url)
85
80
 
86
- @metadata_url
81
+ idp_settings.slice(*RUBY_SAML_SETTINGS).each do |key, value|
82
+ send("#{key}=", value) unless value.nil?
83
+ end
87
84
  end
88
85
 
89
86
  def ruby_saml_settings
90
- config.to_h.slice(*RUBY_SAML_SETTINGS).compact
87
+ RUBY_SAML_SETTINGS.to_h { |key| [key, public_send(key)] }.compact
91
88
  end
92
89
  end
93
90
  end
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+ require 'active_support/core_ext/object/blank'
1
3
  require 'ruby-saml'
2
4
 
3
5
  module RackEntraIdAuth
@@ -97,7 +99,22 @@ module RackEntraIdAuth
97
99
  # @return [String]
98
100
  #
99
101
  def relay_state_url
100
- request.get_header('rack.request.form_hash')['RelayState'] rescue request.params['RelayState'] || base_url
102
+ safe_redirect_url(request.POST['RelayState'].presence || request.GET['RelayState']) || base_url
103
+ end
104
+
105
+ # Returns the URL if it's an http(s) URL on the same host as the request,
106
+ # otherwise returns nil. Used to keep relay state redirects on-site.
107
+ #
108
+ # @param url [String]
109
+ #
110
+ # @return [String|nil]
111
+ #
112
+ def safe_redirect_url (url)
113
+ uri = URI.parse(url.to_s)
114
+
115
+ url if uri.is_a?(URI::HTTP) and uri.host.eql?(request.host)
116
+ rescue URI::InvalidURIError
117
+ nil
101
118
  end
102
119
 
103
120
  # A single sign-on response for the SAMLResponse in the request's header.
@@ -119,16 +136,21 @@ module RackEntraIdAuth
119
136
  # @return [OneLogin::RubySaml::Response] A single sign-on response for a
120
137
  # Service Provideer initiated single sign-on request.
121
138
  #
122
- def saml_auth_response (auth_request_id: request.session[:auth_request_id], skip_conditions: false, allowed_clock_drift: nil, skip_subject_confirmation: false, skip_recipient_check: false, skip_audience: false)
139
+ def saml_auth_response (auth_request_id: request.session[:auth_request_id],
140
+ skip_conditions: false,
141
+ allowed_clock_drift: nil,
142
+ skip_subject_confirmation: false,
143
+ skip_recipient_check: false,
144
+ skip_audience: false)
123
145
  response = OneLogin::RubySaml::Response.new(
124
146
  saml_response,
125
- { :settings => @saml_settings,
126
- :matches_request_id => auth_request_id,
127
- :skip_conditions => skip_conditions,
128
- :allowed_clock_drift => allowed_clock_drift,
129
- :skip_subject_confirmation => skip_subject_confirmation,
130
- :skip_recipient_check => skip_recipient_check,
131
- :skip_audience => skip_audience })
147
+ { settings: @saml_settings,
148
+ matches_request_id: auth_request_id,
149
+ skip_conditions: skip_conditions,
150
+ allowed_clock_drift: allowed_clock_drift,
151
+ skip_subject_confirmation: skip_subject_confirmation,
152
+ skip_recipient_check: skip_recipient_check,
153
+ skip_audience: skip_audience })
132
154
 
133
155
  # the auth request's ID is no longer needed
134
156
  request.session.delete(:auth_request_id)
@@ -152,9 +174,9 @@ module RackEntraIdAuth
152
174
  def saml_logout_request (allowed_clock_drift: nil, relax_signature_validation: false)
153
175
  OneLogin::RubySaml::SloLogoutrequest.new(
154
176
  request.params['SAMLRequest'],
155
- { :settings => @saml_settings,
156
- :allowed_clock_drift => allowed_clock_drift,
157
- :relax_signature_validation => relax_signature_validation })
177
+ { settings: @saml_settings,
178
+ allowed_clock_drift: allowed_clock_drift,
179
+ relax_signature_validation: relax_signature_validation })
158
180
  end
159
181
 
160
182
  # A single logout response for the SAMLResponse in the request's query
@@ -175,9 +197,9 @@ module RackEntraIdAuth
175
197
  logout_response = OneLogin::RubySaml::Logoutresponse.new(
176
198
  request.params['SAMLResponse'],
177
199
  @saml_settings,
178
- { :get_params => request.params,
179
- :matches_request_id => logout_request_id,
180
- :relax_signature_validation => relax_signature_validation })
200
+ { get_params: request.params,
201
+ matches_request_id: logout_request_id,
202
+ relax_signature_validation: relax_signature_validation })
181
203
 
182
204
  # the logout request's ID is no longer needed
183
205
  request.session.delete(:logout_request_id)
@@ -222,13 +244,13 @@ module RackEntraIdAuth
222
244
  def slo_url (params = {})
223
245
  logout_request = OneLogin::RubySaml::Logoutrequest.new
224
246
 
225
- if @saml_settings.idp_slo_service_url.present?
226
- # store the logout request's uuid to validate it in the response
227
- request.session[:logout_request_id] = logout_request.uuid
247
+ return unless @saml_settings.idp_slo_service_url.present?
228
248
 
229
- # return nil if no single logout url is set
230
- logout_request.create(@saml_settings, params)
231
- end
249
+ # store the logout request's uuid to validate it in the response
250
+ request.session[:logout_request_id] = logout_request.uuid
251
+
252
+ # return nil if no single logout url is set
253
+ logout_request.create(@saml_settings, params)
232
254
  end
233
255
 
234
256
  # Returns a single sign-on authentication request URL for the settings
@@ -251,7 +273,7 @@ module RackEntraIdAuth
251
273
  private
252
274
 
253
275
  def saml_response
254
- request.get_header('rack.request.form_hash').try(:[], 'SAMLResponse')
276
+ request.POST['SAMLResponse']
255
277
  end
256
278
  end
257
279
  end
@@ -17,7 +17,7 @@ module RackEntraIdAuth
17
17
  log(env, 'Redirecting login request to Entra ID single sign-on URL…')
18
18
 
19
19
  sso_url = entra_id_request.sso_url(
20
- { :RelayState => request.params['relay_state'] ||
20
+ { RelayState: entra_id_request.safe_redirect_url(request.params['relay_state']) ||
21
21
  RackEntraIdAuth.config.login_relay_state_url ||
22
22
  entra_id_request.base_url })
23
23
 
@@ -33,11 +33,11 @@ module RackEntraIdAuth
33
33
  # destroy session in case single logout fails
34
34
  destroy_session(request.session)
35
35
 
36
- relay_state_url = request.params['relay_state'] ||
36
+ relay_state_url = entra_id_request.safe_redirect_url(request.params['relay_state']) ||
37
37
  RackEntraIdAuth.config.logout_relay_state_url ||
38
38
  entra_id_request.base_url
39
39
 
40
- slo_url = entra_id_request.slo_url({ :RelayState => relay_state_url })
40
+ slo_url = entra_id_request.slo_url({ RelayState: relay_state_url })
41
41
 
42
42
  if request.params['skip_single_logout'].blank? and
43
43
  !RackEntraIdAuth.config.skip_single_logout and
@@ -63,15 +63,15 @@ module RackEntraIdAuth
63
63
  if entra_id_request.login_response?
64
64
  log(env, 'Received single login response…')
65
65
 
66
- auth_response = entra_id_request.saml_auth_response()
66
+ auth_response = entra_id_request.saml_auth_response
67
67
 
68
- if !auth_response.is_valid?
68
+ unless auth_response.is_valid?
69
69
  log(env, "Invalid single login reponse from Entra ID: #{auth_response.errors.first}")
70
70
 
71
71
  return internal_server_error_response("Invalid single login reponse from Entra ID: #{auth_response.errors.first}")
72
72
  end
73
73
 
74
- if !auth_response.success?
74
+ unless auth_response.success?
75
75
  log(env, 'Unsuccessful single single reponse from Entra ID.')
76
76
 
77
77
  return internal_server_error_response('Unsuccessful single login reponse from Entra ID.')
@@ -91,9 +91,9 @@ module RackEntraIdAuth
91
91
  if entra_id_request.logout_request? and !RackEntraIdAuth.config.skip_single_logout
92
92
  log(env, 'Received single logout request…')
93
93
 
94
- logout_request = entra_id_request.saml_logout_request()
94
+ logout_request = entra_id_request.saml_logout_request
95
95
 
96
- if !logout_request.is_valid?
96
+ unless logout_request.is_valid?
97
97
  log(env, "Invalid single logout request from Entra ID: #{logout_request.errors.first}")
98
98
 
99
99
  return internal_server_error_response("Invalid single logout request from Entra ID: #{logout_request.errors.first}")
@@ -108,7 +108,7 @@ module RackEntraIdAuth
108
108
  request_id: logout_request.id,
109
109
  logout_message: nil,
110
110
  params: {
111
- :RelayState => entra_id_request.relay_state_url
111
+ RelayState: entra_id_request.relay_state_url
112
112
  },
113
113
  logout_status_code: nil)
114
114
 
@@ -121,15 +121,15 @@ module RackEntraIdAuth
121
121
  if entra_id_request.logout_response?
122
122
  log(env, 'Received single logout response…')
123
123
 
124
- logout_response = entra_id_request.saml_logout_response()
124
+ logout_response = entra_id_request.saml_logout_response
125
125
 
126
- if !logout_response.validate
126
+ unless logout_response.validate
127
127
  log(env, "Invalid single logout reponse from Entra ID: #{logout_response.errors.first}")
128
128
 
129
129
  return internal_server_error_response("Invalid single logout reponse from Entra ID: #{logout_response.errors.first}")
130
130
  end
131
131
 
132
- if !logout_response.success?
132
+ unless logout_response.success?
133
133
  log(env, 'Unsuccessful single logout reponse from Entra ID.')
134
134
 
135
135
  return internal_server_error_response('Unsuccessful single logout reponse from Entra ID.')
@@ -153,7 +153,7 @@ module RackEntraIdAuth
153
153
  log(env, 'Intercepted 401 Unauthorized response, redirecting to Entra ID single sign-on URL…')
154
154
 
155
155
  return found_redirect_response(
156
- entra_id_request.sso_url(:RelayState => request.url),
156
+ entra_id_request.sso_url(RelayState: request.url),
157
157
  'Intercepted 401 Unauthorized response, redirecting to Entra ID single sign-on URL')
158
158
  end
159
159
 
@@ -176,8 +176,8 @@ module RackEntraIdAuth
176
176
  def internal_server_error_response (content = 'Internal server error')
177
177
  [ 500,
178
178
  { 'content-type' => 'text/html',
179
- 'content-length' => content.length },
180
- [ content ] ]
179
+ 'content-length' => content.bytesize.to_s },
180
+ [ content ] ]
181
181
  end
182
182
 
183
183
  def log (env, message, level = :info)
@@ -19,7 +19,7 @@ module RackEntraIdAuth
19
19
  log(env, 'Rendering mock login page…')
20
20
 
21
21
  return [ 200,
22
- { 'Content-Type' => 'text/html' },
22
+ { 'content-type' => 'text/html' },
23
23
  [ login_page(request.url) ] ]
24
24
  end
25
25
 
@@ -28,8 +28,8 @@ module RackEntraIdAuth
28
28
  log(env, 'Initializing session and redirecting to relay state URL…')
29
29
 
30
30
  attributes = RackEntraIdAuth.config.mock_attributes[request.params['username']] || {}
31
- redirect_url = request.params['relay_state'] ||
32
- request.params['RelayState'] ||
31
+ redirect_url = entra_id_request.safe_redirect_url(request.params['relay_state']) ||
32
+ entra_id_request.safe_redirect_url(request.params['RelayState']) ||
33
33
  RackEntraIdAuth.config.login_relay_state_url ||
34
34
  entra_id_request.base_url
35
35
 
@@ -44,8 +44,8 @@ module RackEntraIdAuth
44
44
  if entra_id_request.logout?
45
45
  log(env, 'Destroying session and redirecting to relay state URL…')
46
46
 
47
- redirect_url = request.params['relay_state'] ||
48
- request.params['RelayState'] ||
47
+ redirect_url = entra_id_request.safe_redirect_url(request.params['relay_state']) ||
48
+ entra_id_request.safe_redirect_url(request.params['RelayState']) ||
49
49
  RackEntraIdAuth.config.logout_relay_state_url ||
50
50
  entra_id_request.base_url
51
51
 
@@ -62,10 +62,8 @@ module RackEntraIdAuth
62
62
  if response[0] == 401
63
63
  log(env, 'Intercepted 401 Unauthorized response, redirecting to mock login URL…')
64
64
 
65
- login_url = URI::HTTP.build(host: request.host,
66
- port: request.port,
67
- path: RackEntraIdAuth.config.login_path,
68
- query: URI.encode_www_form({ :RelayState => request.url }))
65
+ login_url = "#{request.base_url}#{RackEntraIdAuth.config.login_path}?" +
66
+ URI.encode_www_form(RelayState: request.url)
69
67
 
70
68
  return found_redirect_response(
71
69
  login_url,
@@ -96,30 +94,30 @@ module RackEntraIdAuth
96
94
  end
97
95
 
98
96
  def login_page (action)
99
- options = RackEntraIdAuth.config.mock_attributes.keys.map { |key| %Q~<option value="#{key}">#{key}</option>~ } .join
100
-
101
- <<-EOS
102
- <!DOCTYPE html>
103
- <html>
104
- <head>
105
- <meta charset="utf-8">
106
- <title>Mock Middleware Login</title>
107
- </head>
108
-
109
- <body>
110
- <form action="#{action}" method="post">
111
- <label for="username">Username</label>
112
-
113
- <select id="username" name="username">
114
- <option label="No User" value=""></option>
115
- #{options}
116
- </select>
117
-
118
- <input type="submit" value="Login">
119
- </form>
120
- </body>
121
- </html>
122
- EOS
97
+ options = RackEntraIdAuth.config.mock_attributes.keys.map { |key| %(<option value="#{key}">#{key}</option>) }.join
98
+
99
+ <<~HTML
100
+ <!DOCTYPE html>
101
+ <html>
102
+ <head>
103
+ <meta charset="utf-8">
104
+ <title>Mock Middleware Login</title>
105
+ </head>
106
+
107
+ <body>
108
+ <form action="#{action}" method="post">
109
+ <label for="username">Username</label>
110
+
111
+ <select id="username" name="username">
112
+ <option label="No User" value=""></option>
113
+ #{options}
114
+ </select>
115
+
116
+ <input type="submit" value="Login">
117
+ </form>
118
+ </body>
119
+ </html>
120
+ HTML
123
121
  end
124
122
  end
125
123
  end
@@ -1,3 +1,3 @@
1
1
  module RackEntraIdAuth
2
- VERSION = '1.3.3'
2
+ VERSION = '1.4.0'.freeze
3
3
  end
@@ -1,4 +1,6 @@
1
+ require 'rack_entra_id_auth/version'
1
2
  require 'rack_entra_id_auth/configuration'
3
+ require 'rack_entra_id_auth/entra_id_request'
2
4
 
3
5
  module RackEntraIdAuth
4
6
  def self.configure
metadata CHANGED
@@ -1,98 +1,62 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack_entra_id_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.3
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Susco
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ - - "<"
17
20
  - !ruby/object:Gem::Version
18
- version: '7.0'
21
+ version: '9'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: '7.0'
28
+ version: '7.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: rack
28
34
  requirement: !ruby/object:Gem::Requirement
29
35
  requirements:
30
36
  - - "~>"
31
37
  - !ruby/object:Gem::Version
32
- version: '3.0'
38
+ version: '3.2'
33
39
  type: :runtime
34
40
  prerelease: false
35
41
  version_requirements: !ruby/object:Gem::Requirement
36
42
  requirements:
37
43
  - - "~>"
38
44
  - !ruby/object:Gem::Version
39
- version: '3.0'
45
+ version: '3.2'
40
46
  - !ruby/object:Gem::Dependency
41
47
  name: ruby-saml
42
48
  requirement: !ruby/object:Gem::Requirement
43
49
  requirements:
44
50
  - - "~>"
45
51
  - !ruby/object:Gem::Version
46
- version: '1.10'
52
+ version: '1.18'
47
53
  type: :runtime
48
54
  prerelease: false
49
55
  version_requirements: !ruby/object:Gem::Requirement
50
56
  requirements:
51
57
  - - "~>"
52
58
  - !ruby/object:Gem::Version
53
- version: '1.10'
54
- - !ruby/object:Gem::Dependency
55
- name: minitest
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '5.16'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '5.16'
68
- - !ruby/object:Gem::Dependency
69
- name: rake
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '13.0'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '13.0'
82
- - !ruby/object:Gem::Dependency
83
- name: rubocop
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '1.21'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '1.21'
59
+ version: '1.18'
96
60
  description: Rails aware Rack middleware for Entra ID authentication.
97
61
  email:
98
62
  - dsusco@gmail.com
@@ -118,9 +82,10 @@ licenses:
118
82
  - MIT
119
83
  metadata:
120
84
  bug_tracker_uri: https://github.com/dsusco/rack_entra_id_auth/issues
121
- changelog_uri: https://github.com/dsusco/rack_entra_id_auth/releases/tag/v1.3.3
85
+ changelog_uri: https://github.com/dsusco/rack_entra_id_auth/releases/tag/v1.4.0
122
86
  homepage_uri: https://github.com/dsusco/rack_entra_id_auth
123
87
  source_code_uri: https://github.com/dsusco/rack_entra_id_auth
88
+ rubygems_mfa_required: 'true'
124
89
  rdoc_options: []
125
90
  require_paths:
126
91
  - lib
@@ -128,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
93
  requirements:
129
94
  - - ">="
130
95
  - !ruby/object:Gem::Version
131
- version: 3.0.0
96
+ version: 3.2.0
132
97
  required_rubygems_version: !ruby/object:Gem::Requirement
133
98
  requirements:
134
99
  - - ">="
135
100
  - !ruby/object:Gem::Version
136
101
  version: '0'
137
102
  requirements: []
138
- rubygems_version: 3.6.2
103
+ rubygems_version: 4.0.20
139
104
  specification_version: 4
140
105
  summary: Rails aware Rack middleware for Entra ID authentication.
141
106
  test_files: []