Skip to content

Authentication

The Authentication module allows you to configure your mobile application to access SAP Mobile Services using the following authentication methods:

  • Basic
  • SAML
  • OAuth2
  • Certificate
  • API Key Only

If required, you can add One Time Password (OTP) to your authentication steps to provide two factor authentication for your app.

This module provides implementations of okhttp3.Interceptor to handle different types of authentication. Many of these interceptors must display UI to complete authentication. They therefore require access to the top activity of the application.

To make the activity available, the application must call android.app.Application.registerActivityLifecycleCallbacks with AppLifecycleCallbackHandler.

The best place to register the callback is from onCreate in an overridden android.app.Application. For example:

public class SampleApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(AppLifecycleCallbackHandler.getInstance());
    }
}
class SampleApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(AppLifecycleCallbackHandler.getInstance())
    }
}

Make sure to add the custom Application to the AndroidManifest.xml as follows:

<manifest . . . >
    <application android:name=".SampleApplication" . . . >
        . . .
    </application>
</manifest>

By default, an OkHttpClient will attempt to use the most recent TLS version starting with 1.3. However, it will fall back to older versions (back to 1.0) to be compatible with older servers.

Keep in mind that TLS 1.0 has known vulnerabilities and in general the newer versions are more secure. SAP recommends that you only use newer TLS versions.

For example, to only allow TLS 1.2 and 1.3, you could use the following code:

List<ConnectionSpec> connectionSpecs = new LinkedList<>();
ConnectionSpec strict = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
    .build();
connectionSpecs.add(strict);

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectionSpecs(connectionSpecs)
    <... add other things to the OkHttpClient.Builder as usual ...>
    .build();
val connectionSpecs = LinkedList<ConnectionSpec>()
val strict = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
    .build()
connectionSpecs.add(strict)

val okHttpClient = OkHttpClient.Builder()
    .connectionSpecs(connectionSpecs)
    <... add other things to the OkHttpClient.Builder as usual ...>
    .build()

Basic Authentication

See the Javadoc for BasicAuthDialogAuthenticator or for custom handling see Handling authentication.

One Time Passcode (OTP)

General Usage

The OtpInterceptor is capable of detecting OTP challenges in network responses. When a challenge is detected, authentication is done with an OtpProcessor. The default processor is OtpWebViewProcessor.

The OtpConfiguration class holds the configuration required by the interceptor and the processor. There are default values for the challenge header name (x-smp-authentication) and the challenge header value (otp-challenge).

When constructing OtpConfiguration.Builder, if the SettingsParameters object is set for the SettingsProvider class, the authorization endpoint URL and the finish endpoint URL will have default values based on the SettingsParameter object. Otherwise, the authorization endpoint URL and the finish endpoint URL must be set explicitly.

// Create the OtpConfiguration using the SettingsParameters object hold by SettingsProvider class
SettingsProvider.set(SettingsParameter)
OtpConfiguration otpConfig = new OtpConfiguration.Builder().build();

// Add an OtpInterceptor to the OkHttpClient to handle the OTP challenges.
OkHttpClient samlWithOtpHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new SamlInterceptor())
                .addInterceptor(new OtpInterceptor(otpConfig))
                .cookieJar(new WebkitCookieJar())
                .build();
// Create the OtpConfiguration using the SettingsParameters object hold by SettingsProvider class
SettingsProvider.set(SettingsParameter)
val otpConfig = OtpConfiguration.Builder().build()

// Add an OtpInterceptor to the OkHttpClient to handle the OTP challenges.
val samlWithOtpHttpClient = OkHttpClient.Builder()
                .addInterceptor(SamlInterceptor())
                .addInterceptor(OtpInterceptor(otpConfig))
                .cookieJar(WebkitCookieJar())
                .build()

Warning

The WebkitCookieJar is required to make the cookies from the OTP authentication flow available to subsequent network requests.

SAP Authenticator with OTP

SAP Authenticator is a mobile app that generates passcodes for systems that require OTP authentication.

Capturing the Passcode From SAP Authenticator

For SAP Authenticator to send your application the passcode, you must handle a specific scheme. If, for example, your app ID is com.example.myapp, then the scheme would be com.example.myapp.xcallbackurl.

Add this to your AndroidManifest.xml as follows:

<activity
    android:name="com.sap.cloud.mobile.foundation.authentication.OtpResponseActivity"
    tools:node="replace">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="com.example.myapp.xcallbackurl" />
        </intent-filter>
</activity>

Setting up SAP Authenticator

For information about setting up an account on SAP Authenticator, see One-Time Password Authentication User Guide.

When you add your application to SAP Authenticator, use the following format for the URL: com.example.myapp.xcallbackurl://x-callback-url/setCredential?username_paramvalue=[username]&passcode_paramvalue=[passcode]

replacing com.example.myapp with your app ID.

SAML 2.0

General Usage of SAML

The SamlInterceptor class is capable of detecting SAML challenges in the network request's response and starting the SAML authentication flow.

Example:

// Create the SAML configuration for the server
SamlConfiguration samlConfiguration = new SamlConfiguration.Builder()
    .authUrl("https://hcpms-xxxxxxxtrial.hanatrial.ondemand.com/SAMLAuthLauncher")
    .build();

// Create an OKHttp intercepter to listen for challenges
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(new SamlInterceptor(samlConfiguration))
    .cookieJar(new WebkitCookieJar())
    .build();

Warning

The WebkitCookieJar is required to make the cookies from the SAML web flow available to subsequent network requests.

Preauthentication with SAML

Generally, authentication occurs when a response is encountered with a SAML challenge. However, such a response could come at any point in the application’s lifecycle. You may find it undesirable to have a web view be displayed on top of the app at that point.

The SDK supports a way to implement preauthentication at application startup. This requires a direct call to the authenticate method of SAMLProcessor.

Example:

SamlProcessor samlProcessor = new SamlWebViewProcessor(samlConfiguration);

try {
    samlProcessor.authenticate();
} catch (IOException ex) {
    // If there was an error during the authentication process
    // or it was cancelled.
}
val samlProcessor = SamlWebViewProcessor(samlConfiguration)

try {
    samlProcessor.authenticate()
} catch (ex: IOException) {
    // If there was an error during the authentication process
    // or it was cancelled.
}

Certificate Handling with SAML

By default the web view will ignore certificate challenges. To handle certificate challenges with the web view, construct the SamlWebViewProcessor with a CertificateProvider, such as SystemCertificateProvider.

OAuth 2.0

General Usage of OAuth

The OAuth2Interceptor class is capable of detecting OAuth2 challenges in the network responses and starting the OAuth2 authentication flow.

The OAuth2Configuration class holds the configuration. Most of the required configuration items can be found from the configuration on the server. The redirectURI need not be specified on the builder. If not specified, it will default to a URI of the form <package name>://<host of authURL>.

The corresponding custom scheme (the package name) is automatically registered in the AndroidManifest.xml file included in the authentication aar file. For this default redirectURI to work, ensure the redirectURI on the server matches (you will likely have to modify the scheme on the server from HTTPS to the package name of your app). Any scheme other than HTTPS is considered to be a custom scheme (note that the client's default redirectURI has a custom scheme). A custom scheme on the redirectURI is required for OAuth with an external browser or Custom Tabs. If you cannot use a custom scheme, then you must set the redirectURI on the OAuth2Configuration.Builder and use the OAuth2WebViewProcessor. If you need to use a different custom scheme than the default you must add the custom scheme to the AndroidManifest.xml.

Example:

// Server side OAuth2 configuration
OAuth2Configuration oauthConfiguration = new OAuth2Configuration.Builder(activity.getApplicationContext())
    .clientId("<#Client ID hash#>")
    .responseType("code")
    .authUrl("<#URL String#>")
    .tokenUrl("<#URL String#>")
    .build();

// Storage for OAuth2 tokens
OAuth2TokenStore tokenStore = new OAuth2TokenInMemoryStore();

// Create an OKHttp intercepter to listen for challenges
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(new OAuth2Interceptor(new OAuth2BrowserProcessor(oauthConfiguration), tokenStore))
    .cookieJar(new WebkitCookieJar())
    .build();

// Set global OKHttpClient. The OAuth flow will use this for requesting the token.
ClientProvider.set(okHttpClient);
// Server side OAuth2 configuration
val oauthConfiguration = OAuth2Configuration.Builder(activity.applicationContext)
    .clientId("<#Client ID hash#>")
    .responseType("code")
    .authUrl("<#URL String#>")
    .tokenUrl("<#URL String#>")
    .build()

// Storage for OAuth2 tokens
val tokenStore = OAuth2TokenInMemoryStore()

// Create an OKHttp intercepter to listen for challenges
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(OAuth2Interceptor(OAuth2BrowserProcessor(oauthConfiguration), tokenStore))
    .cookieJar(WebkitCookieJar())
    .build()

// Set global OKHttpClient. The OAuth flow will use this for requesting the token.
ClientProvider.set(okHttpClient)

Note

The WebkitCookieJar is not required. In this example it is used for persisting the cookies. The JavaNetCookieJar or other CookieJar implementations can be used.

Warning

Be sure to call ClientProvider.set(okHttpClient); when using OAuth2 or provide a OkHttpClient to the OAuth2 processor constructor.

To use a non-default custom scheme on the redirect URI, add a tag like this to the AndroidManifest.xml inside the application tag:

<activity
        android:name="com.sap.cloud.mobile.foundation.authentication.OAuth2RedirectActivity"
        tools:node="replace">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="customScheme"/>
    </intent-filter>
</activity>

If an HTTPS redirect URI is required instead of a custom scheme, use the same approach:

<activity
        android:name="com.sap.cloud.mobile.foundation.authentication.OAuth2RedirectActivity"
        tools:node="replace">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="https"
              android:host="example.host.com"
              android:path="/oauth2_redirect"/>
    </intent-filter>
</activity>

Preauthentication with OAuth

Generally, authentication occurs when a response is encountered with a OAuth2 challenge. However, such a response could come at any point in the application’s lifecycle. You may find it undesirable to have a web view or browser be displayed on top of the app at that point.

The SDK supports a way to implement preauthentication at application startup. This requires a direct call to the authenticate method of OAuth2Processor.

Example:

OAuth2Processor oAuth2Processor = new OAuth2BrowserProcessor(oauth2Configuration);
try {
    OAuth2Token token = oAuth2Processor.authenticate();
} catch (IOException ex) {
    // If there was an error during the authentication process
    // or it was cancelled.
}
val oAuth2Processor = OAuth2BrowserProcessor(oauth2Configuration)
try {
    val token = oAuth2Processor.authenticate()
} catch (ex: IOException) {
    // If there was an error during the authentication process
    // or it was cancelled.
}

Certificate Handling with OAuth

If OAuth2BrowserProcessor is used, some browsers, including Chrome, ask the user to select a certificate from the system keychain when a client certificate challenge is encountered. When using a web view processor, it will by default ignore certificate challenges. To handle certificate challenges with the web view you must construct the OAuth2WebViewProcessor with a CertificateProvider such as SystemCertificateProvider.

Cross-Context SSO with OAuth

The cross-context SSO feature makes it easy for users to transfer OAuth tokens from desktops or device browsers to native apps. As a prerequisite, the cross-context SSO feature toggle under OAuth configuration should be turned on in SAP mobile service cockpit. Then the user can get the SSO onboarding URL and put it in their web application.

Users can leverage this feature in two ways, either by scanning a QR code or by using an app link:

  • Scanning a QR code is useful when a user opens the SSO onboarding URL on their desktop. The URL will display a QR code containing a short-duration passcode, allowing the native app to perform authentication. The user can open the native app, navigate to the QR code scanning screen, and scan the QR code before it expires. If successful, the native app will get onboarded automatically.

  • App links are appropriate when the user opens the SSO onboarding URL on a device. Note that this also requires that the app link feature be turned on in SAP mobile service cockpit. The URL will display a Next button to launch the native app and get it onboarded automatically.

The app link requires that customer apps configure it in the manifest file (this is not required for the QR code):

<activity
    android:name=".app.SomeActivity"
    android:theme="@style/AppTheme.NoActionBar"
    android:noHistory="true">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="https" android:host="someHost" />
    </intent-filter>
</activity>

Handle the intent data from the onboarding URL Next button in the Activity onCreate method:

Uri uri = getIntent().getData();
if (uri != null) {
    AppConfig appConfig = AppConfig.createAppConfigFromString(uri.toString(), null);
    FlowContext flowContext = new FlowContextBuilder()
            .setApplication(appConfig)
            .build();
    Flow.start(this, flowContext, (requestCode, resultCode, data) -> {
        if (resultCode == RESULT_CANCELED) {
            ...
        } else if (resultCode == RESULT_OK) {
            ...
        }
        return null;
    });
}
val uri = intent.data
if (uri != null) {
    val appConfig = AppConfig.createAppConfigFromString(uri.toString(), null)
    val flowContext = FlowContextBuilder()
            .setApplication(appConfig)
            .build()
    Flow.start(this, flowContext) { _, resultCode, _ ->
        if (resultCode == RESULT_CANCELED) {
            ...
        } else if (resultCode == RESULT_OK) {
            ...
        }
    }
}

SSL Client Certificate

It is possible to handle client certificate challenges by using the SslClientAuth class to create a custom SSLSocketFactory and X509TrustManager for the OKHttpClient.

This provides an implementation that uses system certificates for the challenges. If you require a different source for certificates, you can implement a custom CertificateProvider and provide this to the constructor.

Example:

SslClientAuth sslClientAuth = SslClientAuth.system();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .sslSocketFactory(sslClientAuth.getSslSocketFactory(), sslClientAuth.getTrustManager())
    .addInterceptor(new CertificateFailureInterceptor(sslClientAuth))
    .build();
val sslClientAuth = SslClientAuth.system()
val okHttpClient = OkHttpClient.Builder()
    .sslSocketFactory(sslClientAuth.sslSocketFactory, sslClientAuth.trustManager)
    .addInterceptor(CertificateFailureInterceptor(sslClientAuth))
    .build()

Tip

Use the CertificateFailureInterceptor to handle connection reset errors that can occur while the request is waiting for a certificate from the provider.

Rejected Certificates

If a certificate is rejected, the server returns an "HTTP 403 Forbidden status" error. The client code should clear the current certificate and reset the connection pool.

Example:

Response response = okHttpClient.newCall(request).execute();
if (response.code() == 403) {
    HttpUrl url = response.request().url();
    sslClientAuth.clear(url.host(), url.port());
    okHttpClient.connectionPool().evictAll();
}
val response = okHttpClient.newCall(request).execute()
if (response.code() == 403) {
    val url = response.request().url()
    sslClientAuth.clear(url.host(), url.port())
    okHttpClient.connectionPool().evictAll()
}

Certificate Provider

The CertificateProvider can be used to provide client certificates (X.509 certificate) to authenticate against the server. This interface is used with SSL client certificate authentication and web view-based authentication, such as SAML and OAuth2.

Included in the SDK is the SystemCertificateProvider for getting the certificate from the system. If you require another source for certificates, you can create a custom implementation of the CertificateProvider.

Clearing Client Certificates

Certificate-Based Authentication

Call the clear method on the SslClientAuth instance used.

WebView

Call the clear method on the CertificateProvider used with the WebView.

Note

The web view also remembers the certificate. To forget the certificate in the WebView you must also call WebView.clearClientCertPreferences(null);

Custom Tabs/External Browser

It is outside the application's control to clear/forget the certificate from an external browser. The user of the device will need to forget the browsing data in the browser and restart it.

API Key Only

The API key only authentication method is introduced in SAP BTP SDK for Android version 3.4 for applications on SAP Business Technology Platform, Cloud Foundry environment. To use this type of authentication to access 'SAP Mobile Services', the client code can do the following:

val s = ...
val appConfig = AppConfig.createAppConfigFromJsonString(s)
val parameter = SettingsParameters(
    appConfig.serviceUrl,
    appConfig.appID,
    Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID),
    appConfig.applicationVersion
).apply {
    SettingsProvider.set(this)
}

val httpClient = OkHttpClient.Builder()
    .addInterceptor(AppHeadersInterceptor(parameter))
    .addInterceptor(APIKeyInterceptor("apikey"))
    .cookieJar(WebkitCookieJar())
    .build()
ClientProvider.set(httpClient)
when (val result = UserService().retrieveUser()) {
    is ServiceResult.SUCCESS -> {
        val user = result.data as User
        assertTrue(user.id.contains("anonymous"))
    }
    is ServiceResult.FAILURE -> {
        //Failure
    }
}
AppConfig appConfig = AppConfig.createAppConfigFromJsonString("....");
SettingsParameters sp = new SettingsParameters(appConfig.getServiceUrl(),
        appConfig.getAppID(), "device id", appConfig.getApplicationVersion());
SettingsProvider.set(sp);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new AppHeadersInterceptor(sp))
        .addInterceptor(new APIKeyInterceptor("apikey"))
        .cookieJar(new WebkitCookieJar())
        .build();
ClientProvider.set(client);
new UserService().retrieveUser(new ServiceListener<User>() {
    @Override
    public void onServiceDone(@NotNull ServiceResult<User> serviceResult) {
        //user information in serviceResult
    }
});

If the client code is using the flows component to handle the onboarding process, it only needs to pass the API key into SDKInitializer and leave the preceding logic to the flows component. Subsequently, after the onboarding or restore flow, SettingsParameter and OKHttpClient will be ready and the services can be used directly.

//in onCreate of Application
SDKInitializer.start(
    this,
    apiKey = "apikey",
    services = services.toTypedArray()
)
SDKInitializer.INSTANCE.start(this, services.toArray(new MobileService[0]), "apikey");

If the client code is using the SAP BTP SDK for Android version 3.3 flows component in the callback function of onOkHttpClientReady of FlowStateListener, the interceptor needs to be added into OkHttpClient explicitly:

class MyFlowStateListener : FlowStateListener() {
    override fun onOkHttpClientReady(httpClient: OkHttpClient) {
        val interceptor = object : okhttp3.Interceptor {
            override fun intercept(chain: okhttp3.Interceptor.Chain): Response {
                val request: Request = chain.request()
                val newRequest: Request = request.newBuilder()
                    .header(
                        ClientProvider.HTTP_HEADER_X_API_KEY,
                        "apiKey"
                    )
                    .build()
                return chain.proceed(newRequest)
            }
        }
        httpClient.addUniqueInterceptor(interceptor)
    }
}
@Override
public void onOkHttpClientReady(@NotNull OkHttpClient httpClient) {
    //With the last parameter as 'true', 'client' will be saved back into ClientProvider
    //after adding the interceptor
    OkHttpClient client = SDKUtils.addUniqueInterceptor(httpClient, chain -> {
        Request request = chain.request();
        Request newRequest = request.newBuilder()
                .header(ClientProvider.HTTP_HEADER_X_API_KEY, "apikey")
                .build();
        return chain.proceed(newRequest);
    }, true);

    //set other properties
    OkHttpClient newClient = client.newBuilder()
            .connectTimeout(1000, TimeUnit.MICROSECONDS)
            .callTimeout(500, TimeUnit.MICROSECONDS)
            .build();

    //set it into ClientProvider, this is required if new properties are needed
    //and 'newClient' will be used for API calls after this point in other places.
    ClientProvider.set(newClient);
}

Logout

The SDK does not include an API to log out users. Instead, the application developer must create and send a simple HTTP request to the SAP Business Technology Platform mobile service back end. The logout API URL on the mobile service is: http[s]://<mobile services base URL>/mobileservices/session/logout

Warning

You must send the logout request with the same cookieJar instance you used for authentication. The logout API on the mobile service expects session cookies. Without them, the logout is ineffective.

Example:

Request request = new Request.Builder()
    .post(RequestBody.create(null, ""))
    .url("https://<#SAPcpms base URL#>/mobileservices/sessions/logout")
    .build();

okHttpClient.newCall(request).enqueue(responseCallback);
val request = Request.Builder()
    .post(RequestBody.create(null, ""))
    .url("https://<#SAPcpms base URL#>/mobileservices/sessions/logout")
    .build()

okHttpClient.newCall(request).enqueue(responseCallback)

See Logout Service for more information.

Network Requests From the Background

When the application is in the background, UI will not be shown to authenticate the user. A background application can be destroyed at any time, and any ongoing network requests will also be stopped. There are two options for reliably completing network requests while the application is in the background: foreground services and the Job Scheduler.

A network request running from a foreground service or a scheduled job while the application is not running in the foreground will not be able to show UI to authenticate the user. However, authentication can still succeed if it doesn't require UI - for example, if a valid OAuth token is available.

Foreground Service

A foreground service will only be stopped by the Android operating system under severe resource constraints. The foreground service is therefore a good choice if a network request must be done immediately, even if the application is in the background.

A foreground service must have some UI in the form of a status bar icon.

Job Scheduler

Using Job Scheduler is more suited to tasks that don't need to be done immediately, but should be executed when certain requirements are met, such as when an unmetered internet connection is available. Compared to a foreground service, there is less control over when a job will be executed, however no UI needs to be implemented to use the Job Scheduler.

Preventing Authentication UI

Applications might make requests that are not critical to the application flow and that are unrelated to what the user is doing in the app at that moment. In that case, if a request gets an authentication challenge requiring UI to resolve, you may prefer to have the request fail rather than interrupt the user with authentication UI.

This can be controlled by the app by implementing AuthenticationUiCallback and registering the callback on the AuthenticationUiCallbackManager class. By default, authentication UI is always allowed to be shown. Calling AuthenticationUiCallbackManager.setAuthenticationUICallback with null will restore the default behavior.

Example:

AuthenticationUiCallbackManager.setAuthenticationUiCallback(new AuthenticationUiCallback() {
    @Override
    public boolean allowShowingUiToAuthenticate() {
        if (isUserReadyForUI()) {
            // The user isn't doing anything that can't be interrupted.  Return true to
            // allow the authentication UI to be shown.
            return true;
        }
        return false;
    }
});
AuthenticationUiCallbackManager.setAuthenticationUiCallback {
    if (isUserReadyForUI()) {
        // The user isn't doing anything that can't be interrupted.  Return true to
        // allow the authentication UI to be shown.
        true
    } else false
}

Last update: June 28, 2021