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.1. Sharing

 1model = userModel.standardUser(None)
 2model.saveNewUser("Test_UserName", "Test_Password") # Note: if a user with the same username exists an ValueError will be raised.
 3
 4model2 = userModel.standardUser(None)
 5model2.saveNewUser("Test_UserName2", "Test_Password")
 6
 7# Save value "data" with key "test" and allow access to user "Test_UserName"
 8user2.shareSet("test", "data", ["Test_UserName"])
 9value = model.shareGet("test") # returns b"data". Raises ValueError on error.
10user2.shareDelete("test") # deletes the data - can only be done by the user who shared it

Note

Do make sure that the key in shareSet does not start with _ - those are reserved for Krptn internals.

If the a user has used shareSet to send data to the same user multiple times with the same name (name as in the key/identifier for the data), you will get an error from the SQL Layer. To avoid such conflicts, make sure to shareDelete data, or use a different name.

As you can see above, shareSet requires you to pass a unique name for the data ("test" in this case), the data ("data" in this case), and a list of usernames who can access it (["Test_UserName"] above).

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.