From e3fa0a90943712e063f185c9436d9ebe2d8bcabd Mon Sep 17 00:00:00 2001 From: choelzl Date: Sat, 25 Jun 2022 02:38:42 +0200 Subject: [PATCH] Implemeented OAuth Authentication --- config/config.example.yml | 26 ++++++++++ src/invidious.cr | 3 ++ src/invidious/config.cr | 15 ++++++ src/invidious/helpers/oauth.cr | 36 ++++++++++++++ src/invidious/routes/login.cr | 78 ++++++++++++++++++++++++++++++ src/invidious/routing.cr | 1 + src/invidious/user/cookies.cr | 2 + src/invidious/users.cr | 19 ++++++++ src/invidious/views/user/login.ecr | 6 +++ 9 files changed, 186 insertions(+) create mode 100644 src/invidious/helpers/oauth.cr diff --git a/config/config.example.yml b/config/config.example.yml index ae9509d2..0884079b 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -295,6 +295,32 @@ https_only: false ## #admins: [""] +## +## Force Authentication Backend +## If not provided falls back to the type query parameter +## +## Supported Values: +## - invidious +## - google +## - oauth +## - ldap (Not implemented !) +## - saml (Not implemented !) +## +## Default: nil +## + +## +## OAuth Configuration +## +# oauth: +# host: oauth.example.net +# auth_uri: /oauth/authorize +# token_uri: /oauth/token +# info_uri: /oauth/userinfo +# client_id: CLIENT_ID +# client_secret: CLIENT_SEECRET +# redirect_uri: https://invidious.eexample.net/login/oauth + # ----------------------------- # Background jobs diff --git a/src/invidious.cr b/src/invidious.cr index 4952b365..3f086dca 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -32,6 +32,9 @@ require "yaml" require "compress/zip" require "protodec/utils" +require "oauth2" +require "http/client" + require "./invidious/database/*" require "./invidious/database/migrations/*" require "./invidious/helpers/*" diff --git a/src/invidious/config.cr b/src/invidious/config.cr index a077c7fd..0cc43ca3 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -8,6 +8,18 @@ struct DBConfig property dbname : String end +struct OAuthConfig + include YAML::Serializable + + property host : String + property auth_uri : String + property token_uri : String + property info_uri : String + property client_id : String + property client_secret : String + property redirect_uri : String +end + struct ConfigPreferences include YAML::Serializable @@ -123,6 +135,9 @@ class Config # Use quic transport for youtube api property use_quic : Bool = false + property auth_type : String? = nil + property oauth : OAuthConfig? = nil + # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new diff --git a/src/invidious/helpers/oauth.cr b/src/invidious/helpers/oauth.cr new file mode 100644 index 00000000..b5236e74 --- /dev/null +++ b/src/invidious/helpers/oauth.cr @@ -0,0 +1,36 @@ +def oauth_get() + if oauth = CONFIG.oauth + oauth_host = oauth.host + oauth_auth_uri = oauth.auth_uri + oauth_token_uri = oauth.token_uri + oauth_info_uri = oauth.info_uri + oauth_client_id = oauth.client_id + oauth_client_secret = oauth.client_secret + oauth_redirect_uri = oauth.redirect_uri + + OAuth2::Client.new(oauth_host, oauth_client_id, oauth_client_secret, + authorize_uri: oauth_auth_uri, token_uri: oauth_token_uri, + redirect_uri: oauth_redirect_uri) + else + raise Exception.new("Missing OAuth Config") + end +end + +def oauth_auth(authorization_code) + oauth_get().get_access_token_using_authorization_code(authorization_code) +end + +def oauth_info(token) + if oauth = CONFIG.oauth + oauth_host = oauth.host + oauth_info_uri = oauth.info_uri + + client = HTTP::Client.new(oauth_host, tls: true) + token.authenticate(client) + response = client.get oauth_info_uri + client.close + response.body + else + raise Exception.new("Missing OAuth Config") + end +end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 99fc13a2..60c56cbb 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -21,6 +21,10 @@ module Invidious::Routes::Login account_type = env.params.query["type"]? account_type ||= "invidious" + if CONFIG.auth_type + account_type = CONFIG.auth_type + end + captcha_type = env.params.query["captcha"]? captcha_type ||= "image" @@ -30,6 +34,70 @@ module Invidious::Routes::Login templated "user/login" end + def self.login_oauth(env) + locale = env.get("preferences").as(Preferences).locale + + user = env.get? "user" + return env.redirect "/feed/subscriptions" if user + + referer = get_referer(env, "/feed/subscriptions") + + authorization_code = env.params.query["code"]? + if authorization_code + begin + token = oauth_auth(authorization_code) + info = JSON.parse(oauth_info(token)) + email = info["email"].as_s + user = Invidious::Database::Users.select(email: email) + if user + sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) + Invidious::Database::SessionIDs.insert(sid, email) + env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) + + if env.request.cookies["PREFS"]? + cookie = env.request.cookies["PREFS"] + cookie.expires = Time.utc(1990, 1, 1) + env.response.cookies << cookie + end + else + if !CONFIG.registration_enabled + return error_template(400, "Registration has been disabled by administrator.") + end + + sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) + user, sid = create_user(sid, email) + + if language_header = env.request.headers["Accept-Language"]? + if language = ANG.language_negotiator.best(language_header, LOCALES.keys) + user.preferences.locale = language.header + end + end + + Invidious::Database::Users.insert(user) + Invidious::Database::SessionIDs.insert(sid, email) + view_name = "subscriptions_#{sha256(user.email)}" + PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") + + env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) + if env.request.cookies["PREFS"]? + user.preferences = env.get("preferences").as(Preferences) + Invidious::Database::Users.update_preferences(user) + + cookie = env.request.cookies["PREFS"] + cookie.expires = Time.utc(1990, 1, 1) + env.response.cookies << cookie + end + end + + env.redirect referer + rescue + return error_template(403, "Invalid Authorization Code"); + end + else + return error_template(403, "Missing Authorization Code"); + end + end + def self.login(env) locale = env.get("preferences").as(Preferences).locale @@ -46,6 +114,10 @@ module Invidious::Routes::Login account_type = env.params.query["type"]? account_type ||= "invidious" + if CONFIG.auth_type + account_type = CONFIG.auth_type + end + case account_type when "google" tfa_code = env.params.body["tfa"]?.try &.lchop("G-") @@ -308,6 +380,12 @@ module Invidious::Routes::Login error_message = %(#{ex.message}
Traceback:
#{traceback.gets_to_end}
) return error_template(500, error_message) end + when "oauth" + env.redirect oauth_get().get_authorize_uri("openid email profile") + when "saml" + return error_template(500, "Not implemented") + when "ldap" + return error_template(500, "Not implemented") when "invidious" if !email return error_template(401, "User ID is a required field") diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index bd72c577..00db08bb 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -13,6 +13,7 @@ end macro define_user_routes # User login/out Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page + Invidious::Routing.get "/login/oauth", Invidious::Routes::Login, :login_oauth Invidious::Routing.post "/login", Invidious::Routes::Login, :login Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout Invidious::Routing.get "/Captcha", Invidious::Routes::Login, :captcha diff --git a/src/invidious/user/cookies.cr b/src/invidious/user/cookies.cr index 65e079ec..5a23f759 100644 --- a/src/invidious/user/cookies.cr +++ b/src/invidious/user/cookies.cr @@ -14,6 +14,7 @@ struct Invidious::User return HTTP::Cookie.new( name: "SID", domain: domain, + path: "/", value: sid, expires: Time.utc + 2.years, secure: SECURE, @@ -28,6 +29,7 @@ struct Invidious::User return HTTP::Cookie.new( name: "PREFS", domain: domain, + path: "/", value: URI.encode_www_form(preferences.to_json), expires: Time.utc + 2.years, secure: SECURE, diff --git a/src/invidious/users.cr b/src/invidious/users.cr index b763596b..1f786c79 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -72,6 +72,25 @@ def fetch_user(sid, headers) return user, sid end +def create_user(sid, email) + token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) + + user = Invidious::User.new({ + updated: Time.utc, + notifications: [] of String, + subscriptions: [] of String, + email: email, + preferences: Preferences.new(CONFIG.default_user_preferences.to_tuple), + password: nil, + token: token, + watched: [] of String, + feed_needs_update: true, + }) + + return user, sid +end + + def create_user(sid, email, password) password = Crypto::Bcrypt::Password.create(password, cost: 10) token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) diff --git a/src/invidious/views/user/login.ecr b/src/invidious/views/user/login.ecr index 01d7a210..3cdcf6c6 100644 --- a/src/invidious/views/user/login.ecr +++ b/src/invidious/views/user/login.ecr @@ -43,6 +43,12 @@ + <% when "oauth" %> +
+
+ +
+
<% else # "invidious" %>