Skip to content

Commit 177cd7a

Browse files
authored
Add access token-related functionality including auto-refresh (#83)
The following new functionality has been **extracted** from `code-club-frontend` and `experience-cs` to reduce duplication and hopefully make useful functionality available to other Rails apps using this gem. [This pull request](RaspberryPiFoundation/experience-cs#295) where it was recently added to `experience-cs` and [this follow-up pull request](RaspberryPiFoundation/experience-cs#315) are good references. * ✅ I've used the new `rpi-auth` code in the context of `experience-cs` in [this pull request](RaspberryPiFoundation/experience-cs#317) and it seems to be OK. * ✅ I've used the new `rpi-auth` code in the context of `code-club-frontend` in [this pull request](RaspberryPiFoundation/code-club-frontend#280) and all the test pass. ## Add `RpiAuth::Models::WithTokens` concern This can optionally be included into your user model in order to obtain an access token, a refresh token, and an expiry time when logging in. These attributes are set by the same mechanism in `AuthController#callback` that populates `user_id` on the user via `Authenticatable.from_omniauth`, but instead calls `WithTokens.from_omniauth` which in turn calls `Authenticatable.from_omniauth` via the ancestor chain. This also relies on: - `RpiAuth.configuration.scope` including the "offline" scope in the Rails app which is using the `rpi_auth` gem. - In the `profile` app `hydra_client` config for the Rails app, `grant_types` must include "refresh_token" and `scope` must include "offline". This has been substantially copied from `code-club-frontend`: - [`app/models/user.rb`][1] - [`spec/models/user_spec.rb`][2] ## Add `RpiAuth::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. The latter makes a request to the `profile` app using the refresh token to obtain a new access token. Again this has been substantially copied from `code-club-frontend`: - [`app/controllers/application_controller.rb`][3] - [`spec/requests/refresh_credentials_spec.rb`][4] - [`lib/oauth_client.rb`][5] - [`spec/lib/oauth_client_spec.rb`][6] ## Questions/issues In general I'd prefer to postpone tackling any of the following to separate PRs, because I think it's useful to have a version of the code which matches what's currently in `code-club-frontend` & `experience-cs` as closely as possible before we start changing things further. However, hopefully the following notes are useful: - I haven't included the aliasing of `#user_id` & `#user_id=` to `#id` & `#id=` respectively, because I'm not yet sure whether it's more generally useful - we don't seem to need it in `experience-cs` yet. - I think it might simplify things a bit to move some of the classes/modules that are currently in `lib` into relevant directories under `app`, e.g. I think that moving `lib/rpi_auth/models/authenticatable.rb` -> `app/models/rpi_auth/authenticatable.rb` (or maybe `app/models/concerns/rpi_auth/authenticatable.rb`) would mean the classes were automatically loaded. This seems like a more idiomatic use of a Rails engine. However, that's probably a bigger piece of work, especially to make sure it works with the relevant versions of Rails. - 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. At the very least it would be good to log/report the details of the exception. - 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/models/user.rb [2]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/spec/models/user_spec.rb [3]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/app/controllers/application_controller.rb#L8 [4]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/spec/requests/refresh_credentials_spec.rb [5]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/f7e965798d910584fed0d1eb7867f32a899f9ce8/lib/oauth_client.rb [6]: https://github.com/RaspberryPiFoundation/code-club-frontend/blob/main/spec/lib/oauth_client_spec.rb
2 parents 9229b64 + 2a54a8d commit 177cd7a

21 files changed

+504
-7
lines changed

.rubocop_todo.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# Include: **/*.gemspec, **/Gemfile, **/gems.rb
1313
Gemspec/DevelopmentDependencies:
1414
Exclude:
15-
- 'rpi_auth.gemspec'
15+
- "rpi_auth.gemspec"
1616

1717
# Offense count: 1
1818
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
@@ -43,4 +43,5 @@ RSpec/NestedGroups:
4343
# Include: **/*_spec.rb
4444
RSpec/SpecFilePathFormat:
4545
Exclude:
46-
- 'spec/rpi_auth/models/authenticatable_spec.rb'
46+
- "spec/rpi_auth/models/authenticatable_spec.rb"
47+
- "spec/rpi_auth/models/with_tokens_spec.rb"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- Add access token-related functionality including auto-refresh (#83)
1112

1213
### Fixed
1314
- Fix use of `User#expires_at` in `SpecHelpers#stub_auth_for` (#82)

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,29 @@ class in `config/application.rb`.
187187
config.railties_order = [RpiAuth::Engine, :main_app, :all]
188188
```
189189

190+
### Obtaining an access token for user
191+
192+
This optional behaviour is useful if your Rails app (which is using this gem)
193+
needs to use a RPF API which required authentication via an OAuth2 access
194+
token.
195+
196+
Include the `RpiAuth::Models::WithTokens` concern (which depends on the
197+
`RpiAuth::Models::Authenticatable` concern) into your user model in order to
198+
add `access_token`, `refresh_token` & `expires_at` attributes. These methods
199+
are automatically populated by `RpiAuth::AuthController#callback` via the
200+
`RpiAuth::Models::WithTokens.from_omniauth` method.
201+
202+
This also relies on the following:
203+
- `RpiAuth.configuration.scope` including the "offline" scope in the Rails app
204+
which is using the `rpi_auth` gem.
205+
- In the `profile` app `hydra_client` config for the Rails app, `grant_types`
206+
must include "refresh_token" and `scope` must include "offline".
207+
208+
Include the `RpiAuth::Controllers::AutoRefreshingToken` concern (which depends
209+
on the `RpiAuth::Controllers::CurrentUser` concern) into your controller so
210+
that when the user's access token expires, a new one is obtained using the
211+
user's refresh token.
212+
190213
## Test helpers and routes
191214

192215
There are some standardised test helpers in `RpiAuth::SpecHelpers` that can be used when testing.

gemfiles/rails_6.1.gemfile.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ PATH
22
remote: ..
33
specs:
44
rpi_auth (4.0.0)
5+
oauth2
56
omniauth-rails_csrf_protection (~> 1.0.0)
67
omniauth_openid_connect (~> 0.7.1)
78
rails (>= 6.1.4)
@@ -74,6 +75,7 @@ GEM
7475
ast (2.4.3)
7576
attr_required (1.0.2)
7677
base64 (0.2.0)
78+
bigdecimal (3.1.9)
7779
bindata (2.5.1)
7880
builder (3.3.0)
7981
byebug (12.0.0)
@@ -88,6 +90,9 @@ GEM
8890
xpath (~> 3.2)
8991
coderay (1.1.3)
9092
concurrent-ruby (1.3.5)
93+
crack (1.0.0)
94+
bigdecimal
95+
rexml
9196
crass (1.0.6)
9297
date (3.4.1)
9398
diff-lcs (1.6.1)
@@ -106,6 +111,7 @@ GEM
106111
ffi (1.17.1)
107112
globalid (1.2.1)
108113
activesupport (>= 6.1)
114+
hashdiff (1.1.2)
109115
hashie (5.0.0)
110116
i18n (1.14.7)
111117
concurrent-ruby (~> 1.0)
@@ -117,6 +123,8 @@ GEM
117123
bindata
118124
faraday (~> 2.0)
119125
faraday-follow_redirects
126+
jwt (2.10.1)
127+
base64
120128
language_server-protocol (3.17.0.4)
121129
lint_roller (1.1.0)
122130
listen (3.9.0)
@@ -137,6 +145,8 @@ GEM
137145
mini_mime (1.1.5)
138146
mini_portile2 (2.8.8)
139147
minitest (5.25.5)
148+
multi_xml (0.7.1)
149+
bigdecimal (~> 3.1)
140150
net-http (0.6.0)
141151
uri
142152
net-imap (0.5.6)
@@ -152,6 +162,13 @@ GEM
152162
nokogiri (1.18.7)
153163
mini_portile2 (~> 2.8.2)
154164
racc (~> 1.4)
165+
oauth2 (2.0.9)
166+
faraday (>= 0.17.3, < 3.0)
167+
jwt (>= 1.0, < 3.0)
168+
multi_xml (~> 0.5)
169+
rack (>= 1.2, < 4)
170+
snaky_hash (~> 2.0)
171+
version_gem (~> 1.1)
155172
omniauth (2.1.3)
156173
hashie (>= 3.4.6)
157174
rack (>= 2.2.3)
@@ -237,6 +254,7 @@ GEM
237254
rb-inotify (0.11.1)
238255
ffi (~> 1.0)
239256
regexp_parser (2.10.0)
257+
rexml (3.4.1)
240258
rspec-core (3.13.3)
241259
rspec-support (~> 3.13.0)
242260
rspec-expectations (3.13.3)
@@ -290,6 +308,9 @@ GEM
290308
simplecov_json_formatter (~> 0.1)
291309
simplecov-html (0.13.1)
292310
simplecov_json_formatter (0.1.4)
311+
snaky_hash (2.0.1)
312+
hashie
313+
version_gem (~> 1.1, >= 1.1.1)
293314
sprockets (4.2.1)
294315
concurrent-ruby (~> 1.0)
295316
rack (>= 2.2.4, < 4)
@@ -313,10 +334,15 @@ GEM
313334
validate_url (1.0.15)
314335
activemodel (>= 3.0.0)
315336
public_suffix
337+
version_gem (1.1.7)
316338
webfinger (2.1.3)
317339
activesupport
318340
faraday (~> 2.0)
319341
faraday-follow_redirects
342+
webmock (3.25.1)
343+
addressable (>= 2.8.0)
344+
crack (>= 0.3.2)
345+
hashdiff (>= 0.4.0, < 2.0.0)
320346
websocket-driver (0.7.7)
321347
base64
322348
websocket-extensions (>= 0.1.0)
@@ -342,6 +368,7 @@ DEPENDENCIES
342368
rubocop-rails
343369
rubocop-rspec
344370
simplecov
371+
webmock
345372

346373
BUNDLED WITH
347374
2.3.27

gemfiles/rails_7.0.gemfile.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ PATH
22
remote: ..
33
specs:
44
rpi_auth (4.0.0)
5+
oauth2
56
omniauth-rails_csrf_protection (~> 1.0.0)
67
omniauth_openid_connect (~> 0.7.1)
78
rails (>= 6.1.4)
@@ -80,6 +81,7 @@ GEM
8081
ast (2.4.3)
8182
attr_required (1.0.2)
8283
base64 (0.2.0)
84+
bigdecimal (3.1.9)
8385
bindata (2.5.1)
8486
builder (3.3.0)
8587
byebug (12.0.0)
@@ -94,6 +96,9 @@ GEM
9496
xpath (~> 3.2)
9597
coderay (1.1.3)
9698
concurrent-ruby (1.3.5)
99+
crack (1.0.0)
100+
bigdecimal
101+
rexml
97102
crass (1.0.6)
98103
date (3.4.1)
99104
diff-lcs (1.6.1)
@@ -112,6 +117,7 @@ GEM
112117
ffi (1.17.1)
113118
globalid (1.2.1)
114119
activesupport (>= 6.1)
120+
hashdiff (1.1.2)
115121
hashie (5.0.0)
116122
i18n (1.14.7)
117123
concurrent-ruby (~> 1.0)
@@ -123,6 +129,8 @@ GEM
123129
bindata
124130
faraday (~> 2.0)
125131
faraday-follow_redirects
132+
jwt (2.10.1)
133+
base64
126134
language_server-protocol (3.17.0.4)
127135
lint_roller (1.1.0)
128136
listen (3.9.0)
@@ -143,6 +151,8 @@ GEM
143151
mini_mime (1.1.5)
144152
mini_portile2 (2.8.8)
145153
minitest (5.25.5)
154+
multi_xml (0.7.1)
155+
bigdecimal (~> 3.1)
146156
net-http (0.6.0)
147157
uri
148158
net-imap (0.5.6)
@@ -158,6 +168,13 @@ GEM
158168
nokogiri (1.18.7)
159169
mini_portile2 (~> 2.8.2)
160170
racc (~> 1.4)
171+
oauth2 (2.0.9)
172+
faraday (>= 0.17.3, < 3.0)
173+
jwt (>= 1.0, < 3.0)
174+
multi_xml (~> 0.5)
175+
rack (>= 1.2, < 4)
176+
snaky_hash (~> 2.0)
177+
version_gem (~> 1.1)
161178
omniauth (2.1.3)
162179
hashie (>= 3.4.6)
163180
rack (>= 2.2.3)
@@ -243,6 +260,7 @@ GEM
243260
rb-inotify (0.11.1)
244261
ffi (~> 1.0)
245262
regexp_parser (2.10.0)
263+
rexml (3.4.1)
246264
rspec-core (3.13.3)
247265
rspec-support (~> 3.13.0)
248266
rspec-expectations (3.13.3)
@@ -296,6 +314,9 @@ GEM
296314
simplecov_json_formatter (~> 0.1)
297315
simplecov-html (0.13.1)
298316
simplecov_json_formatter (0.1.4)
317+
snaky_hash (2.0.1)
318+
hashie
319+
version_gem (~> 1.1, >= 1.1.1)
299320
swd (2.0.3)
300321
activesupport (>= 3)
301322
attr_required (>= 0.0.5)
@@ -312,10 +333,15 @@ GEM
312333
validate_url (1.0.15)
313334
activemodel (>= 3.0.0)
314335
public_suffix
336+
version_gem (1.1.7)
315337
webfinger (2.1.3)
316338
activesupport
317339
faraday (~> 2.0)
318340
faraday-follow_redirects
341+
webmock (3.25.1)
342+
addressable (>= 2.8.0)
343+
crack (>= 0.3.2)
344+
hashdiff (>= 0.4.0, < 2.0.0)
319345
websocket-driver (0.7.7)
320346
base64
321347
websocket-extensions (>= 0.1.0)
@@ -341,6 +367,7 @@ DEPENDENCIES
341367
rubocop-rails
342368
rubocop-rspec
343369
simplecov
370+
webmock
344371

345372
BUNDLED WITH
346373
2.3.27

gemfiles/rails_7.1.gemfile.lock

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ PATH
22
remote: ..
33
specs:
44
rpi_auth (4.0.0)
5+
oauth2
56
omniauth-rails_csrf_protection (~> 1.0.0)
67
omniauth_openid_connect (~> 0.7.1)
78
rails (>= 6.1.4)
@@ -109,6 +110,9 @@ GEM
109110
coderay (1.1.3)
110111
concurrent-ruby (1.3.5)
111112
connection_pool (2.5.0)
113+
crack (1.0.0)
114+
bigdecimal
115+
rexml
112116
crass (1.0.6)
113117
date (3.4.1)
114118
diff-lcs (1.6.1)
@@ -128,6 +132,7 @@ GEM
128132
ffi (1.17.1)
129133
globalid (1.2.1)
130134
activesupport (>= 6.1)
135+
hashdiff (1.1.2)
131136
hashie (5.0.0)
132137
i18n (1.14.7)
133138
concurrent-ruby (~> 1.0)
@@ -144,6 +149,8 @@ GEM
144149
bindata
145150
faraday (~> 2.0)
146151
faraday-follow_redirects
152+
jwt (2.10.1)
153+
base64
147154
language_server-protocol (3.17.0.4)
148155
lint_roller (1.1.0)
149156
listen (3.9.0)
@@ -164,6 +171,8 @@ GEM
164171
mini_mime (1.1.5)
165172
mini_portile2 (2.8.8)
166173
minitest (5.25.5)
174+
multi_xml (0.7.1)
175+
bigdecimal (~> 3.1)
167176
mutex_m (0.3.0)
168177
net-http (0.6.0)
169178
uri
@@ -180,6 +189,13 @@ GEM
180189
nokogiri (1.18.7)
181190
mini_portile2 (~> 2.8.2)
182191
racc (~> 1.4)
192+
oauth2 (2.0.9)
193+
faraday (>= 0.17.3, < 3.0)
194+
jwt (>= 1.0, < 3.0)
195+
multi_xml (~> 0.5)
196+
rack (>= 1.2, < 4)
197+
snaky_hash (~> 2.0)
198+
version_gem (~> 1.1)
183199
omniauth (2.1.3)
184200
hashie (>= 3.4.6)
185201
rack (>= 2.2.3)
@@ -282,6 +298,7 @@ GEM
282298
regexp_parser (2.10.0)
283299
reline (0.6.1)
284300
io-console (~> 0.5)
301+
rexml (3.4.1)
285302
rspec-core (3.13.3)
286303
rspec-support (~> 3.13.0)
287304
rspec-expectations (3.13.3)
@@ -336,6 +353,9 @@ GEM
336353
simplecov_json_formatter (~> 0.1)
337354
simplecov-html (0.13.1)
338355
simplecov_json_formatter (0.1.4)
356+
snaky_hash (2.0.1)
357+
hashie
358+
version_gem (~> 1.1, >= 1.1.1)
339359
stringio (3.1.6)
340360
swd (2.0.3)
341361
activesupport (>= 3)
@@ -353,10 +373,15 @@ GEM
353373
validate_url (1.0.15)
354374
activemodel (>= 3.0.0)
355375
public_suffix
376+
version_gem (1.1.7)
356377
webfinger (2.1.3)
357378
activesupport
358379
faraday (~> 2.0)
359380
faraday-follow_redirects
381+
webmock (3.25.1)
382+
addressable (>= 2.8.0)
383+
crack (>= 0.3.2)
384+
hashdiff (>= 0.4.0, < 2.0.0)
360385
websocket-driver (0.7.7)
361386
base64
362387
websocket-extensions (>= 0.1.0)
@@ -382,6 +407,7 @@ DEPENDENCIES
382407
rubocop-rails
383408
rubocop-rspec
384409
simplecov
410+
webmock
385411

386412
BUNDLED WITH
387413
2.3.27

0 commit comments

Comments
 (0)