Store per-device user agent for remoteStorage authorizations
CI / Test (pull_request) Successful in 2m11s

- Add user_agent to remote_storage_authorizations and rs_authorization_codes
- Capture the browser's user agent at consent, carry it on the authorization
  code, and store it on the authorization when the code is exchanged
- Store it directly for the implicit flow too

With the reuse shortcut gone, each authorization is already a distinct
per-device token; this adds the metadata needed to label and display them.

Part of #254
This commit is contained in:
raucao committed 2026-11-02 13:01:44 +04:00
1 parent 0ab0412bb3
commit 25856a7ca7
8 files changed
+28 -5

No files matched your search

+3 -1
View File
@@ -104,6 +104,7 @@ class Rs::OauthController < ApplicationController
redirect_uri: redirect_uri,
permissions: permissions,
expire_at: expire_at,
user_agent: request.user_agent,
code_challenge: challenge,
code_challenge_method: method
)
@@ -116,7 +117,8 @@ class Rs::OauthController < ApplicationController
client_id: client_id,
redirect_uri: redirect_uri,
app_name: client_id,
expire_at: expire_at
expire_at: expire_at,
user_agent: request.user_agent
)
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
@@ -25,7 +25,8 @@ module RsOauthManager
client_id: auth_code.client_id,
redirect_uri: auth_code.redirect_uri,
app_name: auth_code.client_id,
expire_at: auth_code.permission_expire_at
expire_at: auth_code.permission_expire_at,
user_agent: auth_code.user_agent
)
auth_code.consume!
+3 -1
View File
@@ -2,7 +2,7 @@ module RsOauthManager
class IssueCode < RsOauthManagerService
def initialize(user:, client_id:, redirect_uri:, permissions:,
code_challenge:, code_challenge_method: "S256",
expire_at: nil)
expire_at: nil, user_agent: nil)
@user = user
@client_id = client_id
@redirect_uri = redirect_uri
@@ -10,6 +10,7 @@ module RsOauthManager
@code_challenge = code_challenge
@code_challenge_method = code_challenge_method
@expire_at = expire_at
@user_agent = user_agent
end
def call
@@ -19,6 +20,7 @@ module RsOauthManager
redirect_uri: @redirect_uri,
permissions: @permissions,
permission_expire_at: @expire_at,
user_agent: @user_agent,
code_challenge: @code_challenge,
code_challenge_method: @code_challenge_method
)
@@ -0,0 +1,6 @@
class AddUserAgentToRsAuthorizations < ActiveRecord::Migration[8.1]
def change
add_column :remote_storage_authorizations, :user_agent, :string
add_column :rs_authorization_codes, :user_agent, :string
end
end
+3 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_11_02_120000) do
ActiveRecord::Schema[8.1].define(version: 2026_11_02_130000) do
create_table "active_storage_attachments", force: :cascade do |t|
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
@@ -110,6 +110,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_02_120000) do
t.string "redirect_uri"
t.string "token"
t.datetime "updated_at", null: false
t.string "user_agent"
t.integer "user_id", null: false
t.integer "web_app_id"
t.index ["permissions"], name: "index_remote_storage_authorizations_on_permissions"
@@ -128,6 +129,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_02_120000) do
t.text "permissions", default: "--- []\n"
t.string "redirect_uri", null: false
t.datetime "updated_at", null: false
t.string "user_agent"
t.integer "user_id", null: false
t.index ["code_digest"], name: "index_rs_authorization_codes_on_code_digest", unique: true
t.index ["user_id"], name: "index_rs_authorization_codes_on_user_id"
@@ -310,6 +310,7 @@ RSpec.describe Rs::OauthController, type: :controller do
context "when user is signed in" do
before do
sign_in user
request.user_agent = "RSpec Device"
end
after do
@@ -334,6 +335,10 @@ RSpec.describe Rs::OauthController, type: :controller do
expect(@auth.permissions).to eq(%w(documents photos contacts:rw videos:r tasks/work:r))
end
it "stores the device's user agent" do
expect(@auth.user_agent).to eq("RSpec Device")
end
it "redirects to the redirect_uri" do
expect(response).to redirect_to("https://example.com#access_token=#{@auth.token}&state=foobar123")
end
@@ -463,6 +468,7 @@ RSpec.describe Rs::OauthController, type: :controller do
expect(@code.permissions).to eq(%w(documents photos contacts:rw))
expect(@code.code_challenge).to eq("challenge123")
expect(@code.code_challenge_method).to eq("S256")
expect(@code.user_agent).to eq("RSpec Device")
end
it "does not create a remote storage authorization" do
@@ -20,6 +20,7 @@ RSpec.describe RsOauthManager::ExchangeCode do
permissions: %w(documents:rw),
code_challenge: code_challenge,
code_challenge_method: "S256",
user_agent: "RSpec Device",
**overrides
)
end
@@ -43,6 +44,7 @@ RSpec.describe RsOauthManager::ExchangeCode do
expect(auth.permissions).to eq(%w(documents:rw))
expect(auth.client_id).to eq("example.com")
expect(auth.token).to be_present
expect(auth.user_agent).to eq("RSpec Device")
expect(RsAuthorizationCode.exists?(code.id)).to be(false)
end
@@ -11,7 +11,8 @@ RSpec.describe RsOauthManager::IssueCode do
permissions: %w(documents:rw),
code_challenge: "challenge",
code_challenge_method: "S256",
expire_at: 1.month.from_now
expire_at: 1.month.from_now,
user_agent: "RSpec Device"
)
end
@@ -21,6 +22,7 @@ RSpec.describe RsOauthManager::IssueCode do
expect(subject.client_id).to eq("example.com")
expect(subject.permissions).to eq(%w(documents:rw))
expect(subject.code_challenge).to eq("challenge")
expect(subject.user_agent).to eq("RSpec Device")
end
it "does not create a remote storage authorization" do