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/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/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..d310450 --- /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, :text + add_column :rs_authorization_codes, :user_agent, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 79a53bc..4518582 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.text "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.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" 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/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 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