. . .

ApiOmat and OAuth2


This documentation describes what OAuth2 is and how to use the OAuth2 function that’s available in ApiOmat.

What is OAuth2?

OAuth2 is an authorization mechanism which is used by many popular websites such as Google or Facebook.

One use case is the ability of websites to give 3rd party apps limited access to user data without having to provide passwords. Instead, a token with limited validity will be issued. and might also have other limitations associated with it (e.g. only read access). This token may have further associated limitations, such as only allowing read access.

At ApiOmat, we only use OAuth2 for user authentication and token managment, and do not grant 3rd party apps access to user data. We also added functionality like the possibility to have multiple valid tokens per user. We have also added functions, such as the ability of a user to simultaneously possess multiple valid tokens.

OAuth2 at ApiOmat

ApiOmat is a backend and middleware for mobile apps. These mobile apps have users with credentials. Traditionally, the credentials consist of a username and password. But this is very sensitive information. The password may get into the wrong hands when: the app is not secure, the device is rooted, or it’s a webapp written in JavaScript and only cookies are used as storage. With our OAuth2 implementation you can exchange the username and password against a set of so called tokens. The access token is used to authenticate the request and has a limited validity.

Traditional authentication

  1. User <- App | Ask for username + password

  2. User -> App | Enter username + password

  3. App | Save username + password (risky)

  4. App -> ApiOmat | Authenticate with username + password

OAuth2 authentication

  1. User <- App | Ask for username + password

  2. User -> App | Enter username + password

  3. App -> ApiOmat | Ask for a token map in exchange for the username + password

  4. App | Save access token or token map

  5. App <- ApiOmat | Respond with the token map

  6. App -> ApiOmat | Authenticate with the access token

Advantages

  • Due to the limited validity, if the access token gets stolen, the attacker only has a limited amount of time to send authenticated requests. As soon as the access token expires, they no longer have access and can no longer send requests. They also can’t refresh the token or change the password of the user even when the access token is still valid.

  • In case the refresh token is saved by the app and gets stolen by an attacker, the access token can get refreshed, but the attacker still doesn’t get access to the users password, which the user might use on other services and is thus more sensitive.

  • If malicious use is detected, the access and refresh tokens can be revoked before they expire. The user is not affected by this – they only have to re-enter their username + password, so the app can get a new token map.With traditional authentication, the app would have to disable / change the user’s password, which is not in the interest of the user.

Token map

This is how a token map looks for example:

  • Access token -> 4bcb3064-5c64-43f0-9dbd-3e00a9171510

  • Refresh token -> f14997c7-60c8-4z81-8892-36ez2ae3z0a1

  • Session token expiration -> 604800

  • Module -> Basics

  • Model -> com.apiomat.backend.modules.basics.User

The name of the keys and the value of the expiration depends on whether you fetch a token map in the SDK or via REST API.

When fetching in the SDK, the name “session token” gets used instead of “access token” to avoid confusion with the access token attribute in the user class when the Facebook module is activated. Additionally, the value of the expiration is a date.

When fetching via REST API, you get the original “access_token” as key. “expires_in” contains the amount of seconds that the access token remains valid.

Requesting a token map

You can request a token map in the SDK or via REST API.

Android
// Create a user, configure the Datastore, save the user
User user = new User("testUser", "secret");
Datastore.configureWithCredentials( User.baseURL, User.apiKey, user.getUserName( ), user.getPassword(), User.sdkVersion, "LIVE");
user.save();
// Request the token map
TokenContainer tokenMap = user.requestTokenContainer();
/* With the call to requestSessionToken() the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
String sessionToken = tokenMap.getSessionToken( );
Datastore.configureWithSessionToken( User.baseURL, user.apiKey, user.sdkVersion, system, sessionToken );
// All following requests will be authenticated with the session token instead of username + password
Objective-C
// Create a user, configure the Datastore, save the user
AOMUser *user = [[AOMUser alloc] initWithUserName:@"testUser" andPassword:@"secret"];
[AOMDatastore configureWithCredentials:user];
[user save];
// Request the token map asynchronous
[user requestSessionTokenAsync:^(AOMTokenContainer *sessionData, NSError *error) {
/* With the call to requestSessionTokenAsync the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
NSString *sessionToken = [sessionData sessionToken];
[AOMDatastore configureWithSessionToken:sessionToken andWithUrl:baseUrl andApiKey:apiKey];
// All following requests will be authenticated with the session token instead of username + password
}];
Swift
// Create a user, configure the Datastore, save the user
let user = User(userName: "testUser", password: "secret")
DataStore.configureWithCredentials(user: user)
user.save { (error) in
// Request the token map asynchronous
user.requestSessionToken { (tokenContainer, error) in
/* With the call to requestSessionTokenAsync the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
let sessionToken = tokenContainer.sessionToken
DataStore.configureWithSessionToken(sessionToken)
// All following requests will be authenticated with the session token instead of username + password
}
}
JavaScript
// Create a user, configure the Datastore, save the user
var user = new Apiomat.User("testUser", "_password_");
Apiomat.Datastore.configureWithCredentials(user);
var saveCB = {
onOk: function() {
requestTokenMap();
},
 
onError: function(error) {
console.error(error);
}
};
user.save(saveCB);
 
function requestTokenMap()
{
var tokenMap;
// Request the token map
user.requestSessionToken(true, {
onOk : function(result) {
tokenMap = result;
console.info(tokenMap);
},
onError: function(error) {
console.error(error);
}
});
 
/* With the call to requestSessionToken(true, ...)
* the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
var sessionToken = tokenMap.sessionToken;
Apiomat.Datastore.configureWithSessionToken(sessionToken);
 
// All following requests will be authenticated with the session token
// instead of username + password
}
C#
// Create a user, configure the Datastore, save the user
User user = new User("testUser", "secret");
Datastore.ConfigureWithCredentials(user);
await user.SaveAsync();
// Request the token map
IDictionary<string, string> tokenMap = await user.RequestSessionTokenAsync();
/* With the call to RequestSessionTokenAsync() the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
string sessionToken = tokenMap["SessionToken"];
Datastore.ConfigureWithSessionToken(sessionToken);
// All following requests will be authenticated with the session token instead of username + password
Bash
HOST="https://apiomat.org"
APPNAME=testApp
API_KEY=1234567890123456789
SYSTEM=LIVE
 
# assuming the user already exists in the backend
 
USERNAME=testUser
PASSWORD=secret
 
 
# don't change anything below this comment
 
CLIENT_ID=$APPNAME
CLIENT_SECRET=$API_KEY
 
URL="$HOST/yambas/oauth/token"
PARAMS="--data grant_type=aom_user&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=read%20write&username=$USERNAME&app=$APPNAME&password=$PASSWORD&system=$SYSTEM"
 
# fetch token with username + password
echo curl -X POST $URL $PARAMS
curl -X POST $URL $PARAMS
TypeScript
// Create a user, configure the Datastore, save the user
const user = new User("testUser", "secret");
Datastore.configureAsUser(user);
await user.save();
// Request the token map
const tokenMap = await user.requestSessionToken();
/* With the call to requestSessionToken() the Datastore automatically
* gets configured with the received session token. Later you can do:
*/
const sessionToken = tokenMap.sessionToken;
Datastore.configureWithSessionToken(sessionToken);
// All following requests will be authenticated with the session token instead of username + password

Custom expiration time

When using the REST API, you can also set a custom expiration time per token request. This will be added to the SDK soon.

There are two additional values that can be set in a token request:

  • access_expiration

  • refreh_expiration

Example:

cURL
curl -X POST "https://apiomat.org/yambas/oauth/token" --data "grant_type=aom_user&client_id=YOUR_APP&client_secret=YOUR_API_KEY&scope=read%20write&username=USERNAME&app=YOUR_APP&password=PASSWORD&system=LIVE&access_expiration=60&refresh_expiration=120"

In the above example, the access token is valid for 60 seconds and the refresh token is valid for 120 seconds. The standard expiration time for tokens is usually much longer. It’s set to 7 days for access tokens and 30 days for refresh tokens (this might change in the future). Enterprise customers can customize the standard expiration time.

The setting of a custom expiration time only works when a token is created. So when a token exists and gets requested again, only with different expiration values, nothing changes. Only if a token expires or a token with new “extra” (see “Multiple tokens per user”) is requested does the changed custom expiration times take effect.

Read-only tokens

When requesting tokens with the scope “read write” (this means two scopes, they have to be separated by a space character), the token can be used for read and write access to objects. If you want to create a token for read access only, you can use the scope “read_only”.

Example:

cURL
curl -X POST "https://apiomat.org/yambas/oauth/token" --data "grant_type=aom_user&client_id=YOUR_APP&client_secret=YOUR_API_KEY&scope=read_only&username=USERNAME&app=YOUR_APP&password=PASSWORD&system=LIVE"

With the custom expiration times, adding a request with the scope “read_only” to an existing valid token that was created with the scope “read write” doesn’t change the token. The new values only take effect when a new token is created, so the existing one must be expired or a coexisting token has to be created with the “extra” field (see “Multple tokens per user”).

Multiple tokens per user

Note: Currently only possible via REST API, which will soon be implemented into the SDKs.
When using OAuth2 for authentication as opposed to 3rd party application access to user data, usually one token per user is enough as a replacement for username + password. But in some cases you might want to have multiple tokens per user, for example if you’re developing an app for multiple mobile operating systems and you want to have different tokens for each system.

For the seperation of the tokens there’s a field you can set when sending a token request: “extra”.
This field acts as a kind of key. For example you can only have one token for “extra=android_token” and only one for “extra=ios_token”, but they both can coexist. If you send another token request with the same value, the behavior is the same as if you hadn't used use the value at all: If there’s a valid token, it gets returned. If there hasn’t been a token yet or an existing one expired, a new one is created and returned.

There are two cases where the “extra” field is set by the system:

  • When manually setting a token, the “extra” field is pre-set with “manuallySet”

  • When a user signs up / logs in via Facebook and a token is created for the user, the “extra” field will be pre-set with “facebook”

Example:

Bash
curl -X POST "https://apiomat.org/yambas/oauth/token" --data "grant_type=aom_user&client_id=YOUR_APP&client_secret=YOUR_API_KEY&scope=read_only&username=USERNAME&app=YOUR_APP&password=PASSWORD&system=LIVE"

Refreshing a token

There’s always the possibility to request a new token with the user’s original username + password credentials.

When doing so

  • You get the existing tokens while the old one is still valid.

  • You will get a new token map once the old token has expired.

To minimize the use of the user original credentials, you can use the refresh token that’s included in the token map. Again, you can do this while the access token is valid or after it expired. But the result is different!

When refreshing a token with the refresh token

  • while the token is valid: The access token as well as the refresh token will be invalidated / deleted and you get a token map with new tokens

  • after the token has expired: You get a new token map

To find out if a token is expired, you can either keep track of the expiration date that’s included in the token map, or just wait until requests that use the access token for authentication fail.

Code examples

Android
boolean configureDS = true; // automatically configure the datastore with the received session token
// Assuming you saved the refresh token before and it's now in the variable refreshToken
Map<String, String> tokenMap = user.requestSessionToken(refreshToken, configureDS);
Objective-C
BOOL configureDS = TRUE; // automatically configure the datastore with the received session token
// Assuming you saved the refresh token before and it's now in the variable refreshToken
user requestSessionTokenAsyncWithRrefreshToken:refreshToken configure:configureDS Block:^(AOMTokenContainer *sessionData, NSError *error) {
NSLog(@"New sessionToken:%@", [sessionData sessionToken]);
}];
Swift
let configureDS = true // automatically configure the datastore with the received session token
// Assuming you saved the refresh token before and it's now in the variable refreshToken
user.requestSessionToken(configureDS, refreshToken: refreshToken) { (tokenContainer, error) in
print("New sessionToken: \(tokenContainer.sessionToken)")
}
JavaScript
var configureDS = true; // automatically configure the datastore with the received session token
// Assuming you saved the refresh token before and it's now in the variable refreshToken
user.requestSessionTokenWithRefreshToken(refreshToken, configureDS, {
onOk : function(result) {
tokenMap = result;
console.info(tokenMap);
},
onError: function(error) {
console.error(error);
}
});
C#
bool configureDS = true; // automatically configure the datastore with the received session token
// Assuming you saved the refresh token before and it's now in the variable refreshToken
IDictionary<string, string> = await user.RequestSessionTokenAsync(configureDS, refreshToken);
Bash
APPNAME=testApp
API_KEY=1234567890123456789
SYSTEM=LIVE
 
# Assuming the refresh token was fetched before
 
REFRESH_TOKEN=f14997c7-60c8-4z81-8892-36ez2ae3z0a1
 
# don't change anything below this comment
 
HOST="https://apiomat.org"
CLIENT_ID=$APPNAME
CLIENT_SECRET=$API_KEY
 
URL="$HOST/yambas/oauth/token"
PARAMS="--data grant_type=refresh_token&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&refresh_token=$REFRESH_TOKEN"
 
# refresh the access token with the refresh token
echo curl -X POST $URL $PARAMS
curl -X POST $URL $PARAMS
TypeScript
const configureDS = true; // automatically configure the datastore with the received session token
await user.requestSessionToken(configureDS, refreshToken);

Manually setting a token

Note: Currently only possible via REST API, will be implemented into the SDKs soon.

In addition to the standard OAuth2 functionality, where there’s only one token per user and token values are being generated automatically for ensuring their uniqueness, we added the possibility to manually set a token to a user, which can co-exist to the generated ones. Therefore we needed to add the possibility for users to possess multiple tokens (see section “Multiple tokens per user”).

In contrast to a normal token request, where the user credentials get exchanged against a token, manually set tokens can be set by the app admin or by users who have write permission to the user (sub-)class. So if user1 has the necessary permissions, he can set a token to user2 and then use that token to access that user data.

Manually set tokens are always read-only and their expiry is the same as normal tokens (Enterprise customers can customize this and set separate standard expiry times in the configuration).

If you want a group of users to be able to update other users, you should use roles.

To manually set a token, you send a PUT-Request to the user. As noted above, the request needs to be authorized.

Bash
curl -X PUT "https://apiomat.org/yambas/rest/apps/YOUR_APP/models/YOUR_MODULE/YOUR_USER_CLASS/USER_ID" -d "{\"@type\":\"YOUR_MODULE\$YOUR_USER_CLASS\", \"sessionToken\":\"YOUR_MANUAL_TOKEN\"}" -u APP_ADMIN_MAIL:APP_ADMIN_PASSWORD -H "Content-Type: application/json" -H "X-apiomat-apikey:YOUR_API_KEY"

Enterprise customers can manually set a token in a native module like this:

user.setSessionToken("manueller_token_readonly");
user.save();

As you would expect from a PUT request to the user, the manually set token becomes assigned to the user object. But the attribute behaves like the password attribute – when requesting a user object, the field will be empty. This way only the person who set the token knows about it and can use it.

Revoking tokens

In addition to just refreshing a token with a refresh token while the access token is valid, which invalidates the existing access token and refresh token and generates new ones, you can also explicitly revoke tokens. That way, no new tokens are being created.

Token revoke methods will be included in the SDKs in the next release of ApiOmat. Until then, you can do it via the REST API:

If you keep to the OAuth2 standard and have only one token per user, you can simply revoke the specific token:

Android
final HttpClient httpclient = new DefaultHttpClient( );
final String url = HOST + "/yambas/oauth/users/revoke";
final HttpGet httpget = new HttpGet( new URI( url ) );
httpget.setHeader( "Authorization", "Bearer " + accessToken );
httpget.setHeader( "X-Apiomat-Apikey", this.appkey );
final ResponseHandler<String> responseHandler = new BasicResponseHandler( );
httpclient.execute( httpget, responseHandler );
Bash
ACCESS_TOKEN=4bcb3064-5c64-43f0-9dbd-3e00a9171510
API_KEY=1234567890123456789
 
# don't change anything below this comment
 
HOST="https://apiomat.org"
URL="$HOST/yambas/oauth/users/revoke"
 
echo curl -X POST $URL -H "Authorization:Bearer $ACCESS_TOKEN" -H "X-Apiomat-Apikey:$API_KEY"
curl -X POST $URL -H "Authorization:Bearer $ACCESS_TOKEN" -H "X-Apiomat-Apikey:$API_KEY"

The above request also works with read_only tokens.

If you have multiple tokens per user, you can revoke all of them at once, but only if the token you use as authentication is NOT a read_only token (otherwise you get an HTTP 403 Forbidden error). Use the /revokeall instead of the revoke endpoint:

Android
final HttpClient httpclient = new DefaultHttpClient( );
final String url = HOST + "/yambas/oauth/users/revokeall";
final HttpGet httpget = new HttpGet( new URI( url ) );
httpget.setHeader( "Authorization", "Bearer " + accessToken );
httpget.setHeader( "X-Apiomat-Apikey", this.appkey );
final ResponseHandler<String> responseHandler = new BasicResponseHandler( );
httpclient.execute( httpget, responseHandler );
Bash
ACCESS_TOKEN=4bcb3064-5c64-43f0-9dbd-3e00a9171510
API_KEY=1234567890123456789
 
# don't change anything below this comment
 
HOST="https://apiomat.org"
URL="$HOST/yambas/oauth/users/revokeall"
 
echo curl -X POST $URL -H "Authorization:Bearer $ACCESS_TOKEN" -H "X-Apiomat-Apikey:$API_KEY"
curl -X POST $URL -H "Authorization:Bearer $ACCESS_TOKEN" -H "X-Apiomat-Apikey:$API_KEY"

Token Cleaner

After adding the functionality to have multiple tokens per user, a mechanism to delete old expired tokens is needed. This can be done with the token cleaner, which is currently set up to run every 24 hours at around 4 a.m. The token cleaner deletes all token pairs where both the access and refresh tokens are expired.