. . .

ApiOmat and OAuth2


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

What is OAuth2?

OAuth2 is an authorization mechanism which is used by many popular websites like Google and Facebook.

One use case is the possibility for websites to give 3rd party apps a limited access to user data, without having to give a password to those 3rd party apps. Instead, a token will be issued with a limited validity and might also have other limitations associated with it (e.g. only read access).

At ApiOmat we use it for user authentication and token management only and do not let 3rd party apps access the data of a user. We also added functionality like the possibility to have multiple valid tokens per user.

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 are very sensitive information. When the app is not secure, the device is rooted, or it’s a webapp written in JavaScript and only cookies get used as storage, the password may get into the wrong hands. With our OAuth2 implementation you can exchange the username and password against a set of so called tokens. The access token is being 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 is expired, he can’t do this anymore. He 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 doesn’t get access to the users password, which the user might use on other services and thus is more sensitive.

  • If malicious use is detected, the access and refresh tokens can get revoked before they expire. The user is not affected by this – he only has to re-enter his username + password, so the app can get a new token map. In case of the 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.configureWithCredentials(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 gets created. So when a token exists and gets requested again, only with different expiration values, nothing changes. Only if a token is expired or a token with new “extra” (see “Multiple tokens per user”) gets requested, 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, a token that already exists and is still valid and was created with the scope “read write”, another request with the scope “read_only” doesn’t change the token. The new values only take effect when a new token gets 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, will be implemented into the SDKs soon.
When using OAuth2 for authentication and not for 3rd party application access to user data, usually one token per user would be 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 behaviour is the same as if you don’t 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 gets created and returned.

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

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

  • When a user signs up / logs in via Facebook and a token gets created for the user, the “extra” field gets 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

  • while the token is valid: you get the existing tokens

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

To minimize the use of the user original credentials you 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 get invalidated / deleted and you get a token map with new tokens

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

For finding out if a token expired, you can either keep track of the expiration 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 to have multiple tokens per user (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 gets 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 know 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 was needed. This gets done by 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.