14
14
15
15
16
16
class UserInfo (Base ):
17
+ """
18
+ This class represents the information that NativeAuthenticator persists in
19
+ JupyterHub's database.
20
+ """
21
+
17
22
__tablename__ = "users_info"
18
23
id = Column (Integer , primary_key = True , autoincrement = True )
24
+
25
+ # username should be a JupyterHub username, normalized by the Authenticator
26
+ # class normalize_username function.
19
27
username = Column (String (128 ), nullable = False )
28
+
29
+ # password should be a bcrypt generated string that not only contains a
30
+ # hashed password, but also the salt and cost that was used to hash the
31
+ # password. Since bcrypt can extract the salt from this concatenation, this
32
+ # can be used again during validation as salt.
20
33
password = Column (LargeBinary , nullable = False )
34
+
35
+ # is_authorized is a boolean to indicate if the user has been authorized,
36
+ # either by an admin, or by validating via an email for example.
21
37
is_authorized = Column (Boolean , default = False )
38
+
39
+ # login_email_sent is boolean to indicate if a self approval email has been
40
+ # sent out, as enabled by having a allow_self_approval_for configuration
41
+ # set.
22
42
login_email_sent = Column (Boolean , default = False )
43
+
44
+ # email is a un-encrypted string representing the email
23
45
email = Column (String (128 ))
46
+
47
+ # has_2fa is a boolean that is being set to true if the user declares they
48
+ # want to setup 2fa during sign-up.
24
49
has_2fa = Column (Boolean , default = False )
50
+
51
+ # otp_secret (one-time password secret) is given to a user during setup of
52
+ # 2fa. With a shared secret like this, both the user and nativeauthenticator
53
+ # are enabled to generate the same one-time password's, which enables them
54
+ # to be matched against each other.
25
55
otp_secret = Column (String (16 ))
26
56
27
57
def __init__ (self , ** kwargs ):
@@ -31,34 +61,60 @@ def __init__(self, **kwargs):
31
61
32
62
@classmethod
33
63
def find (cls , db , username ):
34
- """Find a user info record by name.
35
- Returns None if not found"""
64
+ """
65
+ Find a user info record by username.
66
+
67
+ Returns None if no user was found.
68
+ """
36
69
return db .query (cls ).filter (cls .username == username ).first ()
37
70
38
71
@classmethod
39
72
def all_users (cls , db ):
40
- """Returns all available user records."""
73
+ """
74
+ Returns all available user info records.
75
+ """
41
76
return db .query (cls ).all ()
42
77
43
- def is_valid_password (self , password ):
44
- """Checks if a password passed matches the
45
- password stored"""
46
- encoded_pw = bcrypt .hashpw (password .encode (), self .password )
47
- return encoded_pw == self .password
48
-
49
78
@classmethod
50
79
def change_authorization (cls , db , username ):
80
+ """
81
+ Toggles the authorization status of a user info record.
82
+
83
+ Returns the user info record.
84
+ """
51
85
user = db .query (cls ).filter (cls .username == username ).first ()
52
86
user .is_authorized = not user .is_authorized
53
87
db .commit ()
54
88
return user
55
89
90
+ def is_valid_password (self , password ):
91
+ """
92
+ Checks if a provided password hashes to the hash we have stored in
93
+ self.password.
94
+
95
+ Note that self.password has been set to the return value of calling
96
+ bcrypt.hashpw(...) before, that returns a concatenation of the random
97
+ salt used and the hashed salt+password combination. So, when we are
98
+ passing self.password back to bcrypt.hashpw(...) as a salt, it is smart
99
+ enough to extract and use only the salt that was originally used.
100
+ """
101
+ return self .password == bcrypt .hashpw (password .encode (), self .password )
102
+
56
103
@validates ("email" )
57
104
def validate_email (self , key , address ):
105
+ """
106
+ Validates any attempt to set the email field of a user info record.
107
+ """
58
108
if not address :
59
109
return
60
110
assert re .match (r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$" , address )
61
111
return address
62
112
63
113
def is_valid_token (self , token ):
114
+ """
115
+ Validates a time-based one-time password (TOTP) as generated by a user's
116
+ 2fa application against the TOTP generated locally by the onetimepass
117
+ module. Assuming the user generated a TOTP with a common shared one-time
118
+ password secret (otp_secret), these passwords should match.
119
+ """
64
120
return onetimepass .valid_totp (token , self .otp_secret )
0 commit comments