# Sample login reference Source: https://www.nianticspatial.com/docs/nsdk/auth_client/ The NSDK sample apps include a complete login flow that exchanges a Scaniverse login for an NSDK access token through Niantic's sample backend. This page explains how that flow works, from browser login through token exchange to NSDK authorization. **The sample login code is a reference implementation**, not an SDK you ship to users. When building a production application, replace the Niantic sample backend with your own backend and identity system. The NSDK only needs the final NSDK access token -- how you obtain it is up to you. Use this page if you are working from the NSDK sample login flow. For development and internal testing without the sample login flow, use [developer tokens](https://www.nianticspatial.com/docs/nsdk/auth_developer_token/). For production deployment, use [backend-issued short-lived access tokens](https://www.nianticspatial.com/docs/nsdk/auth_backend/). This page covers: - **[How the sample login works](#how-the-sample-login-works)**: The token exchange flow and file structure. - **[Callback scheme registration](#register-a-callback-scheme)**: How the sample app is configured to receive the authentication result after login. - **[The sample login flow](#the-sample-login-flow)**: How the sample app starts login, handles the callback, establishes a session, and exchanges session tokens for NSDK access tokens. - **[Token usage in the sample app](#use-tokens-in-your-app)**: How the sample app sets, checks, refreshes, and clears NSDK access tokens. NSDK access tokens are JSON Web Tokens (JWTs) used to authorize requests to Niantic Spatial services. NSDK access tokens expire after a set period. The sample app verifies the token's validity using the expiration timestamp and requests a new one when the token is missing or expired. In a production app, your backend handles token issuance. The client can still monitor token expiration and request a new token from your backend when needed, but tokens should not be refreshed directly with the Identity Service from the client. This page focuses on the sample app's client-side token usage. For help choosing an authorization path, see [Authorization](https://www.nianticspatial.com/docs/nsdk/auth_getting_started/). For development and internal testing, see [Generate developer tokens](https://www.nianticspatial.com/docs/nsdk/auth_developer_token/). For production backend token issuance, see [Generate access tokens](https://www.nianticspatial.com/docs/nsdk/auth_backend/). ## How the sample login works The sample apps log in through Niantic's sample backend, which handles the Scaniverse login and token exchange on behalf of the client. The flow has three stages: 1. **Browser login:** The user logs in with a Scaniverse account in a browser. The sample backend returns an NS sample session token to the app via a deep-link callback. 2. **Token exchange:** The app exchanges the NS sample session token for an NSDK access token through a series of backend calls. 3. **NSDK authorization:** The app passes the NSDK access token to the NSDK session. The NSDK uses this token to authorize API requests. Only the NSDK access token is passed to the NSDK. All intermediate sample-flow tokens, including NS sample session tokens and NSDK refresh tokens, are managed entirely in client code and never reach the native SDK. ### Token exchange detail The sample apps use the Niantic Spatial Identity Service to exchange intermediate sample-flow tokens for an NSDK access token. This is the same service your production backend would call using a service account. In the sample flow, the exchange works as follows: | Step | Request | Returns | |------|---------|---------| | 1 | `refresh_user_session_access_token` with NS sample session token | Rotated NS sample session token (Set-Cookie) | | 2 | `exchange_build_refresh_token` with NS sample session token | NSDK refresh token | | 3 | `refresh_build_access_token` with NSDK refresh token | NSDK access token | All requests are `POST` calls to the identity endpoint: `https://spatial-identity.nianticspatial.com/oauth/token` The sample apps run a background refresh loop that checks token expiration every 10 seconds and re-executes the exchange before the token expires. > **Info:** > > In a production app, your backend replaces steps 1-3. Your backend logs in the user with your own identity system, requests an NSDK access token using a service account (see [Generate access tokens](https://www.nianticspatial.com/docs/nsdk/auth_backend/)), and returns it to the client. The client then passes the NSDK access token to the NSDK using the same APIs shown in this guide. ### File structure Each sample app organizes its login flow into a small set of files: ### Platform: swift | File | Purpose | |------|---------| | `LoginManager.swift` | Opens browser login and handles the deep-link callback | | `NSSampleSessionManager.swift` | Manages the NS sample session token lifecycle, refresh loop, and NSDK access token exchange | | `AuthRequests.swift` | HTTP request helpers for the three token exchange calls | | `AuthUtils.swift` | JWT parsing, token validation, and expiration checks | | `AuthConstants.swift` | Endpoint URLs and callback scheme configuration | | `AuthRetryHelper.swift` | Retry logic for NSDK operations that require authorization | These files are located in the sample app's `Auth/` directory: ``` nsdk-samples-swift/NsdkSamples/NsdkSamples/Auth/ ``` ### Platform: kotlin | File | Purpose | |------|---------| | `LoginManager.kt` | Opens browser login via Chrome Custom Tabs and handles the deep-link callback | | `NSSampleSessionManager.kt` | Manages the NS sample session token lifecycle, refresh loop, and NSDK access token exchange | | `AuthRequests.kt` | HTTP request helpers for the three token exchange calls | | `AuthManager.kt` | Orchestrates login/logout and connects login state to the NSDK session | | `AuthUtils.kt` | JWT parsing, token validation, and expiration checks | | `AuthConstants.kt` | Endpoint URLs and callback scheme configuration | | `TokenStorage.kt` | Persistent token storage abstraction (SharedPreferences) | | `AuthRetryHelper.kt` | Retry logic for NSDK operations that require authorization | These files are located in the sample app's `auth` package: ``` nsdk-samples-kotlin/NsdkSamples/src/main/java/.../auth/ ``` ### Platform: unity | File | Purpose | |------|---------| | `LoginManager.cs` | Opens browser login and handles the deep-link callback | | `NSSampleSessionManager.cs` | Manages the NS sample session token lifecycle, refresh loop, and NSDK access token exchange | | `AuthRequests.cs` | HTTP request helpers for the three token exchange calls | | `AuthManager.cs` | UI manager for login/logout | | `AuthEndpoints.cs` | ScriptableObject configuration for endpoint URLs | | `AuthRetryHelper.cs` | Retry logic for NSDK operations that require authorization | These files are located in the sample project's Auth directory: ``` nsdk-samples-csharp/NsdkSamples/Assets/Samples/Auth/Scripts/ ``` ## Register a callback scheme After login, the authentication flow redirects to a URL using the app's callback scheme. The app must be configured to handle this redirect so it can receive the authentication result. The redirect URL uses the format: - `://signin/redirect?refreshToken=...` The registered callback scheme must match the scheme used by the authentication redirect URL. This is the URL that returns to the app after authentication. Here is how the sample app registers its callback scheme. ### Platform: unity In Unity, the URL scheme is configured to receive the login redirect for either iOS or Android as follows: **iOS:** The sample project configures its URL scheme through Unity's Project Settings: 1. **Edit -> Project Settings -> Player** -> **iOS** tab. 2. Under **Other Settings -> Supported URL schemes**, the sample adds an entry with its scheme. Unity stores the URL scheme in `ProjectSettings/ProjectSettings.asset`. When building for iOS, Unity generates an Xcode project and converts these settings into `Info.plist`. After updating the URL scheme, the iOS project must be rebuilt so Xcode sees the change. **Android:** The sample project registers its callback scheme in `Assets/Plugins/Android/AndroidManifest.xml` with an intent filter. The scheme value matches the one used in the login code. ```xml ``` The following example shows the minimum Android manifest content for this intent filter: ```xml ``` ### Platform: swift The sample app registers a URL scheme in Xcode so iOS can return the authentication result. The configuration is: 1. In Xcode's Project Navigator, the app target's **Info** tab has a **URL Types** section. 2. The sample adds a URL Type with an identifier (e.g. `com.myapp.auth`). 3. The **URL Schemes** field is set to `nsdk-samples`. This value matches the `callbackURLScheme` used in the login code. > **Info:** > > NSDK limits redirect schemes for security reasons. The sample app uses `nsdk-samples`. The other supported scheme is `nsdk-external`. NSDK does not support custom schemes. ### Platform: kotlin The sample app registers an intent filter in `AndroidManifest.xml` so Android can return the authentication result. The intent filter is added to the activity that starts login (typically `MainActivity`). The scheme is set to `nsdk-samples`, matching the callback scheme used in the login code. > **Info:** > > NSDK limits redirect schemes for security reasons. The sample app uses `nsdk-samples`. The other supported scheme is `nsdk-external`. NSDK does not support custom schemes. ```xml ``` ## The sample login flow This section walks through each stage of the sample login flow: 1. [Start login](#start-login) 2. [Handle the login callback](#handle-the-login-callback) 3. [Establish the sample session](#establish-the-sample-session) 4. [Exchange session tokens for NSDK access tokens](#exchange-session-tokens-for-nsdk-access-tokens) ### Start login The sample app initiates the login flow from its UI by calling the login manager. The login manager opens a browser page where the user authenticates with a Scaniverse account. After authentication completes, the browser redirects back to the app with an NS sample session token. ### Platform: unity Before starting login, the sample app initializes the endpoints used by the login flow. The login manager reads endpoint URLs from an `AuthEndpoints` ScriptableObject at runtime, so it must be set before calling `LoginManager.LoginRequested()`. The sample project creates the `AuthEndpoints` asset via **Create -> Scriptable Objects -> AuthEndpoints** in the Unity Project window, then initializes it with a script: ```csharp using UnityEngine; public class AuthEndpointsInitializer : MonoBehaviour { [SerializeField] private AuthEndpoints authEndpoints; private void Awake() { authEndpoints.SetAsSettings(); } } ``` This script is attached to a `GameObject` in the scene with the `AuthEndpoints` asset assigned in the Inspector. The sample app starts the login flow by calling `LoginManager.LoginRequested()` in response to user input. Here is the login button handler: ```csharp using UnityEngine; public class LoginController : MonoBehaviour { public void OnLoginTapped() { LoginManager.LoginRequested(); } } ``` This script is added to a `GameObject` in the scene, and `OnLoginTapped()` is connected to a UI Button in the Inspector. ### Platform: swift The sample app creates a `LoginManager` in its UI layer and passes a currently visible view so the authentication session can be presented. Here is how the sample app's view controller starts login: ```swift import Combine import UIKit class MyViewController: UIViewController { private var loginManager: LoginManager? private var cancellables = Set() @IBAction func onLoginTapped(_ sender: Any) { loginManager = LoginManager(view: self.view) loginManager?.startAuth() } } ``` ### Platform: kotlin The sample app creates a `LoginManager` in the activity that handles login (e.g. `MainActivity.kt`): 1. The activity class declares a `LoginManager` property: ```kotlin private lateinit var loginManager: LoginManager ``` 2. In `override fun onCreate(savedInstanceState: Bundle?)`, after `super.onCreate(savedInstanceState)`, the `LoginManager` is initialized: ```kotlin loginManager = LoginManager(this) ``` 3. When the user taps the login button, the app calls `loginManager.startAuth()`. The following code in `LoginManager.kt` starts the web-based authentication flow. The `redirectType` matches the scheme registered in the manifest. ```kotlin fun startAuth() { val authURL = "${AuthConstants.EndPointUrls.SIGN_IN}?redirectType=nsdk-samples".toUri() val customTabsIntent = CustomTabsIntent.Builder() .setShowTitle(true) .build() customTabsIntent.launchUrl(activity, authURL) } ``` ### Handle the login callback After authentication completes, the app receives the authentication result through a callback URL. This URL contains the NS sample session token required to establish a sample session. ### Platform: unity Unity delivers the authentication result through a deep link. When the browser finishes authentication, it redirects to the app using the registered callback scheme. The sample login flow handles this automatically. `LoginManager.LoginRequested()` sets up the deep link listener and processes the returned URL when the app resumes. No additional callback-handling code is needed in the sample app. ### Platform: swift iOS automatically routes the callback URL to `ASWebAuthenticationSession` when the redirect matches the provided `callbackURLScheme`. In the sample app, `LoginManager` opens an `ASWebAuthenticationSession` for authentication. The callback URL scheme passed to `ASWebAuthenticationSession` matches the scheme registered in Xcode. The following code in `LoginManager.swift` shows how the sample app starts an authentication session and handles the callback when it completes. ```swift func startAuth() { guard let authURL = URL(string: AuthConstants.EndPointUrls.SignIn + "?redirectType=\(AuthConstants.callbackUrlScheme)") else { return } let session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: AuthConstants.callbackUrlScheme ) { callbackURL, error in if let error = error { print("Auth error: \(error.localizedDescription)") return } if let callbackURL = callbackURL { self.handleCallback(url: callbackURL) } } session.presentationContextProvider = self session.start() } ``` ### Platform: kotlin Android routes the callback URL to the activity whose intent filter matches the registered scheme and host. When authentication completes, Android returns the deep link to the activity. The activity passes the returned intent to `LoginManager.handleCallback(...)`. In the sample app, the class that extends `ComponentActivity` or `AppCompatActivity` overrides `onNewIntent` to forward the callback intent to `LoginManager`: ```kotlin override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) loginManager.handleCallback(intent) } ``` This requires the following import at the top of the file: ```kotlin import android.content.Intent ``` ### Establish the sample session After login completes, the login manager extracts the returned NS sample session token from the callback URL and stores it in `NSSampleSessionManager`. ### Platform: unity The following code in `LoginManager.cs` completes the login flow: ```csharp private static void OnDeepLinkActivated(string url) { Application.deepLinkActivated -= OnDeepLinkActivated; IsLoginInProgress = false; var sessionToken = GetParamValue("refreshToken", url); NSSampleSessionManager.SetNSSampleSession(sessionToken); LoginComplete?.Invoke(); } ``` After login completes, the app returns to the scene and continues running. The sample session is established automatically. ### Platform: swift When login completes, iOS returns the callback URL to the login session completion handler. `LoginManager` extracts the returned NS sample session token and stores it in `NSSampleSessionManager`: ```swift private func handleCallback(url: URL) { let tokens = AuthUtils.extractTokens(from: url.absoluteString) NSSampleSessionManager.setNSSampleSession(sessionToken: tokens.refreshToken) } ``` `NSSampleSessionManager` persists the NS sample session token and manages the token exchange and refresh loop while the app is running. ### Platform: kotlin When login completes, Android returns the callback intent to the activity. `LoginManager.handleCallback(...)` extracts the returned NS sample session token and stores it in `NSSampleSessionManager`. The following code in `LoginManager.kt` completes the login flow: ```kotlin fun handleCallback(intent: Intent) { val data = intent.data ?: run { Log.w(TAG, "Received callback with null data") return } if (data.scheme != AuthConstants.CALLBACK_SCHEME) { Log.w(TAG, "Received callback with unexpected scheme: ${data.scheme}") return } val urlString = data.toString() val tokens = AuthUtils.extractTokens(urlString) NSSampleSessionManager.setNSSampleSession(sessionToken = tokens.second) } ``` `NSSampleSessionManager` persists the NS sample session token and manages the token exchange and refresh loop while the app is running. ### Exchange session tokens for NSDK access tokens After the NS sample session token is stored, the session manager exchanges it for an NSDK access token through three sequential HTTP requests (see [Token exchange detail](#token-exchange-detail)) and passes the result to the NSDK session. ### Platform: unity The access flow is set up before login starts to enable NSDK features. The sample login flow stores the NS sample session token after login. `NSSampleSessionManager` then exchanges it for an NSDK access token and sets it on the NSDK automatically. `NSSampleSessionManager` also runs a background refresh loop that periodically checks token expiration and re-exchanges before the token expires, keeping the NSDK authorized for the duration of the app session. ### Platform: swift During app initialization, the sample app calls `NSSampleSessionManager.start()` to begin session management, then uses `NSSampleSessionManager.setupSessionAccess(for:)` to subscribe to the token flow and forward NSDK access tokens to the session. Here is the relevant code from the same `MyViewController` shown earlier: ```swift override func viewDidLoad() { super.viewDidLoad() NSSampleSessionManager.start() sessionCancellable = NSSampleSessionManager.setupSessionAccess(for: nsdkSession) } ``` This subscription is triggered after login completes or when an existing sample session is restored. `NSSampleSessionManager` exchanges the NS sample session token for an NSDK access token and calls `session.setAccessToken(...)` automatically. ### Platform: kotlin During app initialization, the sample app calls `NSSampleSessionManager.start(...)` to begin session management, then uses `NSSampleSessionManager.setupSessionAccess(...)` to subscribe to the token flow and forward NSDK access tokens to the session. Here is the relevant code from the same activity shown earlier, in `override fun onCreate(savedInstanceState: Bundle?)` after `super.onCreate(savedInstanceState)`: ```kotlin NSSampleSessionManager.configure(SharedPrefsTokenStorage(this)) NSSampleSessionManager.start() NSSampleSessionManager.setupSessionAccess(nsdkSession, lifecycleScope) ``` This subscription is triggered after login completes or when an existing sample session is restored. `NSSampleSessionManager` exchanges the NS sample session token for an NSDK access token and calls `nsdkSession.setAccessToken(...)` automatically. Once logged in, the app uses the NSDK access token as described in the following sections. ## Use tokens in your app Whether using the sample login flow or a production backend, the NSDK APIs for managing tokens are the same. This section shows how the sample app: - Sets NSDK access tokens on the NSDK session. - Periodically checks if a token has expired or is about to expire. - Clears tokens on logout. The sample app sets the NSDK access token when initializing the NSDK session: ### Platform: swift ```swift let nsdkSession = NSDKSession(accessToken: currentToken) ``` The token can also be updated on the NsdkSession object after initialization: ```swift nsdkSession.setAccessToken(currentToken) ``` The sample app checks if the NSDK access token has expired or will expire soon using the session's `getAccessAuthInfo` function, which returns an `AuthInfo` object containing the expiration time in seconds since the Unix epoch: ```swift /// Returns true if the NSDK access token has expired or will expire within the next minute. /// Uses NsdkSession.getAccessAuthInfo to read the NSDK access token's expiration. func isAccessExpiredOrExpiringSoon() -> Bool { guard let authInfo = session.getAccessAuthInfo() else { return true } let now = Int32(Date().timeIntervalSince1970) let secondsUntilExpiry = authInfo.expirationTime - now return secondsUntilExpiry < 60 } ``` On logout, the sample app clears the NSDK access token to prevent further NSDK access until a new token is obtained: ```swift nsdkSession.logout() ``` ### Platform: kotlin ```kotlin val nsdkSession = NSDKSession(accessToken = currentToken) ``` The token can also be set on the NSDK session object after initialization: ```kotlin nsdkSession.setAccessToken(currentToken) ``` The sample app checks if the NSDK access token has expired or will expire soon using the session's `getAccessAuthInfo` function, which returns an `AuthInfo` object containing the expiration time in seconds since the Unix epoch: ```kotlin /** * Returns true if the NSDK access token has expired or is nearing expiration. * Uses [NSDKSession.getAccessAuthInfo] to read the NSDK access token's expiration. */ fun isAccessExpiredOrExpiringSoon(session: NSDKSession): Boolean { val authInfo = session.getAccessAuthInfo() ?: return true val nowSeconds = (System.currentTimeMillis() / 1000).toInt() val secondsUntilExpiry = authInfo.expirationTime - nowSeconds return secondsUntilExpiry < 60 } ``` On logout, the sample app clears the NSDK access token to prevent further NSDK access until a new token is obtained: ```kotlin nsdkSession.setAccessToken("") ``` ### Platform: unity ```csharp using NianticSpatial.NSDK.AR.Loader; NsdkSettingsHelper.ActiveSettings.AccessToken = accessToken ``` The sample app checks if the NSDK access token has expired or will expire soon using `AuthClient.GetAccessAuthInfo`, which returns an `AuthInfo` object containing the expiration time in seconds since the Unix epoch: ```csharp using NianticSpatial.NSDK.AR.Auth; /// /// Returns true if the NSDK access token has expired or is about to expire in under a minute. /// Uses to read the NSDK access token's expiration. /// /// true if access is expired or expires in under 60 seconds; false otherwise. public static bool IsAccessExpiredOrExpiringSoon() { var authInfo = AuthClient.GetAccessAuthInfo(); var currentTimeSeconds = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var timeLeft = authInfo.ExpirationTime - currentTimeSeconds; return timeLeft < ExpiringSoonThresholdSeconds; } ``` On logout, the sample app clears the NSDK access token to prevent further NSDK access until a new token is obtained: ```csharp AuthClient.StaticLogout(); ``` ## How this sample relates to production In a production app, your backend issues and manages NSDK access tokens for each logged-in user. See [Generate access tokens](https://www.nianticspatial.com/docs/nsdk/auth_backend/) for server-side setup. The sample login flow demonstrates the client-side pattern a production app would follow. The key differences are: 1. **Login flow.** Replace the login flow. Use your own login UI and identity system instead of the Scaniverse browser login. 2. **Token exchange.** Your production backend requests NSDK access tokens from the Niantic Spatial Identity Service using a service account (see [Generate access tokens](https://www.nianticspatial.com/docs/nsdk/auth_backend/)). Different users or organizations can be associated with different service accounts for asset and permission separation. The client receives only the final NSDK access token. 3. **Refresh pattern.** The sample app's approach of monitoring token expiration on the client and requesting new tokens before they expire applies equally to production -- only the token source changes. 4. **Passing the NSDK access token.** This is identical in both cases -- call `setAccessToken` with whatever token your backend provides. The NSDK uses the final access token the same way, regardless of its source. Tokens from the sample backend, your production backend, and developer tokens issued in Scaniverse web are all passed to the same NSDK APIs.