Upgrade — Link handling by rel and type#

A HAAPI representation’s links are not interchangeable. The Curity Identity Server states each link’s purpose through its rel and type, and a client is expected to honour that: follow some links as authenticated HAAPI requests, render others in place, and hand the rest to the platform.

Earlier guidance told integrators to follow every link with followLink. That is wrong for a link the server never meant to be followed — an inline QR image, a download link to an app store, or an authorization-response on a custom scheme — and it sends an authenticated request where a browser navigation was intended.

Two things changed:

  1. The SDKs classify a link from its rel and type, so you can branch on the result instead of guessing.
  2. followLink refuses a link whose target is not the configured identity server, and sends nothing.

What changed#

BeforeAfter
Every link handed to followLinkClassify first, then follow / render / open
image/png was the only media type rendered inlineAny image/* renders inline
Only a download rel opened in the browserdownload, a non-HAAPI declared type, and non-http(s) schemes all open in the browser
A link’s target host was never checkedfollowLink refuses an off-host or wrong-scheme target with non_haapi_link and transmits nothing

A link with no type is a HAAPI resource and is followed, per the HAAPI data-model contract. A missing type is not a signal to compare the link’s origin against your base URL.

Affected surfaces#

  • SDK Layer, iOS — new LinkKind and Link.kind; followLink may now return HaapiErrorCode.nonHaapiLink.
  • SDK Layer, Android — LinkKind and Link.kind(baseUri).
  • SDK Layer, React Native — no built-in classifier yet; apply the rules in your own code.
  • UI Layer, iOS — handled for you. Built-in view controllers route on the classification, and a custom HaapiUIViewController inherits it through follow(linkItemModel:) with no code change.

Migration#

Replace an unconditional follow:

// Before
let result = await haapiManager.followLink(link)

with a branch on link.kind:

// After
switch link.kind {
case .haapi:
    let result = await haapiManager.followLink(link)
case .inlineImage:
    render(link.href)
case .external:
    guard let url = URL(string: link.href) else { return }
    UIApplication.shared.open(url)
}

If you branched on the media type yourself, drop the string comparison — item.type == "image/png" becomes item.kind == .inlineImage, which also covers image/jpeg, image/svg+xml and the rest.

Using IdsvrHaapiUIKit? Nothing to do. If you implement LinkItemModel yourself, kind arrives as a protocol extension — you do not implement it, and it reads the underlying link, so populate that faithfully rather than leaving it a placeholder. The UI acts on that same link, so a placeholder means the SDK classifies one URL and opens another.

One behavioural change if you implement preFollow: it is now consulted for every link tap, including those that open in the platform browser. A download link previously bypassed it. Returning false now blocks those too — which is what makes preFollow a complete interception point for links leaving your app.

when (link.kind(baseUri)) {
    LinkKind.HAAPI -> haapiManager.followLink(link)
    LinkKind.INLINE_IMAGE -> render(link.href)
    LinkKind.EXTERNAL -> startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.href)))
}

baseUri is your configured identity-server base URL; it resolves a relative href before the scheme is examined.

No SDK API ships yet. Apply the same rules in your app — see React Native Representations for a worked example.

Handling the refusal#

followLink returns non_haapi_link when the resolved target’s host or scheme is not the configured identity server. No request is transmitted, so no access token or DPoP proof leaves the device.

In a correctly configured deployment this never fires: the identity server emits links on its own origin. If you see it, treat it as a server or reverse-proxy misconfiguration rather than something to retry — the result is deterministic. The message names no host, because it may reach an end user; the rejected and configured hosts go to the log. See Error Handling.

Check this before upgrading if your endpoints span hosts. The comparison is against the configured base URL, which is a separate value from the authorization endpoint URL and is not validated against it. If your deployment sets them to different hosts, the representations served by the authorization host will carry links the guard now refuses, and every followLink fails with non_haapi_link. Point the base URL at the host that actually serves your representations.

The port-mismatch warning#

If a link’s target differs from your configured base URL only in port, the request is still sent and the SDK logs a warning. The scheme’s default port is normalized first, so https://host and https://host:443 are the same origin and log nothing.

The warning means the identity server is emitting a different port than the client is configured with — typically a reverse proxy that rewrites the host but not the port. It is worth correcting on the server side, because the two values are supposed to agree.

followLink is guarded; submitForm is not. Extending the check to form submission is tracked separately, because a false positive there would break authentication outright.

Was this helpful?