Skip to content

Commit 7f4fade

Browse files
authored
Add dynamic success_redirect URLs to be generated (#57)
This allows a `proc` to be used as a `success_redirect` value. This is particularly useful if you need to generated URLs based on the one of the parameters returned or applied during the callback function. It is of particular interest in CoderDojo frontend, which needs to send a user to Zen to log in, but also redirect the user back to the correct page in CoderDojo frontend afterwards. So the Zen URL will need to have a dynamic returnTo parameter based on the original returnTo param supplied to omniauth. ## Considerations * The proc is executed using `instance_exec` to make sure it is run in the context of the controller, rather than the context of the configuration block where it is first defined. * I've tried to document this as best as possible!
1 parent 40d8721 commit 7f4fade

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Allow for customisation of returnTo param on log out (#56)
12+
- Allow for customisation of returnTo param on log out (#56)
13+
- Allow `success_redirect` to be configured as a block that is executed in the context of the AuthController.
1314

1415
## [v3.1.0]
1516

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ RpiAuth.configure do |config|
3333
config.identity_url = 'http://localhost:3002' # The url for the profile instance being used for auth
3434
config.user_model = 'User' # The name of the user model in the host app being used, use the name as a string, not the model itself
3535
config.scope = 'openid email profile force-consent' # The required OIDC scopes
36-
config.success_redirect = '/' # After succesful login the route the user should be redirected to; this will override redirecting the user back to where they were when they started the log in / sign up flow (via `omniauth.origin`), so should be used rarely / with caution
36+
config.success_redirect = '/' # After succesful login the route the user should be redirected to; this will override redirecting the user back to where they were when they started the log in / sign up flow (via `omniauth.origin`), so should be used rarely / with caution. This can be a string or a proc, which is exectuted in the context of the RpiAuth::AuthController.
3737
config.bypass_auth = false # Should auth be bypassed and a default user logged in
3838
end
3939
```
@@ -113,7 +113,7 @@ link_to 'Log out', rpi_auth_logout_path
113113
There are a three possible places the user will end up at following logging in,
114114
in the following order:
115115

116-
1. The `success_redirect` URL.
116+
1. The `success_redirect` URL or proc.
117117
2. The specified `returnTo` URL.
118118
3. The page the user was on (if the Referer header is sent in).
119119
4. The root path of the application.
@@ -139,6 +139,26 @@ meaning (most) users will end up back on the page where they started the auth fl
139139

140140
Finally, if none of these things are set, we end up back at the application root.
141141

142+
#### Advanced customisation of the login redirect
143+
144+
On occasion you may wish to heavily customise the way the login redirect is handled. For example, you may wish to redirect to something a bit more dynamic than either the static `success_redirect` or original HTTP referer/`returnTo` parameter.
145+
146+
Fear not! You can set `success_redirect` to a Proc in the configuration, which will then be called in the context of the request.
147+
148+
```ruby
149+
config.success_redirect = -> { request.env['omniauth.origin'] + "?cache_bust=#{Time.now.to_i}&username=#{current_user.nickname}" }
150+
```
151+
152+
will redirect the user to there `/referer/url?cache_bust=1231231231`, if they started the login process from `/referer/url` page. The proc can return a string or a nil. In the case of a nil, the user will be redirected to the `returnTo` parameter. The return value will be checked to make sure it is local to the app. You cannot redirect to other URLs/hosts with this technique.
153+
154+
You can use variables and methods here that are available in the [RpiAuth::AuthController](app/controllers/rpi_auth/auth_controller.rb), i.e. things like
155+
* `current_user` -- the current logged in user.
156+
* `request.env['omniauth.origin']` (the original `returnTo` value)
157+
158+
**Beware** here be dragons! 🐉 You might get difficult-to-diagnose bugs using this technique. The Proc in your configuration may be tricky to test, so keep it simple. If your Proc raises an exception, the URL returned will default to `/` and there should be a warning in the Rails log saying what happened.
159+
160+
When using this, you will find that Rails needs to be restarted when you change the proc, as the configuration block is only evaluated on start-up.
161+
142162
#### Redirecting when logging out
143163

144164
It is also possible to send users to pages within your app when logging out. Just set the `returnTo` parameter again.

app/controllers/rpi_auth/auth_controller.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ def callback
1616
auth = request.env['omniauth.auth']
1717
self.current_user = RpiAuth.user_model.from_omniauth(auth)
1818

19-
redirect_to RpiAuth.configuration.success_redirect.presence ||
20-
ensure_relative_url(request.env['omniauth.origin'])
19+
redirect_to ensure_relative_url(login_redirect_path)
2120
end
2221

2322
def destroy
@@ -43,6 +42,19 @@ def failure
4342

4443
private
4544

45+
def login_redirect_path
46+
unless RpiAuth.configuration.success_redirect.is_a?(Proc)
47+
return RpiAuth.configuration.success_redirect || request.env['omniauth.origin']
48+
end
49+
50+
begin
51+
instance_exec(&RpiAuth.configuration.success_redirect)&.to_s
52+
rescue StandardError => e
53+
Rails.logger.warn("Caught #{e} while processing success_redirect proc.")
54+
'/'
55+
end
56+
end
57+
4658
def ensure_relative_url(url)
4759
url = URI.parse(url)
4860

spec/dummy/config/initializers/rpi_auth.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
config.identity_url = 'http://localhost:3002'
99
config.user_model = 'User'
1010

11+
# Redurect to the next URL
12+
config.success_redirect = -> { "#{request.env['omniauth.origin']}?#{{ email: current_user.email }.to_query}" }
1113
config.bypass_auth = false
1214
end

spec/dummy/spec/requests/auth_request_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,68 @@
223223
expect(response).to redirect_to('/success')
224224
end
225225
end
226+
227+
context 'when success_redirect is set as a proc in config' do
228+
let!(:redirect_proc) { -> {} }
229+
230+
before do
231+
RpiAuth.configuration.success_redirect = redirect_proc
232+
end
233+
234+
it 'redirects back to the root page' do
235+
post '/auth/rpi'
236+
expect(response).to redirect_to('/rpi_auth/auth/callback')
237+
follow_redirect!
238+
239+
expect(response).to redirect_to('/')
240+
end
241+
242+
context 'when the proc resolves to something other than nil' do # rubocop:disable RSpec/NestedGroups
243+
# We use `current_user` and `request.env` here as they're available
244+
# in the context of the controller. We use `let!` to make sure the
245+
# proc is defined straightaway, rather than later, when `request` and
246+
# `current_user` might be in scope.
247+
let!(:redirect_proc) do # rubocop:disable RSpec/LetSetup
248+
-> { "#{request.env['omniauth.origin']}/extra?#{{ email: current_user.email }.to_query}" }
249+
end
250+
251+
it 'redirects back to the correct page' do
252+
post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
253+
expect(response).to redirect_to('/rpi_auth/auth/callback')
254+
follow_redirect!
255+
256+
expect(response).to redirect_to("/bar/extra?#{{ email: user.email }.to_query}")
257+
end
258+
end
259+
260+
context 'when the proc raises an exception' do # rubocop:disable RSpec/NestedGroups
261+
# We use `current_user` and `request.env` here as they're available
262+
# in the context of the controller. We use `let!` to make sure the
263+
# proc is defined straightaway, rather than later, when `request` and
264+
# `current_user` might be in scope.
265+
let!(:redirect_proc) do # rubocop:disable RSpec/LetSetup
266+
-> { raise ArgumentError }
267+
end
268+
269+
it 'redirects back to the root page' do
270+
post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
271+
expect(response).to redirect_to('/rpi_auth/auth/callback')
272+
follow_redirect!
273+
274+
expect(response).to redirect_to('/')
275+
end
276+
277+
it 'logs a warning error' do
278+
allow(Rails.logger).to receive(:warn).with(any_args).and_call_original
279+
280+
post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
281+
expect(response).to redirect_to('/rpi_auth/auth/callback')
282+
follow_redirect!
283+
284+
expect(Rails.logger).to have_received(:warn)
285+
end
286+
end
287+
end
226288
end
227289
end
228290
end

0 commit comments

Comments
 (0)