Skip to content

Commit fdfc794

Browse files
committed
Add 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". Again this has been substantially copied from `code-club-frontend`: - `app/models/user.rb` [1] - `spec/models/user_spec.rb` [2] Some things that I've changed: - Moved methods on `User` into an `RpiAuth` namespaced concern which can be included into the user model in the Rails app. - Override the recently added `Authenticatable#attribute_keys` method, but call `super` to avoid duplication. - I haven't included the aliases for `#id` & `#id=` methods, because it wasn't immediately obvious that these are relevant for projects other than `code-club-frontend`. - Use `RpiAuth.configuration.bypass_auth` instead of reading `BYPASS_AUTH` env var directly as per this PR [3] - Use `RpiAuth.configure` in the `before` block instead of using `ClimateControl.modify` in an `around` block. The former is possible, because the `RpiAuth` configuration is reset before every example [4]. [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/experience-cs/pull/315 [4]: https://github.com/RaspberryPiFoundation/rpi-auth/blob/e54cc7a30c61cf43874a38c79f1e2f3e2b6d2103/spec/spec_helper.rb#L132-L136
1 parent 307c7e0 commit fdfc794

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed

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

lib/rpi_auth/models/with_tokens.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
end
15+
16+
def attribute_keys
17+
super + %w[access_token expires_at refresh_token user_id]
18+
end
19+
20+
def refresh_credentials!
21+
oauth_client = OauthClient.new
22+
credentials = oauth_client.refresh_credentials(access_token:, refresh_token:)
23+
24+
assign_attributes(**credentials)
25+
end
26+
27+
class_methods do
28+
def from_omniauth(auth)
29+
user = super
30+
return unless user
31+
32+
if auth.credentials
33+
user.access_token = auth.credentials.token
34+
user.refresh_token = auth.credentials.refresh_token
35+
user.expires_at = Time.now.to_i + auth.credentials.expires_in
36+
end
37+
38+
user
39+
end
40+
end
41+
end
42+
end
43+
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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
class DummyUser
6+
include RpiAuth::Models::Authenticatable
7+
include RpiAuth::Models::WithTokens
8+
end
9+
10+
RSpec.describe DummyUser do
11+
include ActiveSupport::Testing::TimeHelpers
12+
13+
subject(:user) { described_class.new }
14+
15+
it { is_expected.to respond_to(:access_token) }
16+
it { is_expected.to respond_to(:refresh_token) }
17+
it { is_expected.to respond_to(:expires_at) }
18+
19+
describe '#refresh_credentials!' do
20+
subject(:refresh_credentials) { user.refresh_credentials! }
21+
22+
let(:stub_oauth_client) { instance_double(RpiAuth::OauthClient) }
23+
let(:new_tokens) { { access_token: 'foo', refresh_token: 'bar', expires_at: 1.hour.from_now.utc } }
24+
25+
before do
26+
allow(RpiAuth::OauthClient).to receive(:new).and_return(stub_oauth_client)
27+
allow(stub_oauth_client).to receive(:refresh_credentials).with(access_token: user.access_token, refresh_token: user.refresh_token).and_return(new_tokens)
28+
end
29+
30+
it { expect { refresh_credentials }.to change(user, :access_token).from(user.access_token).to(new_tokens[:access_token]) }
31+
it { expect { refresh_credentials }.to change(user, :refresh_token).from(user.refresh_token).to(new_tokens[:refresh_token]) }
32+
it { expect { refresh_credentials }.to change(user, :expires_at).from(user.expires_at).to(new_tokens[:expires_at]) }
33+
end
34+
35+
describe '#from_omniauth' do
36+
subject(:user) { described_class.from_omniauth(auth) }
37+
38+
let(:omniauth_user) { described_class.new }
39+
let(:info) { omniauth_user.serializable_hash }
40+
let(:credentials) { { token: SecureRandom.base64(12), refresh_token: SecureRandom.base64(12), expires_in: rand(60..240) } }
41+
42+
let(:auth) do
43+
OmniAuth::AuthHash.new(
44+
{
45+
provider: 'rpi',
46+
uid: omniauth_user.user_id,
47+
credentials:,
48+
extra: {
49+
raw_info: info
50+
}
51+
}
52+
)
53+
end
54+
55+
it { is_expected.to be_a described_class }
56+
57+
it 'sets the access_token' do
58+
expect(user.access_token).to eq credentials[:token]
59+
end
60+
61+
it 'sets the refresh_token' do
62+
expect(user.refresh_token).to eq credentials[:refresh_token]
63+
end
64+
65+
context 'when no credentials are returned' do
66+
let(:credentials) { nil }
67+
68+
it 'sets the access_token to be nil' do
69+
expect(user.access_token).to be_nil
70+
end
71+
end
72+
73+
it 'sets the expires_at time correctly' do
74+
freeze_time do
75+
expect(user.expires_at).to eq credentials[:expires_in].seconds.from_now.to_i
76+
end
77+
end
78+
79+
context 'with unusual keys in info' do
80+
let(:info) { { foo: :bar, flibble: :woo } }
81+
82+
it { is_expected.to be_a described_class }
83+
end
84+
85+
context 'with no info' do
86+
let(:info) { nil }
87+
88+
it { is_expected.to be_a described_class }
89+
end
90+
91+
context 'with no auth set' do
92+
let(:auth) { nil }
93+
94+
it { is_expected.to be_nil }
95+
end
96+
end
97+
end

0 commit comments

Comments
 (0)