diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 90e5fd440..91975fd34 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -219,12 +219,21 @@ module Invidious::Routes::Account scopes ||= [] of String callback_url = env.params.query["callback_url"]? + expire = env.params.query["expire"]?.try &.to_i? + + # ArikTube: a client the admin listed, reached through a session the + # trusted-header proxy vouches for, is answered as the consent POST + # below would answer it. The proxy already authenticated this person + # for this origin, so the extra click proves nothing. + if callback_url && Invidious::TrustedHeaderAuth.auto_approve_token?(env, user, callback_url) + access_token = generate_token(user.email, scopes, expire, HMAC_KEY) + return env.redirect self.token_callback_url(callback_url, user, access_token) + end + if callback_url callback_url = URI.parse(callback_url) end - expire = env.params.query["expire"]?.try &.to_i? - templated "user/authorize_token" end @@ -257,20 +266,7 @@ module Invidious::Routes::Account access_token = generate_token(user.email, scopes, expire, HMAC_KEY) if callback_url - access_token = URI.encode_www_form(access_token) - url = URI.parse(callback_url) - - if url.query - query = HTTP::Params.parse(url.query.not_nil!) - else - query = HTTP::Params.new - end - - query["token"] = access_token - query["username"] = URI.encode_path_segment(user.email) - url.query = query.to_s - - env.redirect url.to_s + env.redirect self.token_callback_url(callback_url, user, access_token) else csrf_token = "" env.set "access_token", access_token @@ -278,6 +274,25 @@ module Invidious::Routes::Account end end + # The client's callback URL with the granted token and the user name + # appended. Shared by the consent POST and the ArikTube auto-approval, so + # an approved client is handed exactly the same URL either way. + private def token_callback_url(callback_url : String, user : User, access_token : String) : String + url = URI.parse(callback_url) + + if url.query + query = HTTP::Params.parse(url.query.not_nil!) + else + query = HTTP::Params.new + end + + query["token"] = URI.encode_www_form(access_token) + query["username"] = URI.encode_path_segment(user.email) + url.query = query.to_s + + url.to_s + end + # ------------------- # Manage tokens # ------------------- diff --git a/src/invidious/trusted_header_auth.cr b/src/invidious/trusted_header_auth.cr index 564a3bde8..bd15d6c02 100644 --- a/src/invidious/trusted_header_auth.cr +++ b/src/invidious/trusted_header_auth.cr @@ -45,6 +45,24 @@ module Invidious::TrustedHeaderAuth email end + # Whether a token authorization request may skip the consent page. + # + # Only for a session the proxy vouches for, and only when the callback + # lands on an origin the admin listed. The token still goes to that origin + # and nowhere else, so an attacker who talks the browser into this route + # hands the token to the admin's own client, not to themselves. + def auto_approve_token?(env, user, callback_url : String?) : Bool + config = CONFIG.trusted_header_auth + + Invidious::ArikSettings.auto_approve_token?( + config.enabled, + config.auto_approve_token_callbacks, + asserted_email(env), + user.email, + callback_url + ) + end + # Whether `user` may change their password without typing the current one. # # The header has to assert this very user on this very request, so a