visionOS Integration (Window)

Introduction

  • In this article we will learn how integrate the SDK to your visionOS app.

    • The window option will display the HUMAN challenge in a separate window,

    • the code integration will be added once, and cover all requests sent from anywhere in the app, background or foreground.

    • The challenge window does not block the window the user was interacting with, and can be moved to the side by the user,

    • however once blocked the calls will not go throw until the challenge is resolved.

    • We will cover the following topics:

      • How to start the SDK
      • Bot Defender integration
        • How to add the SDK's HTTP headers to your URL requests.
        • How to handle the blocked request.
        • How to present a challenge to the user in a new window.
        • How to set custom parameters (optional).
      • Account Defender integration
        • How to enable it.
        • How to notify HUMAN's backend on outgoing URL requests from the app.
        • How to set additional data (optional).
  • Note that in order to use challenge in a widnow you have to enable multiple windows in you app's info.plist.

Some features, including the Doctor App, Automatic Interceptor and Hybrid App support, are not available in the SDK for visionOS yet. We will add those features soon.

How to start the SDK

  • The most important thing is to start the SDK as soon as possible in your app flow. The reason for that is when your app will send an URL request to your server before the SDK was started, the request will not include the SDK's HTTP headers. As a result, HUMAN's Enforcer could block the request and the SDK will not be able to present a challenge to the user. The best place to start the SDK is in the App's init function on visionOS.
  • You should start the SDK on the main thread.
  • Note that some SDK features that exist in iOS are currently not available in visionOS.

Here is an example of how it should be:

import UIKit
import HUMAN

@main
struct MyApp: App {

    init() {
        startHumanSDK()
    }

    func startHumanSDK() {
        do {
            let policy = HSPolicy()

            try HumanSecurity.start(appId: "<APP_ID>", policy: policy)
        }
        catch {
            print("Error: \(error)")
        }
    }
}

Don't forget to change the <APP_ID> to your own AppID.

Let's talk about what we have in the code here:

  1. We start the SDK as soon as possible and on the main thread.
  2. We create a HSPolicy instance. This object is used to configure the SDK's behavior.
  3. We call the start function of the SDK. We provide the following parameters:
    • Your AppID.
    • The policy object that we configured.

Note: If your app communicates with several servers that have different AppID, you can call the HumanSecurity/start(appIds:policy:) function which allow you to pass an array of AppIDs. You should specify the relevant AppID for each API call in the SDK.

Bot Defender integration

How to add the SDK’s HTTP headers to your URL requests and handle the blocked request

  • The SDK provides HTTP headers that should be added to your app's URL requests. It is essential that those HTTP headers will be included on every URL request.
  • You should not cache those HTTP headers. They contain a token with expiration date. The SDK manages this token to be up-to-date.
  • The SDK handles the blocked request and present a challenge to the user.
  • You should note that the challenge window affects your application. You should remember to clean up the challenge window in case it is the last window left in the app. In the example below we added the onChange on scenePhase environment variable to check when a window is closed, in that case we will check if challenge window is the only other window and is showen and will close it, if you have a different place in the app where you clean up - you can move the code of closing the window in case it is open their.

Here is an example of how it should be:

import SwiftUI
import HUMAN

@main
struct MyApp: App {

    @Environment(\.scenePhase) var scenePhase
    @Environment(\.openWindow) private var openHumanChallengeWindow
    @Environment(\.dismissWindow) private var dismissHumanChallengeWindow

    init() {
        // start HUMAN...
    }

    var body: some Scene {
        // your main window and content.
        WindowGroup {
            ContentView()
        }

        // HUMAN's challenge window.
        WindowGroup(id: HSChallengeViewModel.name) {
            HSChallengeView()
        }
        .defaultSize(width: HSChallengeViewModel.width,
                     height: HSChallengeViewModel.height, depth: 0)
        .onChange(of: HSChallengeViewModel.shared.showChallenge, {
            if (HSChallengeViewModel.shared.showChallenge) {
                openHumanChallengeWindow(id: HSChallengeViewModel.name)
            }
            else {
                dismissHumanChallengeWindow(id: HSChallengeViewModel.name)
            }
        })
        .onChange(of: scenePhase) { oldPhase, newPhase in
             if (newPhase == .background && UIApplication.shared.connectedScenes.count == 2) {
                if (HSChallengeViewModel.shared.showChallenge) {
                    dismissHumanChallengeWindow(id: HSChallengeViewModel.name)
                }
            }
        }
    }
}
import HUMAN

func sendUrlRequest(url: URL) {
    var request = URLRequest(url: url)
    let myHttpHeaders = [String : String]()

    // config your request and HTTP headers...

    let humanHttpHeaders = HumanSecurity.BD.headersForURLRequest(forAppId: "<APP_ID>")
    request.allHTTPHeaderFields = myHttpHeaders.merging(humanHttpHeaders) { $1 }

    let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
        if let data = data, let response = response as? HTTPURLResponse {
            let isHandled = HumanSecurity.handleResponse(response: response, data: data) { result in
                print("Challenge result = \(result)")
                // You may retry the request here.
            }
            if isHandled {
                print("Blocked response was handled by the SDK")
            }
        }
    }
    dataTask.resume()
}

Let's talk about what we have in the code here:

  • View (App):

    1. We add the @Environment(.openWindow) and @Environment(.dismissWindow) properties and call them based on the HSChallengeViewModel/showChallenge observable in the SDK.
    2. We add the challenge window in the App's body.
    3. We add the @Environment(.scenePhase) and make sure to close the challenge in case it is the last open window in the app.
  • Model / HTTP client:

    1. We get the SDK's HTTP headers.
    2. We add those HTTP headers to our URL requests.
    3. We send the URL request.
    4. We send the response to the SDK. The SDK will check if it's a blocked request. If yes, it will present a challenge to the user.
    5. We print the challenge result (solved/cancelled) in the completion handler.
    6. We may retry the request here.

What should I do when a request is blocked

  • Handle it as a failure.

    Your app should handle the blocked request as a failure. However, you should consider that your app's UI is still shown while the challenge is presented to the user. If the request was triggered by a user's action, you should make it clear that the user may try again the same action.

  • Use the handler callback to write analyitics, logs, etc.

  • You may use the handler callback to retry the original request When appropriate. You should consider the following:

    1. The callback is called out of the original request's scope. Meaning, you will need to ensure your app handles this case correctly.
    2. The challenge could be canceled by the user. In this case, you should not retry the request because it will be blocked again.
    3. It's possible that the retry attempt will be blocked as well. DO NOT assume that it will go through.

Understanding the block response

  1. The HUMAN's Enfrocer, when it decides to block a request, returns a JSON string in the response's body. The HTTP status code is 403. Here is an example of the response:

    {
      "vid": "928d7ab3-9cf1-11ee-a624-b802520f369f",
      "uuid": "fd01e6d6-9cf2-11ee-808c-acde48001122",
      "page": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CiAgICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPgogICAgPHRpdGxlPkFjY2VzcyB0byB0aGlzIHBhZ2UgaGFzIGJlZW4gZGVuaWVkLjwvdGl0bGU+CiAgICA8bGluayBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2Nzcz9mYW1pbHk9T3BlbitTYW5zOjMwMCIgcmVsPSJzdHlsZXNoZWV0Ij4KICAgIDxzdHlsZT4KICAgICAgICBodG1sLCBib2R5IHsKICAgICAgICAgICAgbWFyZ2luOiAwOwogICAgICAgICAgICBwYWRkaW5nOiAwOwogICAgICAgICAgICBmb250LWZhbWlseTogJ09wZW4gU2FucycsIHNhbnMtc2VyaWY7CiAgICAgICAgICAgIGNvbG9yOiAjMDAwOwogICAgICAgIH0KCiAgICAgICAgYSB7CiAgICAgICAgICAgIGNvbG9yOiAjYzVjNWM1OwogICAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IG5vbmU7CiAgICAgICAgfQoKICAgICAgICAuY29udGFpbmVyIHsKICAgICAgICAgICAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICAgICAgICAgICAgZGlzcGxheTogZmxleDsKICAgICAgICAgICAgZmxleDogMTsKICAgICAgICAgICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogICAgICAgICAgICBmbGV4LWRpcmVjdGlvbjogY29sdW1uOwogICAgICAgICAgICBoZWlnaHQ6IDEwMCU7CiAgICAgICAgfQoKICAgICAgICAuY29udGFpbmVyID4gZGl2IHsKICAgICAgICAgICAgd2lkdGg6IDEwMCU7CiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7CiAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogY2VudGVyOwogICAgICAgIH0KCiAgICAgICAgLmNvbnRhaW5lciA+IGRpdiA+IGRpdiB7CiAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7CiAgICAgICAgICAgIHdpZHRoOiA4MCU7CiAgICAgICAgfQoKICAgICAgICAuY3VzdG9tZXItbG9nby13cmFwcGVyIHsKICAgICAgICAgICAgcGFkZGluZy10b3A6IDJyZW07CiAgICAgICAgICAgIGZsZXgtZ3JvdzogMDsKICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjsKICAgICAgICAgICAgdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgIH0KCiAgICAgICAgLmN1c3RvbWVyLWxvZ28gewogICAgICAgICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgIzAwMDsKICAgICAgICB9CgogICAgICAgIC5jdXN0b21lci1sb2dvID4gaW1nIHsKICAgICAgICAgICAgcGFkZGluZy1ib3R0b206IDFyZW07CiAgICAgICAgICAgIG1heC1oZWlnaHQ6IDUwcHg7CiAgICAgICAgICAgIG1heC13aWR0aDogMTAwJTsKICAgICAgICB9CgogICAgICAgIC5wYWdlLXRpdGxlLXdyYXBwZXIgewogICAgICAgICAgICBmbGV4LWdyb3c6IDI7CiAgICAgICAgfQoKICAgICAgICAucGFnZS10aXRsZSB7CiAgICAgICAgICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW4tcmV2ZXJzZTsKICAgICAgICB9CgogICAgICAgIC5jb250ZW50LXdyYXBwZXIgewogICAgICAgICAgICBmbGV4LWdyb3c6IDU7CiAgICAgICAgfQoKICAgICAgICAuY29udGVudCB7CiAgICAgICAgICAgIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47CiAgICAgICAgfQoKICAgICAgICAucGFnZS1mb290ZXItd3JhcHBlciB7CiAgICAgICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgICAgICAgICAgIGZsZXgtZ3JvdzogMC4yOwogICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogICAgICAgICAgICBjb2xvcjogI2M1YzVjNTsKICAgICAgICAgICAgZm9udC1zaXplOiA3MCU7CiAgICAgICAgfQoKICAgICAgICBAbWVkaWEgKG1pbi13aWR0aDogNzY4cHgpIHsKICAgICAgICAgICAgaHRtbCwgYm9keSB7CiAgICAgICAgICAgICAgICBoZWlnaHQ6IDEwMCU7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICA8L3N0eWxlPgogICAgPCEtLSBDdXN0b20gQ1NTIC0tPgo8L2hlYWQ+Cgo8Ym9keT4KPHNlY3Rpb24gY2xhc3M9ImNvbnRhaW5lciI+CiAgICA8ZGl2IGNsYXNzPSJjdXN0b21lci1sb2dvLXdyYXBwZXIiPgogICAgICAgIDxkaXYgY2xhc3M9ImN1c3RvbWVyLWxvZ28iPgogICAgICAgICAgICA8aW1nIHNyYz0iIiBhbHQ9IkxvZ28iLz4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0icGFnZS10aXRsZS13cmFwcGVyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJwYWdlLXRpdGxlIj4KICAgICAgICAgICAgPGgxPlBsZWFzZSB2ZXJpZnkgeW91IGFyZSBhIGh1bWFuPC9oMT4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY29udGVudC13cmFwcGVyIj4KICAgICAgICA8ZGl2IGNsYXNzPSJjb250ZW50Ij4KICAgICAgICAgICAgPGRpdiBpZD0icHgtY2FwdGNoYSI+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIEFjY2VzcyB0byB0aGlzIHBhZ2UgaGFzIGJlZW4gZGVuaWVkIGJlY2F1c2Ugd2UgYmVsaWV2ZSB5b3UgYXJlIHVzaW5nIGF1dG9tYXRpb24gdG9vbHMgdG8gYnJvd3NlIHRoZQogICAgICAgICAgICAgICAgd2Vic2l0ZS4KICAgICAgICAgICAgPC9wPgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIFRoaXMgbWF5IGhhcHBlbiBhcyBhIHJlc3VsdCBvZiB0aGUgZm9sbG93aW5nOgogICAgICAgICAgICA8L3A+CiAgICAgICAgICAgIDx1bD4KICAgICAgICAgICAgICAgIDxsaT4KICAgICAgICAgICAgICAgICAgICBKYXZhc2NyaXB0IGlzIGRpc2FibGVkIG9yIGJsb2NrZWQgYnkgYW4gZXh0ZW5zaW9uIChhZCBibG9ja2VycyBmb3IgZXhhbXBsZSkKICAgICAgICAgICAgICAgIDwvbGk+CiAgICAgICAgICAgICAgICA8bGk+CiAgICAgICAgICAgICAgICAgICAgWW91ciBicm93c2VyIGRvZXMgbm90IHN1cHBvcnQgY29va2llcwogICAgICAgICAgICAgICAgPC9saT4KICAgICAgICAgICAgPC91bD4KICAgICAgICAgICAgPHA+CiAgICAgICAgICAgICAgICBQbGVhc2UgbWFrZSBzdXJlIHRoYXQgSmF2YXNjcmlwdCBhbmQgY29va2llcyBhcmUgZW5hYmxlZCBvbiB5b3VyIGJyb3dzZXIgYW5kIHRoYXQgeW91IGFyZSBub3QgYmxvY2tpbmcKICAgICAgICAgICAgICAgIHRoZW0gZnJvbSBsb2FkaW5nLgogICAgICAgICAgICA8L3A+CiAgICAgICAgICAgIDxwPgogICAgICAgICAgICAgICAgUmVmZXJlbmNlIElEOiAjZmQwMWU2ZDYtOWNmMi0xMWVlLTgwOGMtYWNkZTQ4MDAxMTIyCiAgICAgICAgICAgIDwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0icGFnZS1mb290ZXItd3JhcHBlciI+CiAgICAgICAgPGRpdiBjbGFzcz0icGFnZS1mb290ZXIiPgogICAgICAgICAgICA8cD4KICAgICAgICAgICAgICAgIFBvd2VyZWQgYnkKICAgICAgICAgICAgICAgIDxhIGhyZWY9Imh0dHBzOi8vd3d3LnBlcmltZXRlcnguY29tL3doeXdhc2libG9ja2VkIj5QZXJpbWV0ZXJYPC9hPgogICAgICAgICAgICAgICAgLCBJbmMuCiAgICAgICAgICAgIDwvcD4KICAgICAgICA8L2Rpdj4KICAgIDwvZGl2Pgo8L3NlY3Rpb24+CjwhLS0gUHggLS0+CjxzY3JpcHQ+CiAgICB3aW5kb3cuX3B4QXBwSWQgPSAnUFhqOXk0UThFbSc7CiAgICB3aW5kb3cuX3B4SnNDbGllbnRTcmMgPSAnLy9jbGllbnQucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9tYWluLm1pbi5qcyc7CiAgICB3aW5kb3cuX3B4Rmlyc3RQYXJ0eUVuYWJsZWQgPSBmYWxzZTsKICAgIHdpbmRvdy5fcHhWaWQgPSAnOTI4ZDdhYjMtOWNmMS0xMWVlLWE2MjQtYjgwMjUyMGYzNjlmJzsKICAgIHdpbmRvdy5fcHhVdWlkID0gJ2ZkMDFlNmQ2LTljZjItMTFlZS04MDhjLWFjZGU0ODAwMTEyMic7CiAgICB3aW5kb3cuX3B4SG9zdFVybCA9ICcvL2NvbGxlY3Rvci1QWGo5eTRROEVtLnBlcmltZXRlcngubmV0JzsKPC9zY3JpcHQ+CjwhLS0gQ2FwdGNoYSAtLT4KPHNjcmlwdD4KICAgIHZhciBzID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0Jyk7CiAgICBzLnNyYyA9ICcvL2NhcHRjaGEucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9jYXB0Y2hhLmpzP2E9YyZtPTEmdT1mZDAxZTZkNi05Y2YyLTExZWUtODA4Yy1hY2RlNDgwMDExMjImdj05MjhkN2FiMy05Y2YxLTExZWUtYTYyNC1iODAyNTIwZjM2OWYnOwogICAgdmFyIHAgPSBkb2N1bWVudC5nZXRFbGVtZW50c0J5VGFnTmFtZSgnaGVhZCcpWzBdOwogICAgcC5pbnNlcnRCZWZvcmUocywgbnVsbCk7CiAgICBpZiAoZmFsc2UpIHsKICAgICAgICBzLm9uZXJyb3IgPSBmdW5jdGlvbiAoKSB7CiAgICAgICAgICAgIHMgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsKICAgICAgICAgICAgdmFyIHN1ZmZpeEluZGV4ID0gJy8vY2FwdGNoYS5wZXJpbWV0ZXJ4Lm5ldC9QWGo5eTRROEVtL2NhcHRjaGEuanM/YT1jJm09MSZ1PWZkMDFlNmQ2LTljZjItMTFlZS04MDhjLWFjZGU0ODAwMTEyMiZ2PTkyOGQ3YWIzLTljZjEtMTFlZS1hNjI0LWI4MDI1MjBmMzY5ZicuaW5kZXhPZignY2FwdGNoYS5qcycpOwogICAgICAgICAgICB2YXIgdGVtcGVyZWRCbG9ja1NjcmlwdCA9ICcvL2NhcHRjaGEucGVyaW1ldGVyeC5uZXQvUFhqOXk0UThFbS9jYXB0Y2hhLmpzP2E9YyZtPTEmdT1mZDAxZTZkNi05Y2YyLTExZWUtODA4Yy1hY2RlNDgwMDExMjImdj05MjhkN2FiMy05Y2YxLTExZWUtYTYyNC1iODAyNTIwZjM2OWYnLnN1YnN0cmluZyhzdWZmaXhJbmRleCk7CiAgICAgICAgICAgIHMuc3JjID0gJy8vY2FwdGNoYS5weC1jZG4ubmV0L1BYajl5NFE4RW0vJyArIHRlbXBlcmVkQmxvY2tTY3JpcHQ7CiAgICAgICAgICAgIHAucGFyZW50Tm9kZS5pbnNlcnRCZWZvcmUocywgcCk7CiAgICAgICAgfTsKICAgIH0KPC9zY3JpcHQ+CjwhLS0gQ3VzdG9tIFNjcmlwdCAtLT4KPC9ib2R5Pgo8L2h0bWw+Cg==",
      "appId": "PXj9y4Q8Em",
      "action": "captcha",
      "collectorUrl": "https://collector-pxj9y4q8em.perimeterx.net"
    }
    
  2. The JSON contains metadata for the SDK.

  3. Your app should pass the whole JSON to the SDK via the HSBotDefender/handleResponse(response:data:callback:) function. Otherwise, the SDK won't presnet a challenge to the user.

How to set custom parameters for Bot Defender (optional)

You can set custom parameters in order to configure HUMNAN's backend with additional parameters.

Those parameters can be set with a dictionary (iOS) or an hash map (Android), with the key "custom_param[x]" where [x] is a number between 1-10.

You should call the HSBotDefender/setCustomParameters(parameters:forAppId:) only after the HumanSecurity/start(appId:policy:) function was already called.

Here is an example of how it should be:

import HUMAN

func setCustomParametersForBotDefender() {
    do {
        var customParameters = [String: String]()
        customParameters["custom_param1"] = "hello"
        customParameters["custom_param2"] = "world"
        try HumanSecurity.BD.setCustomParameters(parameters: customParameters, forAppId: "<APP_ID>")
    }
    catch {
        print("Error: \(error)")
    }
}

Account Defender integration

How to enbale Account Defender in your app?

In order to enable Account Defedner, you should set the UserID of your current logged-in user in the SDK.

Here is an example of how it should be:

import HUMAN

func onUserLoggedIn(userID: String) {
    do {
        try HumanSecurity.AD.setUserId(userId: userID, forAppId: "<APP_ID>")
    }
    catch {
        print("Error: \(error)")
    }
}

How to notify HUMAN's backend on outgoing URL requests from the app

In order to allow Account Defender protect the user's account, your app has to provide the SDK with outgoing URL requests.

import HUMAN

func sendUrlRequest(url: URL) {
    do {
        try HumanSecurity.AD.registerOutgoingUrlRequest(url: url.absoluteString, forAppId: "<APP_ID>")
    }
    catch {
        print("Error: \(error)")
    }

    // Send the request...
}

Let's talk about what we have in the code here:

  1. We call the HSAccountDefender/registerOutgoingUrlRequest(url:forAppId:) function before we send the URL request.

How to set additional data (optional)

You can set additional data in order to configure HUMNAN's backend with additional parameters.

Those parameters can be set with a dictionary.

You should call the HSAccountDefender/setAdditionalData(parameters:forAppId:) only after the HumanSecurity/start(appId:policy:) function was already called.

import HUMAN

func setAdditionalDataForAccountDefender() {
    do {
        var additionalData = [String: String]()
        additionalData["my_key1"] = "hello"
        additionalData["my_key2"] = "world"
        try HumanSecurity.AD.setAdditionalData(parameters: additionalData, forAppId: "<APP_ID>")
    }
    catch {
        print("Error: \(error)")
    }
}