diff --git a/config/config.example.yml b/config/config.example.yml
index 8abe1b9e..7605774c 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -307,6 +307,40 @@ https_only: false
##
#enable_user_notifications: true
+##
+## List of Enabled Authentication Backend
+## If not provided falls back to default
+##
+## Supported Values:
+## - invidious
+## - google
+## - oauth
+## - ldap (Not implemented !)
+## - saml (Not implemented !)
+##
+## Default: ["invidious","google","oauth"]
+##
+# auth_type: ["oauth"]
+
+##
+## OAuth Configuration
+##
+## Notes:
+## - Supports multiple OAuth backends
+## - Requires external_port and domain to be configured
+##
+## Default: []
+##
+# oauth:
+# example:
+# host: oauth.example.net
+# auth_uri: /oauth/authorize/
+# token_uri: /oauth/token/
+# info_uri: /oauth/userinfo/
+# client_id: CLIENT_ID
+# client_secret: CLIENT_SECRET
+
+
# -----------------------------
# Background jobs
# -----------------------------
diff --git a/src/invidious/config.cr b/src/invidious/config.cr
index 9fc58409..2135b0d9 100644
--- a/src/invidious/config.cr
+++ b/src/invidious/config.cr
@@ -8,6 +8,17 @@ 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
+end
+
struct ConfigPreferences
include YAML::Serializable
@@ -129,6 +140,9 @@ class Config
# Use quic transport for youtube api
property use_quic : Bool = false
+ property auth_type : Array(String) = ["invidious", "google", "oauth"]
+ property oauth = {} of String => OAuthConfig
+
# 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..4846a904
--- /dev/null
+++ b/src/invidious/helpers/oauth.cr
@@ -0,0 +1,40 @@
+require "oauth2"
+
+module Invidious::OAuthHelper
+ extend self
+
+ def get(key)
+ if HOST_URL == ""
+ raise Exception.new("Missing domain and port configuration")
+ end
+ if provider = CONFIG.oauth[key]?
+ redirect_uri = "#{HOST_URL}/login/oauth/#{key}"
+ OAuth2::Client.new(
+ provider.host,
+ provider.client_id,
+ provider.client_secret,
+ authorize_uri: provider.auth_uri,
+ token_uri: provider.token_uri,
+ redirect_uri: redirect_uri
+ )
+ else
+ raise Exception.new("Invalid OAuth Endpoint: " + key)
+ end
+ end
+
+ def auth(key, authorization_code)
+ self.get(key).get_access_token_using_authorization_code(authorization_code)
+ end
+
+ def info(key, token)
+ if provider = CONFIG.oauth[key]?
+ client = HTTP::Client.new(provider.host, tls: true)
+ token.authenticate(client)
+ response = client.get provider.info_uri
+ client.close
+ response.body
+ else
+ raise Exception.new("Invalid OAuth Endpoint: " + key)
+ end
+ end
+end
diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr
index 6454131a..d8e6b83a 100644
--- a/src/invidious/routes/login.cr
+++ b/src/invidious/routes/login.cr
@@ -21,6 +21,16 @@ module Invidious::Routes::Login
account_type = env.params.query["type"]?
account_type ||= "invidious"
+ if CONFIG.auth_type.find(&.== account_type).nil?
+ if CONFIG.auth_type.size == 0
+ account_type = "invidious"
+ else
+ account_type = CONFIG.auth_type[0]
+ end
+ end
+
+ oauth = CONFIG.auth_type.find(&.== "oauth") && (CONFIG.oauth.size > 0)
+
captcha_type = env.params.query["captcha"]?
captcha_type ||= "image"
@@ -30,6 +40,35 @@ module Invidious::Routes::Login
templated "user/login"
end
+ def self.login_oauth(env)
+ locale = env.get("preferences").as(Preferences).locale
+
+ referer = get_referer(env, "/feed/subscriptions")
+
+ authorization_code = env.params.query["code"]?
+ provider_k = env.params.url["provider"]
+ if authorization_code
+ begin
+ token = OAuthHelper.auth(provider_k, authorization_code)
+ info = JSON.parse(OAuthHelper.info(provider_k, token))
+
+ email = info["email"].as_s
+ user = Invidious::Database::Users.select(email: email)
+
+ if user
+ user_flow_existing(env,email)
+ else
+ user_flow_new(env,email,nil);
+ end
+ rescue
+ return error_template(403, "Invalid Authorization Code")
+ end
+ else
+ return error_template(403, "Missing Authorization Code")
+ end
+ env.redirect referer
+ end
+
def self.login(env)
locale = env.get("preferences").as(Preferences).locale
@@ -42,10 +81,19 @@ module Invidious::Routes::Login
# https://stackoverflow.com/a/574698
email = env.params.body["email"]?.try &.downcase.byte_slice(0, 254)
password = env.params.body["password"]?
+ oauth = CONFIG.auth_type.find(&.== "oauth") && (CONFIG.oauth.size > 0)
account_type = env.params.query["type"]?
account_type ||= "invidious"
+ if CONFIG.auth_type.size == 0
+ return error_template(401, "No authentication backend enabled.")
+ end
+
+ if CONFIG.auth_type.find(&.== account_type).nil?
+ account_type = CONFIG.auth_type[0]
+ end
+
case account_type
when "google"
tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
@@ -308,6 +356,13 @@ module Invidious::Routes::Login
error_message = %(#{ex.message}
Traceback:
#{traceback.gets_to_end}
)
return error_template(500, error_message)
end
+ when "oauth"
+ provider_k = env.params.body["provider"]
+ env.redirect OAuthHelper.get(provider_k).get_authorize_uri("openid email profile")
+ when "saml"
+ return error_template(501, "Not implemented")
+ when "ldap"
+ return error_template(501, "Not implemented")
when "invidious"
if !email
return error_template(401, "User ID is a required field")
@@ -324,21 +379,10 @@ module Invidious::Routes::Login
return error_template(400, "Please sign in using 'Log in with Google'")
end
- if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
- 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)
- else
+ if !Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
return error_template(401, "Wrong username or password")
end
-
- # Since this user has already registered, we don't want to overwrite their preferences
- if env.request.cookies["PREFS"]?
- cookie = env.request.cookies["PREFS"]
- cookie.expires = Time.utc(1990, 1, 1)
- env.response.cookies << cookie
- end
+ user_flow_existing(env, email)
else
if !CONFIG.registration_enabled
return error_template(400, "Registration has been disabled by administrator.")
@@ -417,32 +461,7 @@ module Invidious::Routes::Login
end
end
end
-
- sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
- user, sid = create_user(sid, email, password)
-
- 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
+ user_flow_new(env, email, password);
end
env.redirect referer
@@ -488,4 +507,49 @@ module Invidious::Routes::Login
env.response.headers["Content-Type"] = response.headers["Content-Type"]
response.body
end
+
+ def self.user_flow_existing(env, email)
+ 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)
+
+ # Since this user has already registered, we don't want to overwrite their preferences
+ if env.request.cookies["PREFS"]?
+ cookie = env.request.cookies["PREFS"]
+ cookie.expires = Time.utc(1990, 1, 1)
+ env.response.cookies << cookie
+ end
+ end
+
+ def self.user_flow_new(env, email, password)
+ sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
+ if password
+ user, sid = create_user(sid, email, password)
+ else
+ user, sid = create_user(sid, email)
+ end
+
+ 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
end
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index 9e2ade3d..44e4e7df 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -55,6 +55,7 @@ module Invidious::Routing
def register_user_routes
# User login/out
get "/login", Routes::Login, :login_page
+ get "/login/oauth/:provider", Routes::Login, :login_oauth
post "/login", Routes::Login, :login
post "/signout", Routes::Login, :signout
get "/Captcha", Routes::Login, :captcha
diff --git a/src/invidious/user/cookies.cr b/src/invidious/user/cookies.cr
index 654efc15..4a81e6d3 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..3f5474cc 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -72,6 +72,24 @@ 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..36594ec1 100644
--- a/src/invidious/views/user/login.ecr
+++ b/src/invidious/views/user/login.ecr
@@ -43,6 +43,17 @@
+ <% when "oauth" %>
+
<% else # "invidious" %>
<% end %>