CI / Test (pull_request) Successful in 41s
Implements the optional OAuth 2.0 Authorization Code grant with PKCE (RFC 7636) for remoteStorage, as described in the remoteStorage spec (section 10.1). - Advertise the authorization and token endpoints plus the supported code_challenge method (S256) in WebFinger, and bump the advertised spec version to draft-dejong-remotestorage-27 - Add RsAuthorizationCode: a short-lived (5 min), single-use authorization code stored only as a SHA256 digest - Add RsOauthManager services for issuing and exchanging codes, including S256 verification - Add a cross-origin POST /rs/oauth/token endpoint that exchanges a code (and code_verifier) for an access token, with CORS and no CSRF token - Consent dialog now carries response_type / code_challenge through the form and redirects with the code in the query string for the code flow - Remove the existing-token reuse shortcut so every authorization results in a distinct authorization (and token) Part of #254
191 lines
7.5 KiB
Ruby
191 lines
7.5 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe "WebFinger", type: :request do
|
|
describe "User does not exist" do
|
|
it "returns a 404 status" do
|
|
get "/.well-known/webfinger?resource=acct%3Ajane.doe%40kosmos.org"
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
|
|
context "User exists" do
|
|
let(:user) { create :user, cn: 'tony', ou: 'kosmos.org' }
|
|
|
|
before do
|
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
|
services_enabled: ["mastodon", "remotestorage"]
|
|
})
|
|
end
|
|
|
|
describe "Avatar" do
|
|
context "not available" do
|
|
it "does not include an avatar link" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
res = JSON.parse(response.body)
|
|
|
|
link = res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/avatar"}
|
|
expect(link).to be_nil
|
|
end
|
|
end
|
|
|
|
context "available" do
|
|
let(:fixture_path) { Rails.root.join("spec/fixtures/files/taipei.jpg") }
|
|
let(:img_data) { File.read(fixture_path) }
|
|
let(:img_hash) { Digest::SHA256.hexdigest(img_data) }
|
|
|
|
before do
|
|
ActiveStorage::Blob.create_and_upload!(
|
|
io: File.open(fixture_path),
|
|
filename: "#{img_hash}.jpg",
|
|
content_type: "image/jpeg"
|
|
).tap do |blob|
|
|
user.avatar.attach(blob)
|
|
end
|
|
end
|
|
|
|
it "includes a public avatar link" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
res = JSON.parse(response.body)
|
|
|
|
link = res["links"].find { |l| l["rel"] == "http://webfinger.net/rel/avatar" }
|
|
expect(link).to be_present
|
|
expect(link["type"]).to eq("image/jpeg")
|
|
expect(link["href"]).to match(%r{users/tony/avatars/#{img_hash}.jpg})
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Mastodon entries" do
|
|
context "Mastodon available" do
|
|
it "includes the Mastodon aliases and links for the user" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
|
|
expect(res["aliases"]).to include("http://example.social/@tony")
|
|
expect(res["aliases"]).to include("http://example.social/users/tony")
|
|
|
|
profile_link = res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}
|
|
self_link = res["links"].find{|l| l["rel"] == "self"}
|
|
ostatus_link = res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}
|
|
expect(profile_link["type"]).to eql("text/html")
|
|
expect(profile_link["href"]).to eql("http://example.social/@tony")
|
|
expect(self_link["type"]).to eql("application/activity+json")
|
|
expect(self_link["href"]).to eql("http://example.social/users/tony")
|
|
expect(ostatus_link["template"]).to eql("http://example.social/authorize_interaction?uri={uri}")
|
|
end
|
|
end
|
|
|
|
context "Mastodon not enabled for user" do
|
|
before do
|
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
|
services_enabled: ["ejabberd"]
|
|
})
|
|
end
|
|
|
|
it "does not include Mastodon aliases or links" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
expect(res["aliases"]).not_to include("http://example.social/@tony")
|
|
expect(res["aliases"]).not_to include("http://example.social/users/tony")
|
|
expect(res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}).to be(nil)
|
|
expect(res["links"].find{|l| l["rel"] == "self"}).to be(nil)
|
|
expect(res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}).to be(nil)
|
|
end
|
|
end
|
|
|
|
context "Mastodon not available" do
|
|
before do
|
|
Setting.mastodon_enabled = false
|
|
end
|
|
|
|
it "does not include Mastodon aliases or links" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
expect(res["aliases"]).not_to include("http://example.social/@tony")
|
|
expect(res["aliases"]).not_to include("http://example.social/users/tony")
|
|
expect(res["links"].find{|l| l["rel"] == "http://webfinger.net/rel/profile-page"}).to be(nil)
|
|
expect(res["links"].find{|l| l["rel"] == "self"}).to be(nil)
|
|
expect(res["links"].find{|l| l["rel"] == "http://ostatus.org/schema/1.0/subscribe"}).to be(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "remoteStorage entries" do
|
|
context "remoteStorage available" do
|
|
it "includes the remoteStorage link for the user" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
|
|
|
expect(rs_link["href"]).to eql("#{Setting.rs_storage_url}/tony")
|
|
|
|
oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
|
|
expect(oauth_url).to eql("http://localhost/rs/oauth/tony")
|
|
|
|
authorization_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-3.1"]
|
|
expect(authorization_url).to eql("http://localhost/rs/oauth/tony")
|
|
|
|
token_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-3.2"]
|
|
expect(token_url).to eql("http://localhost/rs/oauth/token")
|
|
|
|
expect(rs_link["properties"]["http://tools.ietf.org/html/rfc7636"]).to eql("S256")
|
|
expect(rs_link["properties"]["http://remotestorage.io/spec/version"]).to eql("draft-dejong-remotestorage-27")
|
|
end
|
|
|
|
it "returns CORS headers" do
|
|
get "/.well-known/nostr.json?name=bobdylan"
|
|
expect(response.headers['Access-Control-Allow-Origin']).to eq("*")
|
|
expect(response.headers['Access-Control-Allow-Methods']).to eq('GET')
|
|
end
|
|
end
|
|
|
|
context "remoteStorage not enabled for user" do
|
|
before do
|
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
|
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
|
|
services_enabled: ["ejabberd"]
|
|
})
|
|
end
|
|
|
|
it "does not include the remoteStorage link" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
|
|
|
expect(rs_link).to be_nil
|
|
end
|
|
end
|
|
|
|
context "remoteStorage not available" do
|
|
before do
|
|
Setting.remotestorage_enabled = false
|
|
end
|
|
|
|
it "does not include the remoteStorage link" do
|
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
res = JSON.parse(response.body)
|
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
|
|
|
expect(rs_link).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|