4. User Authentication
Note
To use Authentication in a supported web framework please see integrations.
There are many different errors that the User Authentication API may raise. To avoid surprises in production environments, we encourage everyone to test what errors are raised on different edge cases (e.g.: incorrect password, missing MFA token).
Warning
This does not protect you against brute force attacks - make sure to enable rate limiting on your host.
User names are not encrypted.
User objects are not thread-safe. Please create a new object to use in each thread!
Krptn does not verify the security of the password (e.g: complexity), please do this yourself!
User names cannot be longer then 450 characters
Here is an example usage of creating a new user:
1from krypton.auth.users import userModel
2
3model = userModel.standardUser(None)
4model.saveNewUser("Test_UserName", "Test_Password")
Warning
Please be carefull when setting credentials. The reasons are the following:
If you lose your credentials, and have not enabled password reset, you will permanently loose access to your account and data. To enable password reset, please read this document or skip to the part about password resets.
The encryption of your data is derieved from your credentials. Therefore, weak password equates to easily cracked encryption.
All that said, don’t panic :-); just enable password resets and validate user passwords for length, complexity, etc…
To delete the user, call the .delete()
method:
1model.delete()
4.1. Data storage
1model = userModel.standardUser(userName="Test_UserName") # If user does not exist will fail silently
2# It will raise an error on model.login as below.
3sessionKey = model.login(pwd="Test_Password") # See below what sessionKey is
To retreive and set user data as key-value pairs using data
attribute of the object:
1model.data.role = "admin" # "admin" would now be stored in the DB
2print(model.data.role) # Would fetch "role" from the DB, and decrypt it
3del model.data.role # Would delete it from the DB
Note: any data set under model.data
will be encrypted & stored in the database. They will be persisted between new instances of user objects. This a very clean solution.
Alternatively, to retreive and set user data as key-value pairs using functions:
1model.setData("test", "example") # test is the key and example is the value
2data = model.getData("test") # Gives b"example". Would raise ValueError on error.
3model.deleteData("test")
model = userModel.standardUser(userName="Test_UserName")
can be replaced by model = userModel.standardUser(userID=123)
. The userID
can be obtained from model.id
for a logged in user.
Note
Do make sure that the key in setData does not start with _
- those are reserved for Krptn internals.
To avoid side channel attacks, userModel.standardUser(userName="xyz")
{l=python} will fail silently if the user does not exist. An error will be raised on login
instead.
You can also use model.encryptWithUserKey
with model.decryptWithUserKey
, or shareSet
with shareGet
, if you want other users to read it. Please study the Data Sharing section of this document.
Warning
In only the stored values are encrypted. Keys are plaintext!! Avoid storing sensitive data in keys!
4.2. User Sessions
Session keys can be used to restore a session after the user object has been destroyed. For example, in a webserver, the session key could be stored in a cookie, so that the model can be retrieved on each request.
Session keys are returned from user.login
and user.saveNewUser
.
To restore a session:
1model = userModel.standardUser(userName="Test_UserName")
2model.restoreSession(sessionKey)
To set session expiry please see the configurations.
4.2.1. Sign out of all sessions
1model.revokeSessions()
4.3. Logs
To control the retention period of logs, please see the configurations.
Once the user is logged in, it is easy to recall the login logs:
1model.getLogs()
This returns a list, in the folowing format:
1[[time: datetime, success: bool], ...]
It is a 2-dimensional list. The first item in the nested list, is always the DateTime object representing the time of the log. The second item in the nested list, is a Boolean representing the success status of the attempt.
As mentioned in ISO/IEC 27002, it is a good idea to display the past login attempts to the user. This way, the user can easily notice an attack.
4.4. Change Username
In case you want to change the user’s username, you can simply do this by calling the changeUserName
method.
1model.changeUserName("NewName")
4.5. MFA
To avoid getting locked out, you may want to read Password Reset section of this document.
Before using MFA, make sure that the required configuration values are set.
4.5.1. TOTP
To enable:
1secret, qr = model.enableMFA()
2# Secret is a shared secret and qr is a string, that when converted to QR code can be scanned by authenticator apps.
3# If QR Codes are not supported by the app, you can tell the user enter secret instead.
4# You MUST discard these once the user enabled MFA.
When logging in:
1model.login(pwd="pwd", mfaToken="123456")
If a wrong code is provided, Krptn will impose a 5 second delay to slow brute force attacks. However, please note that is not enough to fully protect you. Therefore, it is necessary to employ a proper rate limiting solution on your webserver.
To disable TOTP (user must be logged in):
1model.disableMFA()
Note
On a failed login attempt, we will impose a 5 second delay to slow down brute force attacks. This is not available for purely password based authentication, so please do impose rate limiting protection on your server.
4.5.2. FIDO Passwordless
See FIDO Docs.
4.6. Data Sharing
Using these methods, you can grant another user access to some of the user’s data.
While deploying these methods, all data remains encrypted using the user’s credentials. No data is ever plaintext in a database! We use Elliptic-curve Diffie–Hellman
to share a common encryption key between the users, and we encrypt the data with the common key. Each user has their own Elliptic Curve key, with the private key encrypted with the user’s credentials.
Warning
One thing to note: if the original user used to set/encrypt the data is deleted. All other users will loose access. It is important that the other users create their own copy if they want to retain it.
4.6.2. Encryption
When possible, it is preferred to use shareSet
and shareGet
but when required you can directly use only Krptn’s encryption capabilities. E.g: if you want to use another database to store this data.
1model = userModel.standardUser(None)
2model.saveNewUser("Test_UserName", "Test_Password")
3
4model2 = userModel.standardUser(None)
5model2.saveNewUser("Test_UserName2", "Test_Password")
6
7r = model.encryptWithUserKey("data")
8model.decryptWithUserKey(r) # Returns b"data"
9
10## Here is the tricky part:
11
12r = model.encryptWithUserKey("data", ["Test_UserName2"]) # Allow Test_UserName to decrypt the data
13model2.decryptWithUserKey(r[0][1], "Test_UserName") # Returns b"data"
In the case that an incorrect data, or key is provided, a ValueError
will be raised.
encryptWithUserKey
needs the following parameters: data
, otherUsers
(optional). data
is the plaintext to encrypt and otherUsers
is a list of usernames of users who can also decrypt the data.
encryptWithUserKey
returns a list of tuples in the following format: (username, data)
. username
is the name of the user to who we need to provide data
.
When decrypting, call decryptUserKey
, on the user object corresponding to username
, passing data
as the first argument, and the encryptor’s user name as the second argument. It will return the plaintext.
Therefore, by using this method, you can grant another user access to some of the user’s data, simply by allowing that user to decrypt the data.
4.6.3. Unsafe Sharing
Warning
Data which is shared using this method is not encrypted - hence any user can access it. This isn’t necessarily a problem when storing data which is meant to be public. However, just note that you shouldn’t store secret data.
Note
Because this data is shared accross all user’s, its name
must be unique across all users. If you attempt to set a data with a name
which has already been taken by another user, you will get an error from the SQL layer.
1model = userModel.standardUser(None)
2model.saveNewUser("Test_UserName", "Test_Password")
3
4model2 = userModel.standardUser(None)
5model2.saveNewUser("Test_UserName2", "Test_Password")
6
7model.setUnsafe("test", b"TEST_VALUE")
8
9data = user2.getUnsafe("test")
10
11model.deleteData("test")
4.7. Password Reset
To enable password reset you need to obtain recovery codes, that you can use to unlock the account.
1keys = model.enablePWDReset() # keys is a list of OTPs that can be used to unlock the user account
2model.logout() # This is not needed but you can reset the password of a locked out user.
3sessionKey = model.resetPWD(keys[0], "newPWD") # Note: you cannot use keys[0] again, use the next one in the list.
4# Note: when you call resetPWD the model will automatically login, you may want to logout
5model.logout()
You can choose to email these codes to the user (therefore delegating trust to the email account), or any other way to handle this. It is also possible to split the codes in half, email the first half to a primary email, and send the 2nd half to secondary email - this way, for the hacker, they would need to compromise two emails instead of one. Aditionally, you may also decide to email a user when a recovery code is used - to help them prevent attacks.
If a wrong code is provided, Krptn will impose a 5 second delay to slow brute force attacks. However, please note that is not enough to fully protect you. Therefore, it is necessary to impose a proper rate limiting solution on your webserver.
You may notice in the previous code block the resetPWD
returns a sessionKey
. This session key is the same as returned from the model.login
method.
If the OTPs get compromised you can revoke them and generate new ones:
1model.disablePWDReset() # Revoke
2keys = model.enablePWDReset() # Generate. This also revokes all codes but we already did so previously.