Perform transactional authorization
The ForgeRock SDKs have builtin support for transactional authorization.
Transactional authorization improves security by requiring the user to perform additional actions when trying to access a resource protected by a policy in ForgeRock® Access Management (AM). For example, they must re-authenticate to a strong authentication tree, or respond to a push notification.
How does transactional authorization work?
The following diagram shows the flow used during transactional authorization:
When the ForgeRock SDKs attempt to access a resource protected with transactional authorization,
AM returns JSON that has an empty actions
attribute.
A unique transaction ID (TxId) is also included under /advices/TransactionConditionAdvice
.
For example:
{
"resource": "https://app-backend.example.com:8000/protected/feature/",
"actions": {},
"attributes": {},
"advices": {
"TransactionConditionAdvice": [
"7b8bfd4c-60fe-4271-928d-d09b94496f84"
]
},
"ttl": 0
}
The ForgeRock SDKs detect that transactional authorization is required, and make a call to the /authenticate
endpoint
to begin to fulfil the requirements specified in the policy protecting the resource.
The call must include the TxId originally received from AM.
AM responds to the request with a series of required callbacks to fulfil the policy.
Each callback is handled by the SDK; for example, by rendering UI for the user to complete, or responding to a push notification.
When all the callbacks have been completed, the SDK attempts to access the protected resource again,
using the same session or OAuth 2.0 token as before.
The SDK adds the transaction ID into the policy evaluation as an environment
property:
{
"resources" : ["https://app-backend.example.com:8000/protected/feature/"],
"application" : "iPlanetAMWebAgentService",
"subject" : {
"ssoToken" : "AQIC5w....*AJTMQAA*"
},
"environment": {
"TxId": ["77b8bfd4c-60fe-4271-928d-d09b94496f84"]
}
}
As the transaction ID matches an entry in AM’s completed transaction list, AM returns a new policy evaluation result, including the actions the SDK-based application can now perform:
{
"resource": "https://app-backend.example.com:8000/protected/feature/",
"actions": {
"POST": true,
"GET": true
},
"attributes": {},
"advices": {},
"ttl": 0
}
For more information on transactional authorization, and how to set up AM to use it, see Transactional authorization in the AM documentation.
In the ForgeRock Android SDK
In this example, the protected resource (http://openig.example.com/myResource
) is protected by ForgeRock® Identity Gateway (IG) and AM.
The response from IG contains the advice in the redirect URI.
The following example shows how to extract the advice using OKHttpClient
:
// Since IG responds with Advice and a redirect, ensure we do not follow them:
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.followRedirects(false);
// The session token will be injected with the cookie header:
builder.cookieJar(SecureCookieJar.builder()
.context(this.getApplicationContext())
.build());
OkHttpClient client = builder.build();
Request request = new Request.Builder()
.url("http://openig.example.com/myResource")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
// Handle failure scenario...
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.code() == StatusLine.HTTP_TEMP_REDIRECT) {
// Capture the redirect URI
String location = response.header("location");
Uri redirect = Uri.parse(location);
// Extract the advice from the captured URI
String advice = redirect.getQueryParameter("authIndexValue");
PolicyAdvice policyAdvice = PolicyAdvice.parse(advice);
// Perform the transactional authorization
FRSession.getCurrentSession().authenticate(MainActivity.this, policyAdvice, new NodeListener<FRSession>() {
@Override
public void onCallbackReceived(Node node) {
// Fulfil authentication requirements specified in the advice
}
@Override
public void onSuccess(FRSession result) {
// Handle success scenario...
}
@Override
public void onException(Exception e) {
// Handle failure scenario...
}
});
}
}
});
In the ForgeRock iOS SDK
The following steps demonstrate how to handle transactional authorization in the ForgeRock iOS SDK.
This example assumes interaction directly with AM. If the resource server is protected by IG, and routes are configured for protected resources, the optional steps are not required, as the SDK is able to deal directly with the responses from IG.
-
Create an
AuthorizationPolicy
with an array of one or more URLs to evaluate policies against, and a delegate ofAuthorizationPolicyDelegate
:let authPolicy = AuthorizationPolicy(validatingURL: [<URL>, <URL>...], delegate: self)
-
Add
AuthorizationPolicy
toFRURLProtocol
FRURLProtocol.authorizationPolicy = authPolicy
-
Register the
FRURLProtocol
class:URLProtocol.registerClass(FRURLProtocol.self)
-
Create a
URLSessionConfiguration
withFRURLProtocol
, and create aURLSession
with the configuration:// Configure FRURLProtocol for HTTP client let config = URLSessionConfiguration.default config.protocolClasses = [FRURLProtocol.self] self.urlSession = URLSession(configuration: config)
-
(Optional) If the SDK fails to parse the response into
policyAdvice
, construct it with the given response by implementing theAuthorizationPolicyDelegate.evaluateAuthorizationPolicy()
method:extension YourClass: AuthorizationPolicyDelegate { func evaluateAuthorizationPolicy(responseData: Data?, response: URLResponse?, error: Error?) -> PolicyAdvice? { // With given response, construct PolicyAdvice with following methods: // PolicyAdvice(redirectUrl:) - create PolicyAdvice with given redirectUrl // PolicyAdvice(json:) - create PolicyAdvice with given JSON object // PolicyAdvice(type:value:) - create PolicyAdvice with given authZ type, and value if let policyAdvice = PolicyAdvice() { // If PolicyAdvice is constructed, return 'policyAdvice' to continue authZ return policyAdvice } else { // If PolicyAdvice cannot be constructed, return 'nil' to stop authZ return nil } } }
-
Initiate authentication tree flow, including
policyAdvice
, by implementing theAuthorizationPolicyDelegate.onPolicyAdviseReceived()
method:extension YourClass: AuthorizationPolicyDelegate { func onPolicyAdviseReceived(policyAdvice: PolicyAdvice, completion: @escaping FRCompletionResultCallback) { FRSession.authenticate(policyAdvice: policyAdvice) { (token: Token?, node, error) in if error != nil { //Authentication failed completion(false) return } if let _ = token { completion(true) } else { //handle node. At the end of the authentication, you should get back a Token. In this case you will need to call the completion handler } } } }
-
(Optional) Decorate the original
URLRequest
object with updated information, by implementing theAuthorizationPolicyDelegate.updateRequest()
method.If a delegation method is not defined, the SDK appends the
_txid
query parameter automatically to the URL:extension YourClass: AuthorizationPolicyDelegate { func updateRequest(originalRequest: URLRequest, txId: String?) -> URLRequest { // append txId into the request return request } }
In the ForgeRock JavaScript SDK
Transactional authorization is built into the HttpClient
module of the ForgeRock JavaScript SDK.
This module detects when a transactional authorization is needed, and initiates interaction with AM.
When the callbacks are returned from AM, your client app must provide the necessary user interaction.
This callback handling iterates until a success or failure is reached. On success, the SDK can re-request the initial resource endpoint.
The following code shows an example implementation:
console.log('Make a $200 withdrawal from account');
return forgerock.HttpClient.request({
init: {
method: 'POST',
body: JSON.stringify({ amount: '200' }),
},
authorization: {
handleStep: async (step) => {
console.log('Withdraw action requires additional authorization');
step.getCallbackOfType('ValidatedCreateUsernameCallback').setName(un);
step.getCallbackOfType('ValidatedCreatePasswordCallback').setPassword(pw);
return Promise.resolve(step);
},
},
timeout: 0,
url: `${resourceUrl}/withdraw`,
});