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 +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +107 -14
- data/lib/generators/rack_entra_id_auth/templates/config_initializer.rb +2 -2
- data/lib/rack_entra_id_auth/configuration.rb +34 -37
- data/lib/rack_entra_id_auth/entra_id_request.rb +44 -22
- data/lib/rack_entra_id_auth/middleware.rb +15 -15
- data/lib/rack_entra_id_auth/mock_middleware.rb +31 -33
- data/lib/rack_entra_id_auth/version.rb +1 -1
- data/lib/rack_entra_id_auth.rb +2 -0
- metadata +20 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4de08ceb491ee5f4fc94eafde1b0a76710a9db7fcc0a0cae198ec66ef993dd0
|
|
4
|
+
data.tar.gz: e9e3ef35102d6f5d88fe13abd5f812c7e18c930391e0432b061934e742ac6bbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fe33f7129a61e27ef94042734c5110ac756e74e7969012a607fc06a5171fad01e459f763ea790fab85e26c296fd8670da9fce9ec2f879d787605d12a424929a
|
|
7
|
+
data.tar.gz: 5cdf8ed28ed3208bdc06fc70b580f30dc366f6722fd73938fd01c6c7e882c8fd405543b09a9c000fad4f8c32fc80336a795e2a5a7fc0a3377938e077b49c235a
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,35 +1,128 @@
|
|
|
1
1
|
# RackEntraIdAuth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
Optionally, generate `login` and `logout` routes so you can use `login_path` and `logout_path` helpers in your views:
|
|
14
60
|
|
|
15
|
-
|
|
61
|
+
```bash
|
|
62
|
+
bin/rails generate rack_entra_id_auth:routes
|
|
63
|
+
```
|
|
16
64
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
```bash
|
|
121
|
+
bundle install
|
|
122
|
+
bundle exec rake test
|
|
123
|
+
bundle exec rubocop
|
|
124
|
+
```
|
|
32
125
|
|
|
33
126
|
## License
|
|
34
127
|
|
|
35
|
-
|
|
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 =
|
|
98
|
-
config.idp_slo_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/
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
|
2
2
|
require 'ruby-saml'
|
|
3
3
|
|
|
4
4
|
module RackEntraIdAuth
|
|
5
5
|
class Configuration
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
-
{ :
|
|
126
|
-
:
|
|
127
|
-
:
|
|
128
|
-
:
|
|
129
|
-
:
|
|
130
|
-
:
|
|
131
|
-
:
|
|
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
|
-
{ :
|
|
156
|
-
:
|
|
157
|
-
:
|
|
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
|
-
{ :
|
|
179
|
-
:
|
|
180
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
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
|
-
{ :
|
|
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({ :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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(:
|
|
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.
|
|
180
|
-
|
|
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
|
-
{ '
|
|
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 =
|
|
66
|
-
|
|
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| %
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<!DOCTYPE html>
|
|
103
|
-
<html>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</html>
|
|
122
|
-
|
|
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
|
data/lib/rack_entra_id_auth.rb
CHANGED
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.
|
|
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:
|
|
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: '
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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: []
|