Skip to content

Commit 057049a

Browse files
committed
Add Controllers::AutoRefreshingToken concern
This can optionally be included into your controller to automatically use the user's refresh token to obtain a new access token when the old one expires. It adds a before action to the target controller which automatically calls `OauthClient#refresh_credentials!` if the user is signed in and their access token has expired. Again this has been substantially copied from `code-club-frontend`: - `app/controllers/application_controller.rb` [1] - `spec/requests/refresh_credentials_spec.rb` [2] Some things that I've changed: - Moved functionality from `ApplicationController` into an `RpiAuth` namespaced concern which can be included into the controller in the Rails app. - Make `AutoRefreshingToken#refresh_credentials_if_needed` private since it's not a controller action. - Use the home page in the dummy app in the request spec instead of the home page in `code-club-frontend`. - Construct a user directly in the request spec; there's no user factory in the `rpi_auth` gem. - Set `RpiAuth.configuration.user_model = 'User'` in the `before` block as per the other request specs. I'm slightly concerned about a few details of the implementation of `AutoRefreshingToken#refresh_credentials_if_needed`: - Rescuing all `ArgumentError` exceptions seems risky, because it's quite a common exception. It would be better to be more specific. - Even rescuing all `OAuth2::Error` exceptions seems a bit broad, although I haven't investigated what this encompasses. - While resetting the session in the event of an exception seems OK security-wise, it probably doesn't result in a great user experience. It would be better to redirect and/or display an error message to the user to explain what's happened. [1]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/app/controllers/application_controller.rb#L8 [2]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/spec/requests/refresh_credentials_spec.rb
1 parent fdfc794 commit 057049a

File tree

3 files changed

+128
-0
lines changed

3 files changed

+128
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require 'oauth2/error'
4+
5+
module RpiAuth
6+
module Controllers
7+
module AutoRefreshingToken
8+
extend ActiveSupport::Concern
9+
10+
include CurrentUser
11+
12+
included do
13+
before_action :refresh_credentials_if_needed
14+
end
15+
16+
private
17+
18+
def refresh_credentials_if_needed
19+
return unless current_user
20+
21+
return if Time.now.to_i < current_user.expires_at
22+
23+
current_user.refresh_credentials!
24+
self.current_user = current_user
25+
rescue OAuth2::Error, ArgumentError
26+
reset_session
27+
end
28+
end
29+
end
30+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
require 'rpi_auth/controllers/current_user'
2+
require 'rpi_auth/controllers/auto_refreshing_token'
3+
24
class ApplicationController < ActionController::Base
35
include RpiAuth::Controllers::CurrentUser
6+
include RpiAuth::Controllers::AutoRefreshingToken
47
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
require 'oauth2/error'
6+
7+
RSpec.describe 'Refreshing the auth token', type: :request do
8+
include ActiveSupport::Testing::TimeHelpers
9+
10+
subject(:request) { get root_path }
11+
12+
let(:logged_in_text) { 'Log out' }
13+
let(:stub_oauth_client) { instance_double(RpiAuth::OauthClient) }
14+
15+
before do
16+
freeze_time
17+
allow(RpiAuth::OauthClient).to receive(:new).and_return(stub_oauth_client)
18+
allow(stub_oauth_client).to receive(:refresh_credentials).with(any_args)
19+
RpiAuth.configuration.user_model = 'User'
20+
end
21+
22+
after do
23+
unfreeze_time
24+
end
25+
26+
shared_examples 'there is no attempt to renew the token' do
27+
it 'calls refresh_credentials on the oauth client' do
28+
request
29+
expect(stub_oauth_client).not_to have_received(:refresh_credentials)
30+
end
31+
end
32+
33+
shared_examples 'there is an attempt to renew the token' do
34+
it 'does not call refresh_credentials on the oauth client' do
35+
request
36+
expect(stub_oauth_client).to have_received(:refresh_credentials)
37+
end
38+
end
39+
40+
shared_examples 'the user is logged in' do
41+
it do
42+
request
43+
expect(response.body).to include(logged_in_text)
44+
end
45+
end
46+
47+
shared_examples 'the user is logged out' do
48+
it do
49+
request
50+
expect(response.body).not_to include(logged_in_text)
51+
end
52+
end
53+
54+
context 'when not logged in' do
55+
it_behaves_like 'the user is logged out'
56+
it_behaves_like 'there is no attempt to renew the token'
57+
end
58+
59+
context 'when logged in' do
60+
let(:user) { User.new(expires_at:) }
61+
62+
before do
63+
log_in(user:)
64+
end
65+
66+
context 'when the access token has not expired' do
67+
let(:expires_at) { 10.seconds.from_now }
68+
69+
it_behaves_like 'the user is logged in'
70+
it_behaves_like 'there is no attempt to renew the token'
71+
end
72+
73+
context 'when the access token has expired' do
74+
let(:expires_at) { 10.seconds.ago }
75+
76+
before do
77+
allow(stub_oauth_client).to receive(:refresh_credentials).with(any_args).and_return({ access_token: 'foo',
78+
refresh_token: 'bar',
79+
expires_at: 10.minutes.from_now })
80+
end
81+
82+
it_behaves_like 'the user is logged in'
83+
it_behaves_like 'there is an attempt to renew the token'
84+
85+
context 'when an OAuth error is raised' do
86+
before do
87+
allow(stub_oauth_client).to receive(:refresh_credentials).with(any_args).and_raise(OAuth2::Error.new('blargh'))
88+
end
89+
90+
it_behaves_like 'the user is logged out'
91+
it_behaves_like 'there is an attempt to renew the token'
92+
end
93+
end
94+
end
95+
end

0 commit comments

Comments
 (0)