From b8f2736bdfcdc04e0adf5845d2c95068cab4de84 Mon Sep 17 00:00:00 2001
From: Julian Rother <julianr@fsmpi.rwth-aachen.de>
Date: Thu, 12 Aug 2021 17:23:47 +0200
Subject: [PATCH] Moved token generation to common module and introduced
 token_urlfriendly

Broken mail clients like Thunderbird fail to recognize urlsafe characters
like "-" as part of an URL. token_urlfriendly avoids those characters.

Closes #93.

Co-authored-by: nd <nd@cccv.de>
---
 uffd/invite/models.py      |  4 ++--
 uffd/selfservice/models.py |  7 ++-----
 uffd/session/models.py     | 11 +----------
 uffd/signup/models.py      |  4 ++--
 uffd/utils.py              | 20 ++++++++++++++++++++
 5 files changed, 27 insertions(+), 19 deletions(-)
 create mode 100644 uffd/utils.py

diff --git a/uffd/invite/models.py b/uffd/invite/models.py
index 196e333c..1c54ba88 100644
--- a/uffd/invite/models.py
+++ b/uffd/invite/models.py
@@ -1,4 +1,3 @@
-import secrets
 import datetime
 
 from flask import current_app
@@ -9,6 +8,7 @@ from uffd.ldapalchemy.dbutils import DBRelationship
 from uffd.database import db
 from uffd.user.models import User
 from uffd.signup.models import Signup
+from uffd.utils import token_urlfriendly
 
 invite_roles = db.Table('invite_roles',
 	Column('invite_id', Integer(), ForeignKey('invite.id'), primary_key=True),
@@ -18,7 +18,7 @@ invite_roles = db.Table('invite_roles',
 class Invite(db.Model):
 	__tablename__ = 'invite'
 	id = Column(Integer(), primary_key=True, autoincrement=True)
-	token = Column(String(128), unique=True, nullable=False, default=lambda: secrets.token_urlsafe(48))
+	token = Column(String(128), unique=True, nullable=False, default=token_urlfriendly)
 	created = Column(DateTime, default=datetime.datetime.now, nullable=False)
 	creator_dn = Column(String(128), nullable=True)
 	creator = DBRelationship('creator_dn', User)
diff --git a/uffd/selfservice/models.py b/uffd/selfservice/models.py
index 15d7050f..b87148cd 100644
--- a/uffd/selfservice/models.py
+++ b/uffd/selfservice/models.py
@@ -1,15 +1,12 @@
 import datetime
-import secrets
 
 from sqlalchemy import Column, String, DateTime
 
 from uffd.database import db
-
-def random_token():
-	return secrets.token_hex(128)
+from uffd.utils import token_urlfriendly
 
 class Token():
-	token = Column(String(128), primary_key=True, default=random_token)
+	token = Column(String(128), primary_key=True, default=token_urlfriendly)
 	created = Column(DateTime, default=datetime.datetime.now)
 
 class PasswordToken(Token, db.Model):
diff --git a/uffd/session/models.py b/uffd/session/models.py
index c91619e6..ab243e0f 100644
--- a/uffd/session/models.py
+++ b/uffd/session/models.py
@@ -1,6 +1,5 @@
 import datetime
 import secrets
-import math
 import enum
 
 from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Enum
@@ -10,15 +9,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
 from uffd.ldapalchemy.dbutils import DBRelationship
 from uffd.database import db
 from uffd.user.models import User
-
-def token_typeable(nbytes=None):
-	'''Return random text token that is easy to type (on mobile)'''
-	alphabet = '123456789abcdefghkmnopqrstuvwx' # No '0ijlyz'
-	if nbytes is None:
-		nbytes = secrets.DEFAULT_ENTROPY
-	nbytes_per_char = math.log(len(alphabet), 256)
-	nchars = math.ceil(nbytes / nbytes_per_char)
-	return ''.join([secrets.choice(alphabet) for _ in range(nchars)])
+from uffd.utils import token_typeable
 
 # Device login provides a convenient and secure way to log into SSO-enabled
 # services on a secondary device without entering the user password or
diff --git a/uffd/signup/models.py b/uffd/signup/models.py
index 7c7f4cc9..e994acb0 100644
--- a/uffd/signup/models.py
+++ b/uffd/signup/models.py
@@ -1,4 +1,3 @@
-import secrets
 import datetime
 from crypt import crypt
 
@@ -8,6 +7,7 @@ from uffd.ldapalchemy.dbutils import DBRelationship
 from uffd.database import db
 from uffd.ldap import ldap
 from uffd.user.models import User
+from uffd.utils import token_urlfriendly
 
 class Signup(db.Model):
 	'''Model that represents a self-signup request
@@ -26,7 +26,7 @@ class Signup(db.Model):
 	As long as they are not completed, signup requests have no effect each other
 	or different parts of the application.'''
 	__tablename__ = 'signup'
-	token = Column(String(128), primary_key=True, default=lambda: secrets.token_urlsafe(48))
+	token = Column(String(128), primary_key=True, default=token_urlfriendly)
 	created = Column(DateTime, default=datetime.datetime.now, nullable=False)
 	loginname = Column(Text)
 	displayname = Column(Text)
diff --git a/uffd/utils.py b/uffd/utils.py
new file mode 100644
index 00000000..ff40711c
--- /dev/null
+++ b/uffd/utils.py
@@ -0,0 +1,20 @@
+import secrets
+import math
+
+def token_with_alphabet(alphabet, nbytes=None):
+	'''Return random text token that consists of characters from `alphabet`'''
+	if nbytes is None:
+		nbytes = max(secrets.DEFAULT_ENTROPY, 32)
+	nbytes_per_char = math.log(len(alphabet), 256)
+	nchars = math.ceil(nbytes / nbytes_per_char)
+	return ''.join([secrets.choice(alphabet) for _ in range(nchars)])
+
+def token_typeable(nbytes=None):
+	'''Return random text token that is easy to type (on mobile)'''
+	alphabet = '123456789abcdefghkmnopqrstuvwx' # No '0ijlyz'
+	return token_with_alphabet(alphabet, nbytes=nbytes)
+
+def token_urlfriendly(nbytes=None):
+	'''Return random text token that is urlsafe and works around common parsing bugs'''
+	alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+	return token_with_alphabet(alphabet, nbytes=nbytes)
-- 
GitLab