TDS Authentication Guide
Starting from TapSDK 3.0, there will be a built-in account system for you to use in your game. You can generate user accounts (TDSUser
) in your game with the results of TapTap OAuth. You can also link the authentication results of third-party platforms to this account.
The Friends and Achievements services provided by the TapSDK also depend on this account system.
Initialization
See TapSDK Quickstart for how to initialize the SDK.
TDSUser
and LCUser
TDSUser
is inherited from the LCUser
class.
LCUser
is the account system provided by LeanCloud, and TDSUser
basically inherited all the interfaces provided by LCUser
. TDSUser
includes some minor adjustments we made on functionalities and interfaces to fulfill the needs of TDS, so we recommend that you implement the account system in your game with TDSUser
.
TapTap Login
See Integrate TapTap Login for more details.
Guest Login
To create a guest account in the account system:
- Unity
- Android
- iOS
try{
// tdsUSer will hold a unique identifier of the user, if it exists
var tdsUser = await TDSUser.LoginAnonymously();
}catch(Exception e){
// Failed to log in
Debug.Log($"{e.code} : {e.message}");
}
TDSUser.logInAnonymously().subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(TDSUser resultUser) {
// Successfully logged in and obtained an account instance
String userId = resultUser.getObjectId();
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
[TDSUser loginAnonymously:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user) {
NSString *userId = user.objectId;
} else {
NSLog(@"%@", error);
}
}];
The guest account ensures that the player will have access to the same account on different logins. However, if the player deletes the game and then reinstalls the game, it is not guaranteed that the player will still have access to the same account.
Current User
Once the user has logged in, the SDK will automatically save the session to the client so that the user won’t need to log in again the next time they open the client. The code below checks if there is a logged-in user:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
if (currentUser != null) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
TDSUser currentUser = TDSUser.getCurrentUser();
if (currentUser != null) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
TDSUser *currentUser = [TDSUser currentUser];
if (currentUser != nil) {
// Go to homepage
} else {
// Show the sign-up or the log-in page
}
The session will remain valid until the user logs out:
- Unity
- Android
- iOS
await TDSUser.Logout();
// currentUser becomes null
TDSUser currentUser = await TDSUser.GetCurrent();
TDSUser.logOut();
// currentUser becomes null
TDSUser currentUser = TDSUser.getCurrentUser();
[TDSUser logOut];
// currentUser becomes nil
TDSUser *currentUser = [TDSUser currentUser];
Setting the Current User
A session token will be returned to the client after a user is logged in. It will be cached by our SDK and will be used for authenticating requests made by the same TDSUser
in the future. The session token will be included in the header of each HTTP request made by the client, which helps the cloud identify the TDSUser
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 (you can get the session token of the current user by accessing the
sessionToken
property; you can also get thesessionToken
of any user on the server side with your Master Key (also called Server Secret)). - 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 and the server is able to provide the session token to the client.
The code below logs a user in with a session token (the session token will be validated before proceeding):
- Unity
- Android
- iOS
await TDSUser.BecomeWithSessionToken("anmlwi96s381m6ca7o7266pzf");
TDSUser.becomeWithSessionTokenInBackground("anmlwi96s381m6ca7o7266pzf").subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(TDSUser user) {
// Update currentUser
TDSUser.changeCurrentUser(user, true);
}
public void onError(Throwable throwable) {
// session token is invalid
}
public void onComplete() {}
});
[TDSUser becomeWithSessionTokenInBackground:@"anmlwi96s381m6ca7o7266pzf" block:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user != nil) {
// Successfully logged in
} else {
// session token is invalid
}
}];
For security reasons, please avoid passing URLs containing session tokens in non-private environments. This increases the risk of your session tokens being captured by attackers.
If Log out the user when password is updated is enabled on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings, the session token of a user will be reset in the cloud after this user changes the password and the client needs to prompt the user to log in again. Otherwise, 403 (Forbidden)
will be returned as an error.
The code below checks if a session token is valid:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
bool isAuthenticated = await currentUser.IsAuthenticated();
if (isAuthenticated) {
// session token is valid
} else {
// session token is invalid
}
boolean authenticated = TDSUser.getCurrentUser().isAuthenticated();
if (authenticated) {
// session token is valid
} else {
// session token is invalid
}
TDSUser *currentUser = [TDSUser currentUser];
NSString *token = currentUser.sessionToken;
[currentUser isAuthenticatedWithSessionToken:token callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// session token is valid
} else {
// session token is invalid
}
}];
Setting Other User Properties
The account system allows you to store nickname
and avatar
data associated with users. For example, you can store users’ nicknames by using nickname
field.
- Unity
- Android
- iOS
var currentUser = await TDSUser.GetCurrent(); // Get the instance of the current user
currentUser["nickname"] = "Tarara";
await currentUser.Save();
TDSUser currentUser = TDSUser.currentUser(); // Get the instance of the current user
currentUser.put("nickname", "Tarara");
currentUser.saveInBackground().subscribe(new Observer<LCObject>() {
@Override
public void onSubscribe(@NotNull Disposable d) {
}
@Override
public void onNext(@NotNull LCObject lcObject) {
// Saved; the properties of currentUser are updated
TDSUser tdsUser = (TDSUser) lcObject;
}
@Override
public void onError(@NotNull Throwable e) {
}
@Override
public void onComplete() {
}
});
TDSUser *currentUser = [TDSUser currentUser];
currentUser[@"nickname"] = @"Tarara";
[currentUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// Saved
} else {
NSLog(@"%@", error);
}
}];
The account system supports only two extra fields besides the built-in ones: nickname
and avatar
. Adding other new fields will cause an error.
The account system contains users’ authentication data as well as emails and phone numbers, so there will be strict permission settings imposed on it to prevent the leak of the data.
Besides the security concerns, having too much data in the account system can also lead to performance issues like the occurrence of slow queries.
Therefore, we restrict the use of fields. If you want to store other user information, we suggest that you create a dedicated class (like UserProfile
) to store it.
We recommend that you store users’ nicknames with the nickname
field because TDS’s Friends module uses the data in this field when looking up friends with nicknames or generating invitation links.
If you log a user in with the result of TapTap OAuth, the SDK will automatically set the nickname
of the user to be the username of their TapTap account.
Queries on Users
TDSUser
is a subclass of LCObject
. This means that you can create, read, update, and delete user objects in the same way as you do with LCObject
s. See Data Storage Overview for more details.
For security reasons, the account system (the _User
table) 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 Cloud Engine and avoid opening up find
permissions of _User
tables.
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 TDSUser
s work in the same way as that of LCObject
s. The code below saves a new book for an author and retrieves all the books written by that author:
- Unity
- Android
- iOS
LCObject book = new LCObject("Book");
TDSUser 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();
LCObject book = new LCObject("Book");
TDSUser author = TDSUser.getCurrentUser();
book.put("title", "My Fifth Book");
book.put("author", author);
book.saveInBackground().subscribe(new Observer<LCObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(LCObject book) {
// Find all the books by the same author
LCQuery<LCObject> query = new LCQuery<>("Book");
query.whereEqualTo("author", author);
query.findInBackground().subscribe(new Observer<List<LCObject>>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(List<LCObject> books) {
// books is an array of Book objects by the same author
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
LCObject *book = [LCObject objectWithClassName:@"Book"];
TDSUser *author = [TDSUser currentUser];
[book setObject:@"My Fifth Book" forKey:@"title"];
[book setObject:author forKey:@"author"];
[book saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
// Find all the books by the same author
LCQuery *query = [LCQuery queryWithClassName:@"Book"];
[query whereKey:@"author" equalTo:author];
[query findObjectsInBackgroundWithBlock:^(NSArray *books, NSError *error) {
// books is an array of Book objects by the same author
}];
}];
Security of User Objects
The TDSUser
class is secured by default. You are not able to invoke any save- or delete-related methods unless the TDSUser
was obtained using an authenticated method like logging in. This ensures that each user can only update their own data.
The reason behind this is that most data stored in TDSUser
can be very personal and sensitive, such as mobile phone numbers, social network account IDs, etc. Even the app’s owner should avoid tampering with these data for the sake of users’ privacy.
The code below illustrates this security policy:
- Unity
- Android
- iOS
try {
TDSUser tdsUser = await TDSUser.LoginWithTapTap();
// Attempt to change username
user["username"] = "Jerry";
// This will work since the user is authenticated
await user.Save();
// Get the user with a non-authenticated method
LCQuery<TDSUser> userQuery = TDSUser.GetQuery();
TDSUser unauthenticatedUser = await userQuery.Get(user.ObjectId);
unauthenticatedUser["username"] = "Toodle";
// This will cause an error since the user is unauthenticated
unauthenticatedUser.Save();
} catch (LCException e) {
print($"{e.code} : {e.message}");
}
TDSUser.loginWithTapTap(MainActivity.this, new Callback<TDSUser>() {
@Override
public void onSuccess(TDSUser resultUser) {
Toast.makeText(MainActivity.this, "Logged in with TapTap.", Toast.LENGTH_SHORT).show();
// This will work since the user is authenticated
resultUser.put("username", "Toodle");
// For demonstration only; please use the asynchronous method in your project to avoid blocking the thread
resultUser.save();
// Get the user with a non-authenticated method
LCQuery<TDSUser> query = new LCQuery<>("_User");
query.getInBackground(user.getObjectId()).subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(TDSUser unauthenticatedUser) {
unauthenticatedUser.put("username", "Toodle");
// This will cause an error since the user is unauthenticated
unauthenticatedUser.save();
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
}
@Override
public void onFail(TapError error) {
Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
}
}, "public_profile");
[TDSUser loginByTapTapWithPermissions:@[@"public_profile"] callback:^(TDSUser * _Nullable user, NSError * _Nullable error) {
if (user) {
// Attempt to change username
[user setObject:@"Jerry" forKey:@"username")];
// Save changes
[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// This will work since the user is authenticated
// Get the user with a non-authenticated method
LCQuery *query = [TDSUser query];
[query getObjectInBackgroundWithId:user.objectId block:^(LCObject *unauthenticatedUser, NSError *error) {
[unauthenticatedUser setObject:@"Toodle" forKey:@"username"];
[unauthenticatedUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// This will not succeed since the user is unauthenticated
} else {
// Failure is expected
}
}];
}];
} else {
// Error handling
}
}];
} else {
// Error handling
}
}];
The LCUser
obtained from TDSUser.GetCurrent()
will always be authenticated.
To check if a TDSUser
is authenticated, you can invoke the isAuthenticated
method. You do not need to check if TDSUser
is authenticated if it is obtained via an authenticated method.
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
We have already introduced how to implement quick log-in with TapTap.
Besides TapTap, you can also use other services (like Apple and Facebook) to implement your account system. You can also associate existing accounts with these services so that the users can quickly log in with their existing accounts on these services.
Technically, we have implemented our interfaces in an open-ended manner. You can specify your own platform identifiers and authorization data, which means that our account system supports whatever third-party services you wish to connect to. For example, once you get the authorization data from Facebook, you can use TDSUser.loginWithAuthData
to log the user in (you may set the platform name to be facebook
).
The code below shows how you can log a user in with WeChat:
- Unity
- Android
- iOS
Dictionary<string, object> thirdPartyData = new Dictionary<string, object> {
// Optional
{ "openid", "OPENID" },
{ "access_token", "ACCESS_TOKEN" },
{ "expires_in", 7200 },
{ "refresh_token", "REFRESH_TOKEN" },
{ "scope", "SCOPE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(thirdPartyData, "weixin");
Map<String, Object> thirdPartyData = new HashMap<String, Object>();
// Optional
thirdPartyData.put("expires_in", 7200);
thirdPartyData.put("openid", "OPENID");
thirdPartyData.put("access_token", "ACCESS_TOKEN");
thirdPartyData.put("refresh_token", "REFRESH_TOKEN");
thirdPartyData.put("scope", "SCOPE");
TDSUser.loginWithAuthData(TDSUser.class, thirdPartyData, "weixin").subscribe(new Observer<TDSUser>() {
public void onSubscribe(Disposable disposable) {
}
public void onNext(TDSUser user) {
System.out.println("Logged in.");
}
public void onError(Throwable throwable) {
System.out.println("An error occurred.");
}
public void onComplete() {
}
});
NSDictionary *thirdPartyData = @{
// Optional
@"openid":@"OPENID",
@"access_token":@"ACCESS_TOKEN",
@"expires_in":@7200,
@"refresh_token":@"REFRESH_TOKEN",
@"scope":@"SCOPE",
};
TDSUser *user = [TDSUser user];
LCUserAuthDataLoginOption *option = [LCUserAuthDataLoginOption new];
option.platform = LeanCloudSocialPlatformWeiXin;
[user loginWithAuthData:thirdPartyData platformId:LeanCloudSocialPlatformWeiXin options:option callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Logged in.");
} else {
NSLog(@"An error occurred: %@",error.localizedFailureReason);
}
}];
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 includesuid
,access_token
, andexpires_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 identityToken
s and obtain access_token
s 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 toidentity_token
andcode
only when the platform name islc_apple
.uid
: Required. The cloud tells if the user exists withuid
.identity_token
: Optional. The cloud will automatically validateidentity_token
if this field exists. Please make sure you have provided relevant information on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings > Third-party accounts.code
: Optional. The cloud will automatically obtainaccess_token
andrefresh_token
from Apple if this field exists. Please make sure you have provided relevant information on Developer Center > Your game > Game Services > Cloud Services > TDS 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 Developer Center 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 Developer Center, you can log a user in with the following code:
- Unity
- Android
- iOS
Dictionary<string, object> appleAuthData = new Dictionary<string, object> {
// Required
{ "uid", "USER IDENTIFIER" },
// Optional
{ "identity_token", "IDENTITY TOKEN" },
{ "code", "AUTHORIZATION CODE" }
};
TDSUser currentUser = await TDSUser.LoginWithAuthData(appleAuthData, "lc_apple");
// Not supported yet
NSDictionary *appleAuthData = @{
// Required
@"uid":@"USER IDENTIFIER",
// Optional
@"identity_token":@"IDENTITY TOKEN",
@"code":@"AUTHORIZATION CODE",
};
TDSUser *user = [TDSUser user];
[user loginWithAuthData:appleAuthData platformId:"lc_apple" options:nil callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Logged in.");
}else{
NSLog(@"Failed to log in: %@",error.localizedFailureReason);
}
}];
Storing Authentication Data
The authData
of each user is a JSON object with platform names as keys and authentication data as values.
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 Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings.
To disable the feature, please uncheck Validate access tokens when logging in with third-party accounts on Developer Center > Your game > Game Services > Cloud Services > TDS Authentication > Settings.
The reason for configuring the platforms is that when a TDSUser
is created, the cloud will use the relevant data to validate the thirdPartyData
to ensure that the TDSUser
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. For example, if a user first logs in as a guest and then links their TapTap or other third-party accounts, the user will be able to access the same account when they log in with the same TapTap or third-party accounts in the future.
After a user links their third-party account, the account information will be added to the authData
field of the corresponding TDSUser
.
The following code links a WeChat account to a user:
- Unity
- Android
- iOS
await currentUser.AssociateAuthData(weixinData, "weixin");
user.associateWithAuthData(weixinData, "weixin").subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("Account linked.");
}
@Override
public void onError(Throwable e) {
System.out.println("Failed to link the account: " + e.getMessage());
}
@Override
public void onComplete() {
}
});
[user associateWithAuthData:weixinData platformId:LeanCloudSocialPlatformWeiXin options:nil callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Account linked.");
} else{
NSLog(@"Failed to link the account: %@",error.localizedFailureReason);
}
}];
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:
- Unity
- Android
- iOS
TDSUser currentUser = await TDSUser.GetCurrent();
await currentUser.DisassociateWithAuthData("weixin");
TDSUser user = TDSUser.currentUser();
user.dissociateWithAuthData("weixin").subscribe(new Observer<TDSUser>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TDSUser user) {
System.out.println("Unlinked.");
}
@Override
public void onError(Throwable e) {
System.out.println("Failed to unlink: " + e.getMessage());
}
@Override
public void onComplete() {
}
});
[user disassociateWithPlatform:LeanCloudSocialPlatformWeiXin callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"Unlinked.");
} else{
NSLog(@"Failed to unlink: %@",error.localizedFailureReason);
}
}];