Integrating IDaaS OIDC with a mobile app using AppAuth
Introduction
This document describes how to add OIDC authentication to a mobile application using IDaaS and the open-source project AppAuth.
Installation
- Android
- iOS
AppAuth dependencies are available on MavenCentral.
- Gradlew
- Maven
implementation 'net.openid:appauth:<version>'
<dependency>
<groupId>net.openid</groupId>
<artifactId>appauth</artifactId>
<version>0.11.1</version>
</dependency>
With CocoaPods, add this to your podfile
:
implementation 'net.openid:appauth:<version>'
With Swift Package Manager, add this to your Package.swift
:
dependencies: [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.3.0"))
]
Getting started - Endpoints configuration
- Android
- iOS
The configuration can be fetched from discovery url, You can also create the authorization service config by explicitly adding the authorize, token, and endsession endpoints, below is an example using discovery url.
import net.openid.appauth.AuthorizationServiceConfiguration;
private AuthorizationServiceConfiguration config = null;
AuthorizationServiceConfiguration.fetchFromUrl(
Uri.parse("https://yourIdaasAccount.com/api/oidc/.well-known/openid-configuration"),
this::handleConfigurationCallBack, // fetchFromUrl will return all necessary configurations (clientId, endpoints)
// as AuthorizationServiceConfiguration for the callback to persist.
DefaultConnectionBuilder.INSTANCE // your http connection builder
});
private void handleConfigurationCallBack(
AuthorizationServiceConfiguration config) {
this.config = config;
}
You can also explicitly define the configuration in a JSON file auth_config.json
.
{
"client_id": "932c9c22-16d3-46a3-9b6f-9b019003c444",
"redirect_uri": "net.openid.appauthdemo:/oauth2redirect",
"end_session_redirect_uri": "net.openid.appauthdemo:/oauth2redirect",
"authorization_scope": "openid email profile",
"discovery_uri": "",
"authorization_endpoint_uri": "",
"token_endpoint_uri": "",
"registration_endpoint_uri": "",
"user_info_endpoint_uri": "",
"https_required": true
}
For better clarification, the following documentation defines the endpoints in each section.
The configuration can be fetched from discovery url, You can also create the authorization service config by explicitly adding the authorize, token, and endsession endpoints, Below is an example using the discovery URL.
let issuer = URL(string: "https://yourIdaasAccount.com/api/oidc")!
// Discover configuration endpoints
OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
guard let config = configuration else {
print("Error retrieving discovery document: \(error?.localizedDescription ?? "Unknown error")")
return
}
// Use the config for further authorization requests
print("Configuration retrieved successfully: \(config)")
// Perform the auth request or other actions using the configuration...
}
For better clarification, the following documentation defines the endpoints in each section.
Logging in
- Android
- iOS
Step 1
Constructing Authorization Request by using its Builder, In Android AppAuth, PKCE is implemented by default if the server supports it (IOS AppAuth has to be manually passed in the parameters). You don’t need to worry about manually managing the codeVerifier
, codeChallenge
, or codeChallengeMethod
import net.openid.appauth.AuthorizationRequest;
AuthorizationServiceConfiguration serviceConfig = new AuthorizationServiceConfiguration(
Uri.parse("https://your-authorization-server.com/auth"), // authorization endpoint
Uri.parse("https://your-authorization-server.com/token") // token endpoint
);
private static final String CLIENT_ID = "155bc68f-f162-4a4b-bd44-fc4782cd3f0f";
private static final String REDIRECT_URI = "net.openid.appauthdemo:/oauth2redirect"; // this is a pointer back to your application
AuthorizationRequest authRequest = new AuthorizationRequest.Builder(
serviceConfig,
CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(REDIRECT_URI)
)
.setScopes("openid", "profile", "email") // Add the required scopes
.build();
Step 2
Sending Authorization Request by calling
getAuthorizationRequestIntent
to generate an intent (authIntent) that contains the authorization request details,
startActivityForResult
to go to IDaaS web page for authentication,
onActivityResult
to receive and process the authentication result.
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationException;
// If the request code matches 100, then process the result as an authorization response, which can be success or failure
private static final int RC_AUTH = 100;
private void doAuthorization() {
AuthorizationService authService = new AuthorizationService(this);
Intent authIntent = authService.getAuthorizationRequestIntent(authRequest);
startActivityForResult(authIntent, RC_AUTH);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RC_AUTH) {
AuthorizationResponse resp = AuthorizationResponse.fromIntent(data);
AuthorizationException ex = AuthorizationException.fromIntent(data);
// ... process the response or exception ...
} else {
// ...
}
}
Step 3
Exchanging authorization code for access token by calling the token request performTokenRequest()
.
We use AuthorizationService
to perform the token exchange using the authorization code obtained from the previous step AuthorizationResponse
. The AuthState object is used to store and manage tokens (e.g., access tokens, refresh tokens) and their lifecycle. We also use AuthStateManager
to manage and track AuthState at different stages of the authentication flow.
AuthorizationService authService = new AuthorizationService(this);
authService.performTokenRequest(
resp.createTokenExchangeRequest(), // `resp` is the AuthorizationResponse from the previous step, which contains token endpoint information
new AuthorizationService.TokenResponseCallback() {
@Override
public void onTokenRequestCompleted(TokenResponse response, AuthorizationException ex) {
if (response != null) {
// Token exchange succeeded, update the AuthState
authState.update(response, ex);
// Fetch the access token
String accessToken = authState.getAccessToken();
} else {
// Token exchange failed, handle the error
}
}
});
OIDExternalUserAgentSession
is a protocol in the AppAuth library that manages the session between the app and the external user agent (the browser), allowing the app to initiate OAuth and handle the final redirect response.
class AppDelegate: UIResponder, UIApplicationDelegate {
// property of the app's AppDelegate
var currentAuthorizationFlow: OIDExternalUserAgentSession?
}
When building authentication request in IOS, use PKCE (Proof Key for Code Exchange) to securely perform the authorization flow. The following shows how to construct an authorization request using PKCE, including a codeChallenge
and codeChallengeMethod
.
let clientID = "155bc68f-f162-4a4b-bd44-fc4782cd3f0f";
let redirectURI = "net.openid.appauthdemo:/oauth2redirect"; // this is a pointer back to your application
// Build PKCE parameters
let codeVerifier = OIDAuthorizationRequest.generateCodeVerifier()
let codeChallenge = OIDAuthorizationRequest.codeChallenge(forVerifier: codeVerifier)
let codeChallengeMethod = OIDOAuthorizationRequestCodeChallengeMethodS256
// Build authorization request
let request = OIDAuthorizationRequest(configuration: configuration,
clientId: clientID,
scopes: ["openid", "profile", "email"],
redirectURL: redirectURI,
responseType: OIDResponseTypeCode,
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod,
)
For iOS implementation, AppAuth provides a convenience method to do the token exchange : by using the OIDAuthState.authState()
, the token exchange can be performed in one step:
import AppAuth
import UIKit
//AppDelegate stores the current authorization flow session so it can be resumed when the user returns to the app after authentication.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//authorizing + exchange token
appDelegate.currentAuthorizationFlow =
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
let accessToken = authState.lastTokenResponse?.accessToken;
} else {
self.setAuthState(nil)
}
}
Logging out
- Android
- iOS
Build the EndSessionRequest
Start by constructing an EndSessionRequest
to log out. Since the id_token
is required by most OpenID providers to complete the logout, first check if it exists in AuthState
. If the id_token
is missing, skip the logout process.
import net.openid.appauth.AuthState;
import net.openid.appauth.EndSessionRequest;
import android.net.Uri;
// Retrieve the current AuthState
AuthState authState = stateManager.getCurrent();
// Ensure ID token exists before attempting logout
if (authState.getIdToken() != null) {
// Define a post-logout redirect URI specific to the mobile app, using a custom scheme or localhost
Uri postLogoutRedirectUri = Uri.parse("net.openid.appauthdemo:/oauth2redirect");
EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(
authState.getAuthorizationServiceConfiguration(), // serviceConfig included here, very similar with building AuthorizationRequest
authState.getIdToken() // Pass the ID token
)
.setPostLogoutRedirectUri(postLogoutRedirectUri) // Redirect URI back to the app
.build();
} else {
// Log or handle missing ID token: skip logout
}
Perform the end session request
Once the EndSessionRequest
is ready, use performEndSessionRequest()
on AuthorizationService
. This also requires a conditional check for the presence of the id_token
.
import net.openid.appauth.AuthorizationService;
import android.app.PendingIntent;
import android.content.Intent;
// Ensure ID token exists before performing the end session request
if (authState.getIdToken() != null) {
AuthorizationService authService = new AuthorizationService(this);
authService.performEndSessionRequest(
endSessionRequest,
PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCompleteActivity.class), PendingIntent.FLAG_UPDATE_CURRENT),
PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCanceledActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)
);
}
Build the OIDEndSessionRequest
.
let authState = stateManager.getCurrentAuthState()
let endSessionRedirectUri = URL(string: "your.app.scheme:/logout-callback")!
guard let configuration = authState.authorizationServiceConfiguration,
let idToken = authState.lastTokenResponse?.idToken else {
// Handle missing configuration or idToken
return
}
let endSessionRequest = OIDEndSessionRequest(
configuration: configuration,
idTokenHint: idToken,
postLogoutRedirectURL: endSessionRedirectUri,
additionalParameters: nil
)
Use OIDAuthorizationService.perform
to send the end session request and handle the completion or cancellation of the logout process.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
OIDAuthorizationService.perform(endSessionRequest) { endSessionResponse, error in
if let response = endSessionResponse {
// Transition to the completion view controller (logout complete)
let logoutCompleteVC = MyAuthCompleteViewController()
appDelegate.window?.rootViewController = logoutCompleteVC
} else if let error = error {
// Handle the cancellation or error case
let logoutCanceledVC = MyAuthCanceledViewController()
appDelegate.window?.rootViewController = logoutCanceledVC
}
}
In the code above,OIDEndSessionRequest
is built using the currentOIDAuthState
, which contains the authorization
service configuration and the idToken. The request is sent with OIDAuthorizationService.perform
, and you can handle
the transition to a "complete" or "cancelled" view based on the response.
Using access tokens
Using the working flow above, the access token you get is the OIDC Access Token, which can be used to access userInfo endpoint.
In the AuthorizationRequest
, if you pass in the audience value, you will get an OAuth Access Token, that can be used to access protected resource.
Using OIDC Access Tokens to access User Information
- Android
- iOS
performActionWithFreshTokens
can be used to call endpoints using access token by taking the AuthorizationService and an API call using access token as parameters.
AppAuth does not provide a specific wrapper for userInfo
requests, so you must create an HTTP connection manually.
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
import net.openid.appauth.AuthorizationService;
Uri userInfoEndpoint = Uri.parse("https://YOUR_DOMAIN.auth0.com/userinfo");
private AuthorizationService service = new AuthorizationService(this);
authState.performActionWithFreshTokens(service, (accessToken, endpointUri) -> {
// Manually set up the UserInfo request
HttpURLConnection conn = DefaultConnectionBuilder.INSTANCE.openConnection(userInfoEndpoint);
conn.setRequestProperty("Authorization", "Bearer " + accessToken);
conn.setInstanceFollowRedirects(false);
// Read the response from the user info endpoint
String response = Okio.buffer(Okio.source(conn.getInputStream()))
.readString(Charset.forName("UTF-8"));
// Handle the user info response here
});
OIDAuthState.performAction()
can be used to call endpoints using the availableaccess token.
AccessToken can be fetched by authState.lastTokenResponse.accessToken
.
let userinfoEndpoint = URL(string:"https://YOUR_DOMAIN.auth0.com/userinfo")!
self.authState?.performAction() { (accessToken, error) in
if error != nil {
return
}
let accessToken: String? = self.authState?.lastTokenResponse?.accessToken
// Add access token to request
var performActionWithFreshTokensRequest = URLRequest(url: userinfoEndpoint)
urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"]
// Perform request
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let data = data {
do {
// userInfo JSON response
let json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
//exception
}
}
}
}
Using OAuth access tokens to access protected resources
- Android
- iOS
Follow this page to configure API Authorization with resource audience in IDaaS.
In AppAuth, audience value is passed in as additionalParams
, set the audience value by setAdditionalParameters
, audience
value is the url you want to protect.
The OAuth Access token can be fetched from OIDAuthState.lastTokenResponse?.accessToken
.
If you also require a refresh token to be included in the response with the access token, include the offline_access scope.
// build the authorization request by passing in all mandatory attributes.
AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(
serviceConfiguration,
clientId,
ResponseTypeValues.CODE,
redirectUri
// Add the audience parameter.
Map<String, String> additionalParams = new HashMap<>();
additionalParams.put("audience", "https://protected.com");
AuthorizationRequest request = builder
.setAdditionalParameters(additionalParams) // Add audience here
.build();
When constructing the AuthorizationRequest
, you also need to ensure the offline_access
scope is included, as this typically requests the refresh token
.
AuthorizationRequest request = builder
.setScopes("openid", "profile", "email", "offline_access") // Add the scope for refresh token.
.build();
For sending the authorizationRequest and getting the token, please see the logging in section.
Follow this page to configure API Authorization with resource audience in your IDaaS.
In AppAuth for iOS, the audience value is passed in as additionalParameters
. You can set the audience value by using the additionalParameters
field of OIDAuthorizationRequest
, where audience
is the URL of the resource you want to protect.
The OAuth access token can be fetched from OIDAuthState.lastTokenResponse?.accessToken
.
If you also require a refresh token to be include in the response with the access token, include the offline_access scope.
// Create an OIDAuthorizationRequest with all required attributes
let configuration = OIDServiceConfiguration(
authorizationEndpoint: authorizationEndpoint,
tokenEndpoint: tokenEndpoint
)
// Add the audience parameter
let additionalParams = ["audience": "https://protected.com"]
let request = OIDAuthorizationRequest(
configuration: configuration,
clientId: clientId,
scopes: ["openid", "profile", "email", "offline_access"], // Add the scope for refresh token
redirectURL: redirectUri,
responseType: OIDResponseTypeCode,
additionalParameters: additionalParams // Add audience here
)
// perform the authorization request. Exchange the authorization code for the OAuth access token and refresh token
appDelegate.currentAuthorizationFlow =
OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
if let authState = authState {
self.setAuthState(authState)
// fetch the access token and refresh token by using authState.lastTokenResponse?.accessToken and authState.lastTokenResponse?.refreshToken
} else {
self.setAuthState(nil)
}
}