ForgeRock Developer Experience

Set up transactional authorization

Applies to:

  • ForgeRock SDK for Android

  • ForgeRock SDK for iOS

  • ForgeRock SDK for JavaScript

The ForgeRock SDKs have builtin support for transactional authorization.

Transactional authorization requires a user to authorize every access to a resource. It is part of an AM policy that grants single-use or one-shot access.

For example, a user might approve a financial transaction with a one-time password (OTP) sent to their device, or respond to a push notification to confirm that they have indeed signed on from an unexpected location.

Performing the additional action successfully grants access to the protected resource but only once. Additional attempts to access the resource require the user to perform the configured actions again.

How does transactional authorization work?

The following diagram shows the flow used during transactional authorization:

transactional-how-it-works

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 value 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.

Handle transactions in an Android app

In this example, an API is protected by ForgeRock® Identity Gateway (IG) and AM.

The example adds a x-authenticate-response header to the access request. This header causes IG to return the advice as JSON in a header named Www-Authenticate.

The SDK provides interceptors for handling the returned advice:

After obtaining the advice, pass it to the authenticate() method of the FRSession class.

The following example shows how to use the interceptors to handle the advice using OKHttpClient:

val builder: OkHttpClient.Builder = OkHttpClient.Builder()

builder.addInterceptor(object: IdentityGatewayAdviceInterceptor() {
  override fun getAdviceHandler(advice: PolicyAdvice): AdviceHandler {
    return object: AdviceHandler {
      override suspend fun onAdviceReceived(
        context: Context,
        advice: PolicyAdvice) {
        // Authenticate the advice with
        // FRSession.getCurrentSession().authenticate(context, advice, ...)
      }
    }
  }
})

builder.cookieJar(SecureCookieJar.builder().context(context).build())

val client: OkHttpClient = builder.build()
val requestBuilder: Request.Builder = Request.Builder().url(api)

requestBuilder.addHeader("x-authenticate-response", "header");

val request = requestBuilder.build()

client.newCall(request).enqueue(object: Callback {
  override fun onFailure(call: Call, e: IOException) {
    // Handle Failure
  }

  override fun onResponse(call: Call, response: Response) {
    // Handle Response
  }
})

Handle transactions in an iOS app

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.

  1. Create an AuthorizationPolicy with the URL to evaluate policies against, and a delegate of AuthorizationPolicyDelegate:

    let authPolicy = AuthorizationPolicy(
      validatingURL: ["https://protectedendpoint"],
      delegate: self
    )
  2. Add AuthorizationPolicy to FRURLProtocol

    FRURLProtocol.authorizationPolicy = authPolicy
  3. Register the FRURLProtocol class:

    URLProtocol.registerClass(FRURLProtocol.self)
  4. Create a URLSessionConfiguration with FRURLProtocol, and create a URLSession with the configuration.

    (Optional) If using IG, add the x-authenticate-response header so that IG returns the advice response as JSON in a header named Www-Authenticate, rather than as a redirect with query parameters:

    // Configure FRURLProtocol for HTTP client
    let config = URLSessionConfiguration.default
    config.protocolClasses = [FRURLProtocol.self]
    
    var request = URLRequest(url: "https://protectedendpoint")
    request.setValue("header", forHTTPHeaderField: "x-authenticate-response")
    
    self.urlSession = URLSession(configuration: config)
    self.urlSession.dataTask(with: request) { (data, response, error) in }.resume()
  5. (Optional) If the SDK is unable to parse the response into policyAdvice, construct it with the given response by implementing the AuthorizationPolicyDelegate.evaluateAuthorizationPolicy() method:

    extension YourClass: AuthorizationPolicyDelegate {
      func evaluateAuthorizationPolicy(
        responseData: Data?,
        response: URLResponse?,
        error: Error?
      ) -> PolicyAdvice? {
        if let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 401,
          let json = httpResponse.allHeaderFields["Www-Authenticate"] as? String,
          let policyAdvice = PolicyAdviceCreator().parseAsBase64(advice: json)
        {
          return policyAdvice
        } else {
          // If PolicyAdvice cannot be constructed, return 'nil' to stop authZ
          return nil
        }
      }
    }
  6. Initiate authentication tree flow, including policyAdvice, by implementing the AuthorizationPolicyDelegate.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
          }
        }
      }
    }
  7. (Optional) Decorate the original URLRequest object with updated information, by implementing the AuthorizationPolicyDelegate.updateRequest() method:

    extension YourClass: AuthorizationPolicyDelegate {
      func updateRequest(originalRequest: URLRequest, txId: String?) -> URLRequest {
        // append txId into the request
        return request
      }
    }

    If a delegation method is not defined, the SDK appends the _txid query parameter automatically to the URL.

Handle transactions in a JavaScript app

Transactional authorization is built into the HttpClient module of the ForgeRock JavaScript SDK.

The HttpClient module detects when transactional authorization is enabled, and depending on your setup, initiates interaction with either AM or IG.

Ensure you specify credentials: 'include', so that the request includes the necessary cookies.

When transactional authorization is enabled and callbacks are returned, your client app must implement the necessary user interaction. Ensure that you iterate through returned callbacks until you receive a success or failure response.

When you do receive a success response, make a new request to the initial resource endpoint, which will now be authorized.

The following code shows a sample JavaScript implementation. The included authorization middleware handles the callbacks that the configured transactional authorization returns:

console.log('Make a $200 withdrawal from account');
return HttpClient.request({
    init: {
        method: 'POST',
        body: JSON.stringify({ amount: '200' }),
        credentials: 'include',
    },
    authorization: {
        handleStep: async (step) => {
            console.log('Withdrawal endpoint is set up for transational authorization...');
            step.getCallbackOfType('ValidatedCreateUsernameCallback').setName(un);
            step.getCallbackOfType('ValidatedCreatePasswordCallback').setPassword(pw);
            return Promise.resolve(step);
        },
    },
    timeout: 0,
    url: `${resourceUrl}/withdraw`,
});