Skip to content

Commit

Permalink
Add dynamic success_redirect URLs to be generated (#57)
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
patch0 authored Jun 5, 2023
1 parent 40d8721 commit 7f4fade
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 5 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

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

## [v3.1.0]

Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ RpiAuth.configure do |config|
config.identity_url = 'http://localhost:3002' # The url for the profile instance being used for auth
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
config.scope = 'openid email profile force-consent' # The required OIDC scopes
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
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.
config.bypass_auth = false # Should auth be bypassed and a default user logged in
end
```
Expand Down Expand Up @@ -113,7 +113,7 @@ link_to 'Log out', rpi_auth_logout_path
There are a three possible places the user will end up at following logging in,
in the following order:

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

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

#### Advanced customisation of the login redirect

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.

Fear not! You can set `success_redirect` to a Proc in the configuration, which will then be called in the context of the request.

```ruby
config.success_redirect = -> { request.env['omniauth.origin'] + "?cache_bust=#{Time.now.to_i}&username=#{current_user.nickname}" }
```

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.

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
* `current_user` -- the current logged in user.
* `request.env['omniauth.origin']` (the original `returnTo` value)

**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.

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.

#### Redirecting when logging out

It is also possible to send users to pages within your app when logging out. Just set the `returnTo` parameter again.
Expand Down
16 changes: 14 additions & 2 deletions app/controllers/rpi_auth/auth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ def callback
auth = request.env['omniauth.auth']
self.current_user = RpiAuth.user_model.from_omniauth(auth)

redirect_to RpiAuth.configuration.success_redirect.presence ||
ensure_relative_url(request.env['omniauth.origin'])
redirect_to ensure_relative_url(login_redirect_path)
end

def destroy
Expand All @@ -43,6 +42,19 @@ def failure

private

def login_redirect_path
unless RpiAuth.configuration.success_redirect.is_a?(Proc)
return RpiAuth.configuration.success_redirect || request.env['omniauth.origin']
end

begin
instance_exec(&RpiAuth.configuration.success_redirect)&.to_s
rescue StandardError => e
Rails.logger.warn("Caught #{e} while processing success_redirect proc.")
'/'
end
end

def ensure_relative_url(url)
url = URI.parse(url)

Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/config/initializers/rpi_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
config.identity_url = 'http://localhost:3002'
config.user_model = 'User'

# Redurect to the next URL
config.success_redirect = -> { "#{request.env['omniauth.origin']}?#{{ email: current_user.email }.to_query}" }
config.bypass_auth = false
end
62 changes: 62 additions & 0 deletions spec/dummy/spec/requests/auth_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,68 @@
expect(response).to redirect_to('/success')
end
end

context 'when success_redirect is set as a proc in config' do
let!(:redirect_proc) { -> {} }

before do
RpiAuth.configuration.success_redirect = redirect_proc
end

it 'redirects back to the root page' do
post '/auth/rpi'
expect(response).to redirect_to('/rpi_auth/auth/callback')
follow_redirect!

expect(response).to redirect_to('/')
end

context 'when the proc resolves to something other than nil' do # rubocop:disable RSpec/NestedGroups
# We use `current_user` and `request.env` here as they're available
# in the context of the controller. We use `let!` to make sure the
# proc is defined straightaway, rather than later, when `request` and
# `current_user` might be in scope.
let!(:redirect_proc) do # rubocop:disable RSpec/LetSetup
-> { "#{request.env['omniauth.origin']}/extra?#{{ email: current_user.email }.to_query}" }
end

it 'redirects back to the correct page' do
post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
expect(response).to redirect_to('/rpi_auth/auth/callback')
follow_redirect!

expect(response).to redirect_to("/bar/extra?#{{ email: user.email }.to_query}")
end
end

context 'when the proc raises an exception' do # rubocop:disable RSpec/NestedGroups
# We use `current_user` and `request.env` here as they're available
# in the context of the controller. We use `let!` to make sure the
# proc is defined straightaway, rather than later, when `request` and
# `current_user` might be in scope.
let!(:redirect_proc) do # rubocop:disable RSpec/LetSetup
-> { raise ArgumentError }
end

it 'redirects back to the root page' do
post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
expect(response).to redirect_to('/rpi_auth/auth/callback')
follow_redirect!

expect(response).to redirect_to('/')
end

it 'logs a warning error' do
allow(Rails.logger).to receive(:warn).with(any_args).and_call_original

post '/auth/rpi', params: { returnTo: 'http://www.example.com/bar' }
expect(response).to redirect_to('/rpi_auth/auth/callback')
follow_redirect!

expect(Rails.logger).to have_received(:warn)
end
end
end
end
end
end

0 comments on commit 7f4fade

Please sign in to comment.