Token Endpoint Response Listener#
TokenResponse carries only the parsed accessToken, refreshToken, and error fields. When you need access to raw HTTP headers (for example, the dpop-nonce header for audit logging) or the unparsed response body, register a TokenEndpointResponseListener in the HaapiConfiguration. The listener fires on every call to OAuthTokenManager.fetchAccessToken, refreshAccessToken, and revokeRefreshToken.
When to Use a Listener#
Most apps do not need a TokenEndpointResponseListener. The parsed TokenResponse carries the access token, refresh token, expiry, and OAuth-protocol errors — enough for the authentication flow itself. Reach for the listener when:
- You need raw HTTP headers — most commonly
dpop-noncefor audit logging,x-request-idfor correlation, or a custom header that your deployment adds. - You need the unparsed JSON body — to forward it to a separate audit pipeline, or because your backend returns extension fields the SDK does not deserialize.
- You need transport-level metadata — content type, status code, server timing — that the parsed model does not expose.
- You want to react to all token-endpoint calls uniformly — fetch, refresh, and revoke — for example, to invalidate a cached resource that depends on the current access token, or to emit a metric per call.
If none of these apply, skip the listener and work with TokenResponse directly. The API is leaner, and you avoid the double-handling pitfall described below.
Implementing a Listener#
On React Native: the listener implementation lives in native code, not JavaScript. The React Native config carries only a flag (hasTokenEndpointResponseListener: true on iOS / tokenEndpointResponseListenerOption: TokenEndpointResponseListenerOption.Custom on Android) that activates a listener registered on the native side. There is no JS event emitter or callback — pure-JS RN apps cannot subscribe to token-endpoint responses. Use this only when you are willing to write Swift / Kotlin in the host app’s native modules. The listener registration is part of the same Native Registry mechanism described in Native Resolvers .
class MyTokenEndpointResponseListener: OAuthTokenManager.TokenEndpointResponseListener {
func onSuccess(_ value: OAuthTokenManager.SuccessTokenHTTPURLResponseContent) {
// value.httpURLResponse — raw HTTPURLResponse
// value.headerFields["dpop-nonce"] — DPoP nonce header
// value.dataAsDictionary["access_token"] — raw JSON access_token
}
func onError(_ value: ErrorHTTPURLResponseContent) {
// raw error response
}
func onTokenError(_ value: OAuthTokenManager.ErrorTokenHTTPURLResponseContent) {
// parsed ErrorTokenResponse with raw access too
}
}
let haapiConfiguration = HaapiConfiguration(
// ...other configuration
tokenEndpointResponseListener: MyTokenEndpointResponseListener()
) class MyTokenEndpointResponseListener : OAuthTokenManager.TokenEndpointResponseListener {
override fun onSuccess(value: OAuthTokenManager.SuccessTokenResponseContent) {
// value.successfulTokenResponse — parsed
// value.headerFields — raw header map
// value.responseAsJsonObject — raw JSON
}
override fun onTokenError(value: OAuthTokenManager.ErrorTokenResponseContent) {
// parsed error + raw access
}
override fun onError(value: HttpClient.Response.Failure) {
// transport or driver failure
}
}
val haapiConfiguration = HaapiConfiguration(
// ...other configuration
tokenEndpointResponseListener = MyTokenEndpointResponseListener()
) See the warning above the iOS tab — the listener implementation lives in native code on React Native.
JS-side configuration — enable the native listener via the platform config records:
import {
createIOSConfiguration,
createAndroidConfiguration,
TokenEndpointResponseListenerOption,
} from 'identityserver.haapi.reactnative.sdk'
const iosConfig = createIOSConfiguration({
clientId: 'haapi-ios-client',
appRedirect: 'app://haapi',
hasTokenEndpointResponseListener: true,
})
const androidConfig = createAndroidConfiguration({
clientId: 'haapi-android-client',
appRedirect: 'app://haapi',
tokenEndpointResponseListenerOption: TokenEndpointResponseListenerOption.Custom,
})iOS-side registration — register the listener instance against the native NativeRegistry before the accessor is initialised. From your host app’s Swift code (e.g., an Expo module or AppDelegate extension):
import IdsvrHaapiReactNativeSdk
NativeRegistry.shared.setTokenEndpointResponseListener(
MyTokenEndpointResponseListener()
)Implement MyTokenEndpointResponseListener exactly as in the iOS tab above.
Android-side registration — wire the listener through your native module’s Expo registry equivalent. Implement OAuthTokenManager.TokenEndpointResponseListener exactly as in the Android tab above and register it before initializeForHaapi is called from JS.
For projects without a native-code surface (pure-Expo apps that don’t eject), skip this feature.
Three callbacks fire on both platforms — successful token response, OAuth-protocol error response, and transport-level failure. Each callback receives the parsed model plus accessors for the raw HTTP response headers, content type, and unparsed body.
Avoiding Double-Handling#
When a listener is configured and an exception is raised, the framework delivers the failure to both the listener and the calling method. The same error reaches your code twice — once through the callback, once through the throw or completion handler — which can produce duplicate UX (two error dialogs) or duplicate metric emissions.
Avoid double-handling by treating the listener as raw-logging-only (audit, telemetry, header capture) and letting the caller handle the user-facing flow. Or, if the listener must take action on errors, mark handled errors via a shared flag the caller checks.
A defensive pattern — a thread-safe flag the listener flips on every callback, which the caller checks afterward:
final class HandledFlag {
private var handled = false
private let lock = NSLock()
func reset() {
lock.lock(); defer { lock.unlock() }
handled = false
}
func set() {
lock.lock(); defer { lock.unlock() }
handled = true
}
var isHandled: Bool {
lock.lock(); defer { lock.unlock() }
return handled
}
}
let listenerHandled = HandledFlag()
class HandlingListener: OAuthTokenManager.TokenEndpointResponseListener {
let flag: HandledFlag
init(flag: HandledFlag) { self.flag = flag }
func onSuccess(_ value: OAuthTokenManager.SuccessTokenHTTPURLResponseContent) {
// audit + handle
flag.set()
}
// onError / onTokenError set the flag the same way
}
// Before each request, reset:
listenerHandled.reset()
oAuthTokenManager.refreshAccessToken(with: refreshToken) { [weak self] tokenResponse in
if listenerHandled.isHandled {
// Listener already handled — avoid duplicate UX
} else {
// Caller-side handling
}
} val listenerHandled = java.util.concurrent.atomic.AtomicBoolean(false)
val listener = object : OAuthTokenManager.TokenEndpointResponseListener {
override fun onError(value: HttpClient.Response.Failure) {
// audit + handle
listenerHandled.set(true)
}
// onSuccess / onTokenError similarly
}
// Before each request, reset:
listenerHandled.set(false)
try {
val tokenResponse = oAuthTokenManager.refreshAccessToken(
refreshToken = refreshToken,
onCoroutineContext = coroutineContext
)
if (listenerHandled.get()) {
// Listener already handled — avoid duplicate UX
} else {
// Caller-side handling
}
} catch (err: Exception) {
if (listenerHandled.get()) {
// Listener already handled
} else {
// Caller-side handling
}
} Double-handling is not an issue at the JS layer on React Native because the native listener fires entirely outside JS — your await fetchAccessToken(...) call sees only the parsed TokenResponse (or a rejected HaapiError), regardless of whether a native listener observed the request.
If your native listener needs to communicate with JS — for example, to suppress a follow-up UX action — your host app’s native module must send an event over the bridge (Expo’s EventEmitter API or React Native’s RCTEventEmitter). That bridging logic is out of scope for this page; see the React Native and Expo Modules documentation for the event-emitter patterns. From the JS side, treat await fetchAccessToken(...) as the single source of truth for what the user sees.
How to implement this: OAuthTokenManager · Error Handling (SDK Layer) · Error Handling (concept)