Skip to content

Commit f222bbe

Browse files
committed
WIP
1 parent e54cc7a commit f222bbe

File tree

14 files changed

+401
-4
lines changed

14 files changed

+401
-4
lines changed

gemfiles/rails_7.2.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)
@@ -103,6 +104,9 @@ GEM
103104
coderay (1.1.3)
104105
concurrent-ruby (1.3.5)
105106
connection_pool (2.5.0)
107+
crack (1.0.0)
108+
bigdecimal
109+
rexml
106110
crass (1.0.6)
107111
date (3.4.1)
108112
diff-lcs (1.6.1)
@@ -129,6 +133,7 @@ GEM
129133
ffi (1.17.1-x86_64-linux-musl)
130134
globalid (1.2.1)
131135
activesupport (>= 6.1)
136+
hashdiff (1.1.2)
132137
hashie (5.0.0)
133138
i18n (1.14.7)
134139
concurrent-ruby (~> 1.0)
@@ -145,6 +150,8 @@ GEM
145150
bindata
146151
faraday (~> 2.0)
147152
faraday-follow_redirects
153+
jwt (2.10.1)
154+
base64
148155
language_server-protocol (3.17.0.4)
149156
lint_roller (1.1.0)
150157
listen (3.9.0)
@@ -164,6 +171,8 @@ GEM
164171
method_source (1.1.0)
165172
mini_mime (1.1.5)
166173
minitest (5.25.5)
174+
multi_xml (0.7.1)
175+
bigdecimal (~> 3.1)
167176
net-http (0.6.0)
168177
uri
169178
net-imap (0.5.6)
@@ -192,6 +201,13 @@ GEM
192201
racc (~> 1.4)
193202
nokogiri (1.18.7-x86_64-linux-musl)
194203
racc (~> 1.4)
204+
oauth2 (2.0.9)
205+
faraday (>= 0.17.3, < 3.0)
206+
jwt (>= 1.0, < 3.0)
207+
multi_xml (~> 0.5)
208+
rack (>= 1.2, < 4)
209+
snaky_hash (~> 2.0)
210+
version_gem (~> 1.1)
195211
omniauth (2.1.3)
196212
hashie (>= 3.4.6)
197213
rack (>= 2.2.3)
@@ -294,6 +310,7 @@ GEM
294310
regexp_parser (2.10.0)
295311
reline (0.6.1)
296312
io-console (~> 0.5)
313+
rexml (3.4.1)
297314
rspec-core (3.13.3)
298315
rspec-support (~> 3.13.0)
299316
rspec-expectations (3.13.3)
@@ -348,6 +365,9 @@ GEM
348365
simplecov_json_formatter (~> 0.1)
349366
simplecov-html (0.13.1)
350367
simplecov_json_formatter (0.1.4)
368+
snaky_hash (2.0.1)
369+
hashie
370+
version_gem (~> 1.1, >= 1.1.1)
351371
stringio (3.1.6)
352372
swd (2.0.3)
353373
activesupport (>= 3)
@@ -366,10 +386,15 @@ GEM
366386
validate_url (1.0.15)
367387
activemodel (>= 3.0.0)
368388
public_suffix
389+
version_gem (1.1.7)
369390
webfinger (2.1.3)
370391
activesupport
371392
faraday (~> 2.0)
372393
faraday-follow_redirects
394+
webmock (3.25.1)
395+
addressable (>= 2.8.0)
396+
crack (>= 0.3.2)
397+
hashdiff (>= 0.4.0, < 2.0.0)
373398
websocket-driver (0.7.7)
374399
base64
375400
websocket-extensions (>= 0.1.0)
@@ -402,6 +427,7 @@ DEPENDENCIES
402427
rubocop-rails
403428
rubocop-rspec
404429
simplecov
430+
webmock
405431

406432
BUNDLED WITH
407433
2.3.27

lib/rpi_auth.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'rpi_auth/engine'
55
require 'rpi_auth/configuration'
66
require 'rpi_auth/models/authenticatable'
7+
require 'rpi_auth/models/with_tokens'
78
require 'omniauth/rails_csrf_protection'
89

910
module RpiAuth
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
# TODO: rescuing all ArgumentErrors is risky
26+
rescue OAuth2::Error, ArgumentError
27+
reset_session
28+
end
29+
end
30+
end
31+
end

lib/rpi_auth/models/authenticatable.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ module Authenticatable
2323
include ActiveModel::Serialization
2424

2525
attr_accessor :user_id, *PROFILE_KEYS
26+
end
2627

27-
# Allow serialization
28-
def attributes
29-
(['user_id'] + PROFILE_KEYS).index_with { |_k| nil }
30-
end
28+
def attribute_keys
29+
%w[user_id] + PROFILE_KEYS
30+
end
31+
32+
# Allow serialization
33+
def attributes
34+
attribute_keys.map(&:to_s).index_with { |_k| nil }
3135
end
3236

3337
class_methods do

lib/rpi_auth/models/with_tokens.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require 'rpi_auth/oauth_client'
4+
5+
module RpiAuth
6+
module Models
7+
module WithTokens
8+
extend ActiveSupport::Concern
9+
10+
include Authenticatable
11+
12+
included do
13+
attr_accessor :access_token, :expires_at, :refresh_token
14+
15+
alias id user_id
16+
alias id= user_id=
17+
end
18+
19+
def attribute_keys
20+
super + %w[access_token expires_at refresh_token user_id]
21+
end
22+
23+
def refresh_credentials!
24+
oauth_client = OauthClient.new
25+
credentials = oauth_client.refresh_credentials(access_token:, refresh_token:)
26+
27+
assign_attributes(**credentials)
28+
end
29+
30+
class_methods do
31+
def from_omniauth(auth)
32+
user = super
33+
return unless user
34+
35+
if auth.credentials
36+
user.access_token = auth.credentials.token
37+
user.refresh_token = auth.credentials.refresh_token
38+
user.expires_at = Time.now.to_i + auth.credentials.expires_in
39+
end
40+
41+
user
42+
end
43+
end
44+
end
45+
end
46+
end

lib/rpi_auth/oauth_client.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
require 'oauth2/client'
4+
require 'oauth2/access_token'
5+
6+
module RpiAuth
7+
class OauthClient
8+
attr_reader :client
9+
10+
def initialize
11+
@client = OAuth2::Client.new(
12+
RpiAuth.configuration.auth_client_id,
13+
RpiAuth.configuration.auth_client_secret,
14+
site: RpiAuth.configuration.auth_token_url,
15+
token_url: RpiAuth.configuration.token_endpoint.path,
16+
authorize_url: RpiAuth.configuration.authorization_endpoint.path
17+
)
18+
end
19+
20+
def refresh_credentials(**credentials)
21+
new_credentials = if RpiAuth.configuration.bypass_auth
22+
credentials.merge(expires_at: 1.hour.from_now)
23+
else
24+
OAuth2::AccessToken.from_hash(client, credentials).refresh.to_hash
25+
end
26+
27+
{
28+
access_token: new_credentials[:access_token],
29+
refresh_token: new_credentials[:refresh_token],
30+
expires_at: new_credentials[:expires_at].to_i
31+
}
32+
end
33+
end
34+
end

rpi_auth.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
2121

2222
spec.required_ruby_version = '>= 3.1.0'
2323

24+
spec.add_dependency 'oauth2'
2425
spec.add_dependency 'omniauth_openid_connect', '~> 0.7.1'
2526
spec.add_dependency 'omniauth-rails_csrf_protection', '~> 1.0.0'
2627
spec.add_dependency 'rails', '>= 6.1.4'
@@ -36,4 +37,5 @@ Gem::Specification.new do |spec|
3637
spec.add_development_dependency 'rubocop-rails'
3738
spec.add_development_dependency 'rubocop-rspec'
3839
spec.add_development_dependency 'simplecov'
40+
spec.add_development_dependency 'webmock'
3941
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
require 'rpi_auth/controllers/current_user'
2+
require 'rpi_auth/controllers/auto_refreshing_token'
3+
14
class ApplicationController < ActionController::Base
25
include RpiAuth::Controllers::CurrentUser
6+
include RpiAuth::Controllers::AutoRefreshingToken
37
end

spec/dummy/app/models/user.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
class User
22
include RpiAuth::Models::Authenticatable
3+
include RpiAuth::Models::WithTokens
34
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)