From 7c4bad860af70dc5a3bc10f609746774e112c633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 2 Nov 2026 13:01:44 +0400 Subject: [PATCH 1/3] Store per-device user agent for remoteStorage authorizations - 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 --- app/controllers/rs/oauth_controller.rb | 4 +++- app/services/rs_oauth_manager/exchange_code.rb | 3 ++- app/services/rs_oauth_manager/issue_code.rb | 4 +++- .../20261102130000_add_user_agent_to_rs_authorizations.rb | 6 ++++++ db/schema.rb | 4 +++- spec/controllers/rs/oauth_controller_spec.rb | 6 ++++++ spec/services/rs_oauth_manager/exchange_code_spec.rb | 2 ++ spec/services/rs_oauth_manager/issue_code_spec.rb | 4 +++- 8 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb diff --git a/app/controllers/rs/oauth_controller.rb b/app/controllers/rs/oauth_controller.rb index fe69198..c5e1952 100644 --- a/app/controllers/rs/oauth_controller.rb +++ b/app/controllers/rs/oauth_controller.rb @@ -112,6 +112,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 ) @@ -124,7 +125,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), diff --git a/app/services/rs_oauth_manager/exchange_code.rb b/app/services/rs_oauth_manager/exchange_code.rb index 131fbe2..8ed5298 100644 --- a/app/services/rs_oauth_manager/exchange_code.rb +++ b/app/services/rs_oauth_manager/exchange_code.rb @@ -36,7 +36,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 ) end diff --git a/app/services/rs_oauth_manager/issue_code.rb b/app/services/rs_oauth_manager/issue_code.rb index b155117..87ccb79 100644 --- a/app/services/rs_oauth_manager/issue_code.rb +++ b/app/services/rs_oauth_manager/issue_code.rb @@ -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 ) diff --git a/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb b/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb new file mode 100644 index 0000000..841369e --- /dev/null +++ b/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 79a53bc..524de14 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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: "[]" 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" diff --git a/spec/controllers/rs/oauth_controller_spec.rb b/spec/controllers/rs/oauth_controller_spec.rb index 6e2c233..e06be1a 100644 --- a/spec/controllers/rs/oauth_controller_spec.rb +++ b/spec/controllers/rs/oauth_controller_spec.rb @@ -346,6 +346,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 @@ -370,6 +371,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 @@ -499,6 +504,7 @@ RSpec.describe Rs::OauthController, type: :controller do expect(@code.permissions).to eq(%w(documents photos contacts:rw)) expect(@code.code_challenge).to eq("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") 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 diff --git a/spec/services/rs_oauth_manager/exchange_code_spec.rb b/spec/services/rs_oauth_manager/exchange_code_spec.rb index 8bf1661..e0169a2 100644 --- a/spec/services/rs_oauth_manager/exchange_code_spec.rb +++ b/spec/services/rs_oauth_manager/exchange_code_spec.rb @@ -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 diff --git a/spec/services/rs_oauth_manager/issue_code_spec.rb b/spec/services/rs_oauth_manager/issue_code_spec.rb index c80593f..3e424d1 100644 --- a/spec/services/rs_oauth_manager/issue_code_spec.rb +++ b/spec/services/rs_oauth_manager/issue_code_spec.rb @@ -11,7 +11,8 @@ RSpec.describe RsOauthManager::IssueCode do permissions: %w(documents:rw), code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", 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("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") + expect(subject.user_agent).to eq("RSpec Device") end it "does not create a remote storage authorization" do -- 2.50.1 From eeb399ff8389632a683a4b2291168d135ca8328f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 2 Nov 2026 17:40:00 +0400 Subject: [PATCH 2/3] Document the storage-first launch flow Add an explicit spec for the storage-first handoff (remoteStorage spec section 11): launching an app passes only `#remotestorage=
`, never an access token. The app then runs the application-first (now PKCE-capable) flow itself. Part of #254 --- app/controllers/services/rs_auths_controller.rb | 2 ++ spec/controllers/services/rs_auths_controller_spec.rb | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/services/rs_auths_controller.rb b/app/controllers/services/rs_auths_controller.rb index 2fff161..c081538 100644 --- a/app/controllers/services/rs_auths_controller.rb +++ b/app/controllers/services/rs_auths_controller.rb @@ -29,6 +29,8 @@ class Services::RsAuthsController < Services::BaseController current_user.address end + # Storage-first bearer token issuance (remoteStorage spec ยง11): the app + # receives the address and runs the application-first flow itself. launch_url = "#{@auth.launch_url}#remotestorage=#{user_address}" redirect_to launch_url, allow_other_host: true diff --git a/spec/controllers/services/rs_auths_controller_spec.rb b/spec/controllers/services/rs_auths_controller_spec.rb index 1c619c2..812a842 100644 --- a/spec/controllers/services/rs_auths_controller_spec.rb +++ b/spec/controllers/services/rs_auths_controller_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Services::RsAuthsController, type: :controller do ]) end - describe "GET /services/storage/rs_auths/:id/launch_app" do + describe "GET /services/storage/rs_auths/:id/launch_app (storage-first)" do context "when user is signed in" do before do sign_in user @@ -32,10 +32,14 @@ RSpec.describe Services::RsAuthsController, type: :controller do @auth.destroy end - it "redirects to the given URL with the correct RS URL fragment params" do + it "hands the app the user address in the URL fragment" do launch_url = "https://app.example.com#remotestorage=#{user.address}" expect(response).to redirect_to(launch_url) end + + it "does not hand over an access token" do + expect(response.location).not_to include("access_token") + end end context "when remoteStorage is not enabled for the user" do -- 2.50.1 From 3dcd263d438f8b41457278218c7a06e7221ee91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 3 Nov 2026 11:42:06 +0400 Subject: [PATCH 3/3] Store the remoteStorage user agent as text, not varchar(255) Real-world user agents (in-app browsers, Android WebView variants) commonly exceed 255 characters. On PostgreSQL that would raise and 500 the consent POST and token exchange; SQLite dev/test does not enforce the limit, so it went unnoticed. Use text columns for user_agent on both tables. Part of #254 --- .../20261102130000_add_user_agent_to_rs_authorizations.rb | 4 ++-- db/schema.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb b/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb index 841369e..d310450 100644 --- a/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb +++ b/db/migrate/20261102130000_add_user_agent_to_rs_authorizations.rb @@ -1,6 +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 + add_column :remote_storage_authorizations, :user_agent, :text + add_column :rs_authorization_codes, :user_agent, :text end end diff --git a/db/schema.rb b/db/schema.rb index 524de14..4518582 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -110,7 +110,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_02_130000) do t.string "redirect_uri" t.string "token" t.datetime "updated_at", null: false - t.string "user_agent" + t.text "user_agent" t.integer "user_id", null: false t.integer "web_app_id" t.index ["permissions"], name: "index_remote_storage_authorizations_on_permissions" @@ -129,7 +129,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_02_130000) do t.text "permissions", default: "[]" t.string "redirect_uri", null: false t.datetime "updated_at", null: false - t.string "user_agent" + t.text "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" -- 2.50.1