]> git.ipfire.org Git - ipfire.org.git/blobdiff - src/web/auth.py
people: Move SSO for Discourse
[ipfire.org.git] / src / web / auth.py
index cb980666341ebd6e71b940c6a6d8fcdbdcf507ab..792205feb40030e1e700abbfd78c1aab0fce4d65 100644 (file)
@@ -2,6 +2,7 @@
 
 import logging
 import tornado.web
+import urllib.parse
 
 from . import base
 
@@ -11,7 +12,7 @@ class CacheMixin(object):
                if self.current_user:
                        self.add_header("Cache-Control", "private")
 
-               self.add_header("Vary", "Cookie")
+               self.add_header("Cache-Control", "no-store")
 
 
 class AuthenticationMixin(CacheMixin):
@@ -26,7 +27,7 @@ class AuthenticationMixin(CacheMixin):
 
                # Send session cookie to the client
                self.set_cookie("session_id", session_id,
-                       domain=self.request.host, expires=session_expires)
+                       domain=self.request.host, expires=session_expires, secure=True)
 
        def logout(self):
                session_id = self.get_cookie("session_id")
@@ -39,15 +40,13 @@ class AuthenticationMixin(CacheMixin):
 
 
 class LoginHandler(AuthenticationMixin, base.BaseHandler):
-       @base.blacklisted
        def get(self):
                next = self.get_argument("next", None)
 
                self.render("auth/login.html", next=next,
                        incorrect=False, username=None)
 
-       @base.blacklisted
-       @base.ratelimit(minutes=60, requests=5)
+       @base.ratelimit(minutes=15, requests=10)
        def post(self):
                username = self.get_argument("username")
                password = self.get_argument("password")
@@ -83,18 +82,17 @@ class LogoutHandler(AuthenticationMixin, base.BaseHandler):
                self.redirect("/")
 
 
-class RegisterHandler(base.BaseHandler):
-       @base.blacklisted
+class RegisterHandler(CacheMixin, base.BaseHandler):
        def get(self):
                # Redirect logged in users away
                if self.current_user:
                        self.redirect("/")
+                       return
 
                self.render("auth/register.html")
 
-       @tornado.gen.coroutine
-       @base.ratelimit(minutes=24*60, requests=5)
-       def post(self):
+       @base.ratelimit(minutes=15, requests=5)
+       async def post(self):
                uid   = self.get_argument("uid")
                email = self.get_argument("email")
 
@@ -102,7 +100,7 @@ class RegisterHandler(base.BaseHandler):
                last_name  = self.get_argument("last_name")
 
                # Check if this is a spam account
-               is_spam = yield self.backend.accounts.check_spam(uid, email,
+               is_spam = await self.backend.accounts.check_spam(email,
                        address=self.get_remote_ip())
 
                if is_spam:
@@ -147,8 +145,112 @@ class ActivateHandler(AuthenticationMixin, base.BaseHandler):
                self.render("auth/activated.html", account=account)
 
 
+class PasswordResetInitiationHandler(CacheMixin, base.BaseHandler):
+       def get(self):
+               username = self.get_argument("username", None)
+
+               self.render("auth/password-reset-initiation.html", username=username)
+
+       @base.ratelimit(minutes=15, requests=10)
+       def post(self):
+               username = self.get_argument("username")
+
+               # Fetch account and submit password reset
+               account = self.backend.accounts.get_by_uid(username)
+               if account:
+                       with self.db.transaction():
+                               account.request_password_reset()
+
+               self.render("auth/password-reset-successful.html")
+
+
+class PasswordResetHandler(AuthenticationMixin, base.BaseHandler):
+       def get(self, uid, reset_code):
+               account = self.backend.accounts.get_by_uid(uid)
+               if not account:
+                       raise tornado.web.HTTPError(404, "Could not find account: %s" % uid)
+
+               self.render("auth/password-reset.html", account=account)
+
+       def post(self, uid, reset_code):
+               account = self.backend.accounts.get_by_uid(uid)
+               if not account:
+                       raise tornado.web.HTTPError(404, "Could not find account: %s" % uid)
+
+               password1 = self.get_argument("password1")
+               password2 = self.get_argument("password2")
+
+               if not password1 == password2:
+                       raise tornado.web.HTTPError(400, "Passwords do not match")
+
+               # Try to perform password reset
+               with self.db.transaction():
+                       account.reset_password(reset_code, password1)
+
+                       # Login the user straight away after reset was successful
+                       self.login(account)
+
+               # Redirect back to /
+               self.redirect("/")
+
+
+class SSODiscourse(CacheMixin, base.BaseHandler):
+       @base.ratelimit(minutes=24*60, requests=100)
+       @tornado.web.authenticated
+       def get(self):
+               # Fetch Discourse's parameters
+               sso = self.get_argument("sso")
+               sig = self.get_argument("sig")
+
+               # Decode payload
+               try:
+                       params = self.accounts.decode_discourse_payload(sso, sig)
+
+               # Raise bad request if the signature is invalid
+               except ValueError:
+                       raise tornado.web.HTTPError(400)
+
+               # Redirect back if user is already logged in
+               args = {
+                       "nonce" : params.get("nonce"),
+                       "external_id" : self.current_user.uid,
+
+                       # Pass email address
+                       "email" : self.current_user.email,
+                       "require_activation" : "false",
+
+                       # More details about the user
+                       "username" : self.current_user.uid,
+                       "name" : "%s" % self.current_user,
+                       "bio" : self.current_user.description or "",
+
+                       # Avatar
+                       "avatar_url" : self.current_user.avatar_url(absolute=True),
+                       "avatar_force_update" : "true",
+
+                       # Send a welcome message
+                       "suppress_welcome_message" : "false",
+
+                       # Group memberships
+                       "admin" : "true" if self.current_user.is_admin() else "false",
+                       "moderator" : "true" if self.current_user.is_moderator() else "false",
+               }
+
+               # Format payload and sign it
+               payload = self.accounts.encode_discourse_payload(**args)
+               signature = self.accounts.sign_discourse_payload(payload)
+
+               qs = urllib.parse.urlencode({
+                       "sso" : payload,
+                       "sig" : signature,
+               })
+
+               # Redirect user
+               self.redirect("%s?%s" % (params.get("return_sso_url"), qs))
+
+
 class APICheckUID(base.APIHandler):
-       @base.ratelimit(minutes=10, requests=100)
+       @base.ratelimit(minutes=1, requests=100)
        def get(self):
                uid = self.get_argument("uid")
                result = None
@@ -166,3 +268,26 @@ class APICheckUID(base.APIHandler):
 
                # Username seems to be okay
                self.finish({ "result" : result or "ok" })
+
+
+class APICheckEmail(base.APIHandler):
+       @base.ratelimit(minutes=1, requests=100)
+       def get(self):
+               email = self.get_argument("email")
+               result = None
+
+               if not email:
+                       result = "empty"
+
+               elif not self.backend.accounts.mail_is_valid(email):
+                       result = "invalid"
+
+               # Check if this email address is blacklisted
+               elif self.backend.accounts.mail_is_blacklisted(email):
+                       result = "blacklisted"
+
+               # Check if this email address is already useed
+               elif self.backend.accounts.get_by_mail(email):
+                       result = "taken"
+
+               self.finish({ "result" : result or "ok" })