Skip to main content

Authentication Guide

At the core of many apps, there is a notion of user accounts that allows users to access their information in a secure manner. We provide a specialized user class called LCUser which automatically handles much of the functionality required for user account management in your app.

LCUser is a subclass of LCObject. Therefore, all the methods that work for LCObject also work for LCUser. The only difference is that LCUser has some additional features specific to user accounts.

User Properties

LCUser offers the following fields that LCObject does not have:

  • username: The username of the user.
  • password: The password of the user.
  • email: The email address of the user.
  • emailVerified: Whether the user has verified the email address or not.
  • mobilePhoneNumber: The mobile phone number of the user.
  • mobilePhoneVerified: Whether the user has verified the mobile phone number or not.

We'll go through each of these in detail as we run through the various use cases for users.

Signing up

When a user first opens your app, you may want them to sign up for an account. The following code shows a typical sign-up process with username and password:

// Create an instance
LCUser user = new LCUser();

// Same as user["username"] = "Tom";
user.Username = "Tom";
user.Password = "cat!@#123";

// Optional
user.Email = "tom@xd.com";
user.Mobile = "+15559463664";

// Other fields can be set in the same way as LCObject
user["gender"] = "secret";
await user.SignUp();

A new LCUser should always be created using SignUp rather than Save. Subsequent updates to a user can be done with Save.

If the code returns the error 202, it means that a user with the same username already exists and the client should prompt the user to try a different username. It is also required that each email or mobilePhoneNumber appears only once in the corresponding column. Otherwise, error 203 or 214 will occur. You may ask a user to sign up with an email address and make the username to be the same as the email. By doing so, the user can directly reset their password with email.

When creating a user with username and password, the SDK sends the password to the cloud in plaintext through HTTPS and the password will be hashed once it arrives the cloud. The cloud has no restrictions on password length and complexity. We never store passwords in plaintext, nor will we ever transmit passwords back to the client in plaintext. Our hashing algorithm guarantees that the original password cannot be retrieved by rainbow table attack and even app developers themselves will not be able to see the password. Keep in mind that the password should not be hashed by the client, or the password reset function will not work.

Signing up with Phones

For a mobile app, it's also common to ask users to sign up with their phone numbers instead of usernames and passwords. There are two basic steps in it. First, ask the user to enter a phone number that can receive text messages. When the user clicks on the "Get Verification Code" button, call the following method to have a 6-digit verification code sent to the phone number the user just entered:

await LCSMSClient.RequestSMSCode("+15559463664");

After the user enters the verification code, call the following method to finish signing up:

await LCUser.SignUpOrLoginByMobilePhone("+15559463664", "123456");

The username will be the same as mobilePhoneNumber and a password will be generated by the cloud automatically. If you wish to let the user set their own password, you can let them fill in a password together with their mobile phone number, and then follow the process of registering with username and password described in the previous section, submitting the user's mobile phone number as the value of both the username and the mobilePhoneNumber fields. If you wish, you can also check the following options on "Dashboard > Authentication > Settings": "Do not allow users with unverified phone numbers to log in" and/or "Allow users with verified phone numbers to login with SMS".

Phone Number Format

A phone number should have a leading plus sign (+) immediately followed by the country code and the phone number without any dashes, spaces, or other non-numeric characters. For instance, +8619201680101 is a valid China number (86 is the country code) and +15559463664 is a valid U.S. or Canada number (1 is the country code).

For a list of supported countries and regions, please refer to the Pricing page on our website.

Logging in

The code below logs a user in with their username and password:

try {
// Logged in successfully
LCUser user = await LCUser.Login("Tom", "cat!@#123");
} catch (LCException e) {
// Failed to log in (the password may be incorrect)
print($"{e.code} : {e.message}");
}

Logging in with Emails

The code below logs a user in with their email and password:

try {
// Logged in successfully
LCUser user = await LCUser.LoginByEmail("tom@xd.com", "cat!@#123");
} catch (LCException e) {
// Failed to log in (the password may be incorrect)
print($"{e.code} : {e.message}");
}

Logging in with Phones

If you are allowing users to sign up with their phone numbers, you can also let them log in with either a password or a verification code sent via text message. The code below logs a user in with their phone number and password:

try {
// Logged in successfully
LCUser user = await LCUser.LoginByMobilePhoneNumber("+15559463664", "cat!@#123");
} catch (LCException e) {
// Failed to log in (the password may be incorrect)
print($"{e.code} : {e.message}");
}

By default, a user can log in to their account as long as the phone number and the password they provided are correct even when the ownership of the phone hasn't been verified. To make your app more secure, you can choose to allow only those who have their phones verified to log in. The option can be found on Dashboard > Authentication > Settings.

You may also let a user in with a verification code sent to their phone, which is useful when the user forgets the password and does not want to reset it at the moment. Similar to the steps of signing a user up with phone numbers, ask the user to enter the phone number associated with the account, and call the following method once the user clicks on the "Get Verification Code" button:

await LCUser.RequestLoginSMSCode("+15559463664");

After the user enters the verification code, call the following method to finish logging in:

try {
// Logged in successfully
await LCUser.SignUpOrLoginByMobilePhone("+15559463664", "123456");
} catch (LCException e) {
// The verification code is incorrect
print($"{e.code} : {e.message}");
}

Sandbox Phone Number

During the development of your application, you may need to test the sign-up– or log-in–related APIs intensively with your phone. As there are, however, limits to how quickly messages can be sent into the carrier networks, your testing pace can be significantly affected.

To work around it, you can set up a sandbox phone number on Dashboard > SMS > Settings. The cloud will issue a fixed verification code to go with that sandbox phone number. Whenever the cloud detects such a combination of data, the user will be let right in authenticated without any connections to the carrier networks being made.

On a related note, a sandbox phone number also comes in handy for iOS apps that allow users to log in with SMS code. This is because Apple may ask developers to provide a fixed combination of phone number and verification code for them to review the app as a normal user. Failure to do so may result in their app being rejected by the App Store.

For more details regarding the limitations of sending and receiving SMS messages, see SMS Guide.

Single Device Sign-on

In some scenarios you may want to restrict a user's account to be logged on by no more than one device at a time. That is, when a user logs in to the app on a new device, all the previous sessions on other devices will become invalid. Here's the instruction about how you can implement this feature:

  1. Create a new class that keeps track of each user's credentials and device information.
  2. Each time when a user logs in on a device, update the device information of this user to be the current device.
  3. When the app running on another device is opened, check if the device matches the one stored in the cloud. If it does not, log the user out.

User Account Lockout

If the wrong password or verification code is entered for an account for more than 6 times within 15 minutes, the account will be disabled temporarily and the error { "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." } will be returned.

The account will be automatically recovered 15 minutes after the last attempt and the process cannot be expedited through SDK or REST API. While the account is disabled, the user cannot be logged in even though the correct credentials are provided. The restriction applies to both client-side SDKs and LeanEngine.

Verifying Emails

You can request that your users have their email addresses verified before they can log in or access certain functions in your app. This makes it harder for spam users to abuse your app. By default, each user has an emailVerified field which becomes false when the user first signs up or has their email address changed. In your app's Dashboard > Authentication > Settings, you can enable Send verification emails when users register or change email addresses from clients so that when a user signs up or changes their email address, an email containing a verification link will be sent out automatically. You can find the option to prevent users with unverified email addresses from logging in on the same page.

If a user forgets to click on the link and needs to have their account verified later, the following code can be used to send a new email:

await LCUser.RequestEmailVerify("tom@xd.com");

The emailVerified will become true after the link is clicked on. This field can never be true when the email field is empty.

Verifying Phone Numbers

Similar to Verifying Emails, you can also request that your users have their phone numbers verified before they can log in or access certain functions in your app. By default, each user has a mobilePhoneVerified field which becomes false when the user first signs up or has their phone number changed. In your app's Dashboard > Authentication > Setting, you can find the option to prevent users with unverified phone numbers from logging in on the same page.

You can also initiate a verification request at anytime with the following code (if the user's mobilePhoneVerified is already true, the verification code will not be sent):

await LCUser.RequestMobilePhoneVerify("+15559463664");

After the verification code is entered by the user, call the following method and the user's mobilePhoneVerified will become true:

await LCUser.VerifyMobilePhone("+15559463664", "123456");

Verify Phone Numbers Before Updating and Binding

The cloud also supports verifying the number before a user binds or updates their number. This means that when a user binds or updates their number, they need to first request for a verification code (while they are logged in) and then complete the process with the verification code they received.

await LCUser.RequestSMSCodeForUpdatingPhoneNumber("+15559463664");

await LCUser.VerifyCodeForUpdatingPhoneNumber("+15559463664", "123456");
// Update local data
LCUser currentUser = await LCUser.GetCurrent();
user.Mobile = "+15559463664";

Current User

Once a user logs in, the SDK will automatically store the session information of this user on the client so that the user does not need to log in each time they open the client. The following code checks if there exists a logged-in user:

LCUser currentUser = await LCUser.GetCurrent();
if (currentUser != null) {
// Redirect to the home page
} else {
// Show the sign-up or log-in page
}

The session information of a user will remain on the client until the user is logged out:

await LCUser.Logout();

// currentUser becomes null
LCUser currentUser = await LCUser.GetCurrent();

Setting The Current User

A session token will be returned to the client once a user logs in. It will be cached by the SDK and will be used for authenticating requests made by the same user in the future. The session token will be included in the header of each HTTP request made by the client, allowing the cloud to tell which user is sending the request.

Below are the situations when you may need to log a user in with their session token:

  • A session token is already cached on the client which can be used to automatically log the user in.
  • A WebView within the app needs to know the current user.
  • The user is logged in on the server side using your own authentication routines. The server can then send the session token to the client so the client can get logged in with the session token.

The code below logs a user in with their session token (the cloud will check if the session token is valid):

await LCUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf");

For security reasons, please avoid including session tokens as a part of URLs in non-private environments. This could increase the risk that they will be captured by attackers.

If Log out the user when password is updated is enabled on Dashboard > Authentication > Settings, the session token of a user will be reset if this user changes their password. The client will have to prompt the user to log in again, or the client will get the 403 (Forbidden) error.

The code below checks if a session token is valid:

LCUser currentUser = await LCUser.GetCurrent();
bool isAuthenticated = await currentUser.IsAuthenticated();
if (isAuthenticated) {
// The session token is valid
} else {
// The session token is invalid
}

Resetting Passwords

It's quite common for the users of an app to forget their passwords. There are a couple of ways for the users to reset their passwords.

Here is the procedure of resetting the password with email:

  1. The user enters the email address used for their account.
  2. The cloud sends an email to the address including a link for resetting the password.
  3. The user opens the link and provides a new password.
  4. The password will be reset once the user submits the new password.

To begin with, ask the user to enter the email used for the account and then call the function below:

await LCUser.RequestPasswordReset("tom@xd.com");

The code above will check if there is a user whose email is the same as the one provided and send them a password reset email if so. As mentioned earlier, you can make the username of each user to be the same as their email, or collect the email separately and store it in the email field.

The content of the password reset email is fully customizable. You can edit the template by going to your app's Dashboard > Authentication > Email templates.

Alternatively, a user can reset their password with their mobile phone number:

  1. The user enters the mobile phone number used for the account.
  2. The cloud sends a text message containing a verification code to the number.
  3. The user provides the verification code and a new password.

The code below sends a verification code to a number:

await LCUser.RequestPasswordRestBySmsCode("+15559463664");

The code above will check if there is a user whose mobilePhoneNumber is the same as the one provided and will send them a verification code if so.

On Dashboard > Authentication > Settings, you can decide if a user can reset their password only if their mobilePhoneVerified is true.

Once a user provides their verification code and a new password, use the code below to finish resetting the password:

await LCUser.ResetPasswordBySmsCode("+15559463664", "123456", "cat!@#123");

Queries on Users

Use the code below to retrieve the users from the cloud:

LCQuery<LCUser> userQuery = LCUser.GetQuery();

For security reasons, the _User table of each new app has its find permission disabled by default. Each user can only access their own data in the _User table and cannot access that of others. If you need to allow each user to view other users' data, we recommend that you create a new table to store such data and enable the find permission of this table. You may also encapsulate queries on users within LeanEngine and avoid opening up the find permission of the _User table.

See Security of User Objects for other restrictions applied to the _User table and Data Security for more information regarding class-level permission settings.

Associations

Associations involving users work in the same way as those involving basic objects. The code below saves a new book for an author and retrieves all the books written by this author:

LCObject book = new LCObject("Book");
LCUser author = await LCUser.GetCurrent();
book["title"] = "My Fifth Book";
book["author"] = author;
await book.Save();

LCQuery<LCObject> query = new LCQuery<LCObject>("Book");
query.WhereEqualTo("author", author);
// books is an array of Book objects by the same author
ReadOnlyCollection<LCObject> books = await query.Find();

Security of User Objects

User objects are secured by default. You are not able to update or delete a user object unless this object is obtained using a method that gets it authenticated. This ensures that each user can only update their own data.

The consideration of this design is that most data stored in an user object can be very personal and sensitive, such as mobile phone number, social media ID, etc. Even the app's owner should avoid tampering with these data for the sake of user's privacy.

The code below demonstrates this security measure:

try {
LCUser user = await LCUser.Login("Tom", "cat!@#123");
// Attempt to change the username
user["username"] = "Jerry";
// The password is encrypted and an empty string will be obtained
string password = user["password"];
// This will work because the user is authenticated
await user.Save();

// Getting a user without authenticating
LCQuery<LCUser> userQuery = LCUser.GetQuery();
LCUser unauthenticatedUser = await userQuery.Get(user.ObjectId);
unauthenticatedUser["username"] = "Toodle";

// This will not work because the user is not authenticated
unauthenticatedUser.Save();
} catch (LCException e) {
print($"{e.code} : {e.message}");
}

When you obtain a user object with the method for obtaining the current user, the object will always be authenticated.

To check if a user object is authenticated, you can invoke the method shown below. You do not need to check if a user object is authenticated if it is obtained via an authenticated method.

IsAuthenticated

As a reminder, the user's password can be set when signing up but cannot be modified and saved to the cloud afterward unless the user requests it to be reset. It will not be cached on the client and will show as null when being retrieved from the cloud after the user is logged in.

Security of Other Objects

For each given object, you can specify which users are allowed to read it and which are allowed to modify it. To support this type of security, each object has an access control list implemented by an ACL object. More details can be found in ACL Guide.

Third-Party Sign-on

You can let your users sign up and log in with their existing accounts on services like WeChat, Weibo, and QQ. You can also let them link their existing accounts with those services so that they will be able to log in with their accounts on those services in the future.

The code below shows how you can log a user in with WeChat:

Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// Required
{ "openid", "OPENID" },
{ "access_token", "ACCESS_TOKEN" },
{ "expires_in", 7200 },

// Optional
{ "refresh_token", "REFRESH_TOKEN" },
{ "scope", "SCOPE" }
};
LCUser currentUser = await LCUser.LoginWithAuthData(thirdPartyData, "weixin");

loginWithAuthData requires two arguments to locate a unique account:

  • The name of the third-party platform, which is weixin in the example above. You can decide this name on your own.
  • The authorization data from the third-party platform, which is the thirdPartyData in the example above (depending on the platform, it usually includes uid, access_token, and expires_in).

The cloud will then verifies that the provided authData is valid and checks if a user is already associated with it. If so, it returns the status code 200 OK along with the details (including a sessionToken of the user). If the authData is not linked to any accounts, you will instead receive the status code 201 Created, indicating that a new user has been created. The body of the response contains objectId, createdAt, sessionToken, and an automatically-generated unique username. For example:

{
"username": "k9mjnl7zq9mjbc7expspsxlls",
"objectId": "5b029266fb4ffe005d6c7c2e",
"createdAt": "2018-05-21T09:33:26.406Z",
"updatedAt": "2018-05-21T09:33:26.575Z",
"sessionToken": "…",
// authData won't be returned in most cases; see explanations below
"authData": {
"weixin": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}
}
// …
}

Now we will see a new record showing up in the _User table that has an authData field. Within this field is the authorization data from the third-party platform. For security reasons, the authData field won’t be returned to the client unless the current user owns it.

You will need to implement the authentication process involving the third-party platform yourself (usually with OAuth 1.0 or 2.0) to obtain the authentication data, which will be used to log a user in.

Sign in with Apple

If you plan to implement Sign in with Apple, the cloud can help you verify identityTokens and obtain access_tokens from Apple. Below is the structure of authData for Sign in with Apple:

{
"lc_apple": {
"uid": "The User Identifier obtained from Apple",
"identity_token": "The identityToken obtained from Apple",
"code": "The Authorization Code obtained from Apple"
}
}

Each authData has the following fields:

  • lc_apple: The cloud will run the logic related to identity_token and code only when the platform name is lc_apple.
  • uid: Required. The cloud tells if the user exists with uid.
  • identity_token: Optional. The cloud will automatically validate identity_token if this field exists. Please make sure you have provided relevant information on Dashboard > Authentication > Settings > Third-party accounts.
  • code: Optional. The cloud will automatically obtain access_token and refresh_token from Apple if this field exists. Please make sure you have provided relevant information on Dashboard > Authentication > Settings > Third-party accounts.

Getting Client ID

Client ID is used to verify identity_token and to obtain access_token. It is the identifier of an Apple app (AppID or serviceID). For native apps, it is the Bundle Identifier in Xcode, which looks like com.mytest.app. See Apple’s docs for more details.

Getting Private Key and Private Key ID

Private Key is used to obtain access_token. You can go to Apple Developer, select “Keys” from “Certificates, Identifiers & Profiles”, add a Private Key for Sign in with Apple, and then download the .p8 file. You will also obtain the Private Key ID from the page you download the key. See Apple’s docs for more details.

The last step is to fill in the Key ID on the dashboard and upload the downloaded Private Key. You can only upload Private Keys, but cannot view or download them.

Getting Team ID

Team ID is used to obtain access_token. You can view your team’s Team ID by going to Apple Developer and looking at the top-right corner or the Membership page. Make sure to select the team matching the selected Bundle ID.

Logging in to Cloud Services With Sign in with Apple

After you have filled in all the information on the dashboard, you can log a user in with the following code:

Dictionary<string, object> appleAuthData = new Dictionary<string, object> {
// Required
{ "uid", "USER IDENTIFIER" },

// Optional
{ "identity_token", "IDENTITY TOKEN" },
{ "code", "AUTHORIZATION CODE" }
};
LCUser currentUser = await LCUser.LoginWithAuthData(appleAuthData, "lc_apple");

Storing Authentication Data

The authData of each user is a JSON object with platform names as keys and authentication data as values.

A user associated with a WeChat account will have the following object as its authData:

{
"weixin": {
"openid": "…",
"access_token": "…",
"expires_in": 7200,
"refresh_token": "…",
"scope": "…"
}
}

A user associated with a Weibo account will have the following object as its authData:

{
"weibo": {
"refresh_token": "2.0xxx",
"uid": "271XFEFEW273",
"expires_in": 115057,
"access_token": "2.00xxx"
}
}

A user can be associated with multiple third-party platforms. If a user is associated with both WeChat and Weibo, their authData may look like this:

{
"weixin": {
"openid": "…",
"access_token": "…",
"expires_in": 7200,
"refresh_token": "…",
"scope": "…"
},
"weibo": {
"refresh_token": "2.0xxx",
"uid": "271XFEFEW273",
"expires_in": 115057,
"access_token": "2.00xxx"
}
}

It’s important to understand the data structure of authData. When a user logs in with the following authentication data:

"platform": {
"openid": "OPENID",
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"scope": "SCOPE"
}

The cloud will first look at the account system to see if there is an account that has its authData.platform.openid to be the OPENID. If there is, return the existing account. If not, create a new account and write the authentication data into the authData field of this new account, and then return the new account’s data as the result.

The cloud will automatically create a unique index for the authData.<PLATFORM>.<uid> of each user, which prevents the formation of duplicate data. For some of the platforms specially supported by us, <uid> refers to the openid field. For others (the other platforms specially supported by us, and those not specially supported by us), it refers to the uid field.

Automatically Validating Third-Party Authorization Data

The cloud can automatically validate access tokens for certain platforms, which prevents counterfeit account data from entering your app’s account system. If the validation fails, the cloud will return the invalid authData error, and the association will not be created. For those services that are not recognized by the cloud, you need to validate the access tokens yourself. You can validate access tokens when a user signs up or logs in by using LeanEngine’s beforeSave hook and beforeUpdate hook.

To enable the feature, please configure the platforms’ App IDs and Secret Keys on Dashboard > Authentication > Settings.

To disable the feature, please uncheck Validate access tokens when logging in with third-party accounts on Dashboard > Authentication > Settings.

The reason for configuring the platforms is that when a user object is created, the cloud will use the relevant data to validate the thirdPartyData to ensure that the user object matches a real user, which ensures the security of your app.

Linking Third-Party Accounts

If a user is already logged in, you can link third-party accounts to this user.

After a user links their third-party account, the account information will be added to the authData field of the corresponding user object.

The following code links a WeChat account to a user:

await currentUser.AssociateAuthData(weixinData, "weixin");

The code above omitted the authorization data of the platform. See Third-Party Sign-on for more details.

Unlinking

Similarly, a third-party account can be unlinked.

For example, the code below unlinks a user’s WeChat account:

LCUser currentUser = await LCUser.GetCurrent();
await currentUser.DisassociateWithAuthData("weixin");

Anonymous Users

With the support of anonymous users, you can have your users try the application without signing up or logging in. The code below creates an anonymous user:

await LCUser.LoginAnonymously();

You can add new properties or fields to an anonymous user just like with a normal user, such as username, password, email, etc. You can also convert an anonymous user to a normal user by going through the same sign-up process as you do with a normal user. An anonymous user can:

The code below sets a username and password for an anonymous user:

LCUser currentUser = await LCUser.LoginAnonymously();
currentUser.Username = "Tom";
currentUser.Password = "cat!@#123";

await currentUser.SignUp();

The code below checks if the current user is anonymous:

LCUser currentUser = await LCUser.GetCurrent();
if (currentUser.IsAnonymous) {
// currentUser is anonymous
} else {
// currentUser is not anonymous
}

If an anonymous user is not converted to a normal user before they log out, they will not be able to log in to the same account later and the data stored in that account cannot be retrieved anymore.