Skip to content

Sign in with KRDPASS (App-to-App): SDK Integration Guide

This page covers the full auth flow for all platforms. Pick your SDK — the steps are the same, only the code differs.

Prerequisites

Before starting, you should have:

  • clientId and clientSecret from Getting Started
  • SDK installed in your project
  • Backend server running with /oauth/par and /oauth/token endpoints

You'll use two URLs throughout:

ValueWhat it isExample
redirectUriYour app-launch callback URL (Universal Link)https://app-link.example.com/_krdpass/oauth/callback
backendUrlYour backend API base URLhttps://auth-api.example.com

These are different values. Don't mix them up.

Step 1: Initialize the SDK

dart
import 'package:krdpass_auth_flutter/krdpass_auth_flutter.dart';

final config = KrdpassConfig(
  clientId: 'your-client-id',
  redirectUri: 'https://app-link.example.com/_krdpass/oauth/callback',
  environment: KrdpassEnvironment.development,
);

await KrdpassAuth.instance.initialize(config: config);
kotlin
import krd.pass.auth.*

// In MainActivity.onCreate or Application startup
val config = KrdpassConfig(
    clientId = "your-client-id",
    redirectUri = "https://app-link.example.com/_krdpass/oauth/callback",
    environment = KrdpassEnvironment.Development
)

KrdpassAuth.initialize(config)
KrdpassAuth.register(this) // Must be called in Activity.onCreate
swift
import KrdpassAuth

let config = KrdpassConfig(
    clientId: "your-client-id",
    redirectUri: "https://app-link.example.com/_krdpass/oauth/callback",
    environment: .development
)

// Keep this instance alive for the entire auth flow
let auth = KrdpassAuth(config: config)
ts
import * as KrdpassAuth from 'krdpass-auth-react-native';
import { initialize } from 'krdpass-auth-react-native';

initialize({
  clientId: 'your-client-id',
  redirectUri: 'https://app-link.example.com/_krdpass/oauth/callback',
  environment: 'development',
});

Step 2: Generate PKCE + State

Generate fresh cryptographic values for every sign-in attempt. Never reuse these.

dart
final pkce = KrdpassAuth.instance.generatePkcePair();
final state = KrdpassAuth.instance.generateState();
final nonce = KrdpassAuth.instance.generateState();
kotlin
val pkce = KrdpassAuth.generatePkcePair()
val state = KrdpassAuth.generateState()
val nonce = KrdpassAuth.generateState()
swift
let pkce = try auth.generatePkcePair()
let state = try auth.generateState()
let nonce = try auth.generateState()
ts
const pkce = await KrdpassAuth.generatePkcePair();
const state = KrdpassAuth.generateState();
const nonce = KrdpassAuth.generateState();

Step 3: Get a Request URI from Your Backend

Generate Client with OpenAPI

You can generate your backend client code using our OpenAPI Specification instead of implementing these calls manually.

Call your own backend (not the SDK) to create a PAR request. Your backend calls POST /oauth/par on CAS and returns a requestUri.

dart
final par = await backend.getRequestUri(
  codeChallenge: pkce.codeChallenge,
  state: state,
  nonce: nonce,
  environment: 'development',
  redirectUri: redirectUri,
  scope: 'openid profile citizen_identity',
);
// par.requestUri, par.state, par.expiresIn
kotlin
val par = backend.getRequestUri(
    codeChallenge = pkce.codeChallenge,
    state = state,
    nonce = nonce,
    environment = "development",
    redirectUri = redirectUri,
    scope = "openid profile citizen_identity"
)
swift
let par = try await backend.getRequestUri(
    codeChallenge: pkce.codeChallenge,
    state: state,
    nonce: nonce,
    environment: .development,
    redirectUri: redirectUri,
    scope: "openid profile citizen_identity"
)
ts
const par = await backend.getRequestUri({
  codeChallenge: pkce.codeChallenge,
  state,
  nonce,
  environment: 'development',
  redirectUri,
  scope: 'openid profile citizen_identity',
});
What is backend.getRequestUri(...)?

This is your helper method — it's not part of the SDK. It should make a POST to ${backendUrl}/oauth/par with the parameters above and return { requestUri, state, expiresIn }.

See Reference → Endpoint Contracts for the exact request/response format, or expand for a minimal implementation:

dart
Future<ParResponse> getRequestUri({
  required String codeChallenge,
  required String environment,
  required String redirectUri,
  String? state, String? nonce, String? scope,
}) async {
  final response = await http.post(
    Uri.parse('$backendUrl/oauth/par'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'codeChallenge': codeChallenge,
      'codeChallengeMethod': 'S256',
      'environment': environment,
      'redirectUri': redirectUri,
      if (state != null) 'state': state,
      if (nonce != null) 'nonce': nonce,
      if (scope != null) 'scope': scope,
    }),
  );
  final data = jsonDecode(response.body);
  return ParResponse(
    requestUri: data['requestUri'],
    state: data['state'],
    expiresIn: data['expiresIn'],
  );
}
kotlin
suspend fun getRequestUri(
    codeChallenge: String,
    environment: String,
    redirectUri: String,
    state: String? = null,
    nonce: String? = null,
    scope: String? = null
): ParResponse {
    val body = JSONObject().apply {
        put("codeChallenge", codeChallenge)
        put("codeChallengeMethod", "S256")
        put("environment", environment)
        put("redirectUri", redirectUri)
        state?.let { put("state", it) }
        nonce?.let { put("nonce", it) }
        scope?.let { put("scope", it) }
    }
    // POST $backendUrl/oauth/par, parse response
}
swift
func getRequestUri(
  codeChallenge: String, state: String,
  nonce: String? = nil,
  environment: KrdpassEnvironment,
  redirectUri: String, scope: String
) async throws -> ParResponse {
  let url = URL(string: "\(backendUrl)/oauth/par")!
  var body: [String: Any] = [
    "codeChallenge": codeChallenge,
    "codeChallengeMethod": "S256",
    "state": state,
    "environment": environment.name,
    "redirectUri": redirectUri,
    "scope": scope
  ]
  if let nonce = nonce { body["nonce"] = nonce }
  var request = URLRequest(url: url)
  request.httpMethod = "POST"
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  request.httpBody = try JSONSerialization.data(withJSONObject: body)
  // parse requestUri/state/expiresIn from response
}
ts
async function getRequestUri(params: {
  codeChallenge: string; environment: string;
  redirectUri: string; state?: string;
  nonce?: string; scope?: string;
}) {
  const res = await fetch(`${BACKEND_URL}/oauth/par`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      codeChallenge: params.codeChallenge,
      codeChallengeMethod: 'S256',
      environment: params.environment,
      redirectUri: params.redirectUri,
      ...(params.state && { state: params.state }),
      ...(params.nonce && { nonce: params.nonce }),
      ...(params.scope && { scope: params.scope }),
    }),
  });
  return await res.json(); // { requestUri, state, expiresIn }
}

Step 4: Launch KRDPASS & Wait for Callback

This opens the KRDPASS app. The user authenticates there, then gets redirected back to your app with an auth code.

dart
final authResult = await KrdpassAuth.instance.authenticate(
  requestUri: par.requestUri,
  state: par.state,
  timeout: Duration(seconds: par.expiresIn ?? 300),
);

if (!authResult.isSuccess) {
  throw KrdpassAuth.instance.authResultToException(authResult);
}
kotlin
val authTimeout = (par.expiresIn ?: 300).coerceAtLeast(1).seconds

val result = KrdpassAuth.authenticate(
    requestUri = par.requestUri,
    state = par.state,
    timeout = authTimeout
)

when (result) {
    is AuthResult.Success -> { /* continue to Step 5 */ }
    is AuthResult.Cancelled -> { /* user cancelled */ }
    is AuthResult.Timeout -> { /* flow timed out */ }
    is AuthResult.Busy -> { /* auth already in progress */ }
    is AuthResult.Error -> { /* handle error */ }
}
swift
let result = await auth.authenticate(
    requestUri: par.requestUri,
    state: par.state,
    timeout: TimeInterval(par.expiresIn ?? 300)
)

switch result {
case .success(let response):
    // continue to Step 5 with response.code
case .cancelled, .timeout, .busy:
    break
case .error(let err):
    print(err.message)
default:
    break
}
ts
const authResult = await KrdpassAuth.authenticate({
  requestUri: par.requestUri,
  state: par.state,
});

if ('error' in authResult) {
  throw new Error(authResult.errorDescription ?? authResult.error);
}

Step 5: Exchange Code for Tokens

Send the auth code to your backend, which exchanges it for tokens via POST /oauth/token:

dart
final tokens = await backend.exchangeToken(
  code: authResult.code!,
  state: par.state ?? '',
  codeVerifier: pkce.codeVerifier,
  environment: 'development',
  redirectUri: redirectUri,
);
kotlin
val tokens = backend.exchangeToken(
    code = result.code,
    state = result.state ?: "",
    codeVerifier = pkce.codeVerifier,
    environment = "development",
    redirectUri = redirectUri
)
swift
let tokens = try await backend.exchangeToken(
    code: response.code,
    state: response.state ?? "",
    codeVerifier: pkce.codeVerifier,
    environment: .development,
    redirectUri: redirectUri
)
ts
const tokens = await backend.exchangeToken({
  code: authResult.code,
  state: authResult.state ?? '',
  codeVerifier: pkce.codeVerifier,
  environment: 'development',
  redirectUri,
});

Done! You now have tokens. Use access_token to call APIs and id_token to read user identity claims.

Platform Setup

Each platform needs some native configuration for the callback to work.

  1. Open your target in Xcode → Signing & Capabilities
  2. Add Associated Domains capability
  3. Add: applinks:app-link.example.com (your redirect host)
  4. Host an Apple App Site Association (AASA) file at https://app-link.example.com/.well-known/apple-app-site-association
  5. Forward callbacks to the SDK:
swift
@main
struct MyApp: App {
    @StateObject private var viewModel = AuthViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: viewModel)
                .onOpenURL { url in
                    viewModel.handleDeepLink(url)
                }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    guard let url = activity.webpageURL else { return }
                    viewModel.handleDeepLink(url)
                }
        }
    }
}

final class AuthViewModel: ObservableObject {
    let auth = KrdpassAuth(config: ...)

    func handleDeepLink(_ url: URL) {
        if auth.canHandle(url) { _ = auth.handle(url) }
    }
}
swift
func application(
  _ app: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  guard let url = userActivity.webpageURL else { return false }
  if auth.canHandle(url) { return auth.handle(url) }
  return false
}
Minimal AASA example
json
{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "AB3456789K.krd.gov.myapp.ios",
      "paths": ["/_krdpass/*"]
    }]
  }
}

Serve as application/json with no redirects at /.well-known/apple-app-site-association.

Android: Package & SHA-256

Register your package name and SHA-256 fingerprint during onboarding. See Reference → Android SHA-256 for how to generate it.

React Native: Expo & Bare RN

Expo apps:

Add the plugin to app.json:

json
{
  "expo": {
    "plugins": ["krdpass-auth-react-native"],
    "ios": {
      "associatedDomains": ["applinks:app-link.example.com"]
    }
  }
}

Then run npx expo prebuild.

Bare React Native apps:

bash
npx install-expo-modules@latest
npx pod-install
Bare RN native config you need to apply manually
xml
<!-- AndroidManifest.xml -->
<application ...>
  <activity android:name=".MainActivity" android:launchMode="singleTask" ... />
</application>

<queries>
  <package android:name="krd.pass" />
  <package android:name="krd.pass.staging" />
  <package android:name="krd.pass.dev" />
</queries>
xml
<!-- Info.plist -->
<key>LSApplicationQueriesSchemes</key>
<array><string>krdpass</string></array>
swift
// AppDelegate.swift — forward callbacks to RCTLinkingManager
func application(_ app: UIApplication, open url: URL,
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  return RCTLinkingManager.application(app, open: url, options: options)
}

func application(_ application: UIApplication,
  continue userActivity: NSUserActivity,
  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
  return RCTLinkingManager.application(
    application, continue: userActivity, restorationHandler: restorationHandler)
}

Direct Mode (Testing Only)

Currently Unavailable

Direct Mode only works when a client secret is not required (public client). This configuration is not currently available to anyone. You must use the Server-Mediated Flow.

For quick testing without a backend, you would use direct mode. Don't use this in production.

dart
final tokens = await KrdpassAuth.instance.signIn(
  scopes: [KrdpassScopes.openid, KrdpassScopes.profile],
);
kotlin
val tokens = KrdpassAuth.signIn(listOf("openid", "profile"))
swift
let tokens = try await auth.signIn(scopes: ["openid", "profile"])
ts
const tokens = await KrdpassAuth.signIn({
  clientId: 'your-client-id',
  redirectUri: 'https://app-link.example.com/_krdpass/oauth/callback',
  scopes: 'openid profile',
});

Other SDK APIs

All SDKs also provide:

  • getUserInfo(accessToken) — fetch user profile
  • refreshTokens(refreshToken) — refresh expired access tokens
  • revokeToken(token) — revoke a token
  • verifyToken(token) — verify token validity

Example App Configuration

Each SDK has a working example app. Configuration files:

SDKConfig fileTemplate
Flutterexample/.envexample/env.example
Androidexample/config.propertiesexample/config.properties.example
iOSXcode scheme env varsexample/env.example
React Nativeexample/.env + example/app.jsonexample/.env.example

For step-by-step example app testing, see Testing & Go-Live.

Next Step

Testing & Go-Live — Test locally with scripts, troubleshoot issues, and prepare for production