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:
clientIdandclientSecretfrom Getting Started- SDK installed in your project
- Backend server running with
/oauth/parand/oauth/tokenendpoints
You'll use two URLs throughout:
| Value | What it is | Example |
|---|---|---|
redirectUri | Your app-launch callback URL (Universal Link) | https://app-link.example.com/_krdpass/oauth/callback |
backendUrl | Your backend API base URL | https://auth-api.example.com |
These are different values. Don't mix them up.
Step 1: Initialize the SDK
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);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.onCreateimport 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)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.
final pkce = KrdpassAuth.instance.generatePkcePair();
final state = KrdpassAuth.instance.generateState();
final nonce = KrdpassAuth.instance.generateState();val pkce = KrdpassAuth.generatePkcePair()
val state = KrdpassAuth.generateState()
val nonce = KrdpassAuth.generateState()let pkce = try auth.generatePkcePair()
let state = try auth.generateState()
let nonce = try auth.generateState()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.
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.expiresInval par = backend.getRequestUri(
codeChallenge = pkce.codeChallenge,
state = state,
nonce = nonce,
environment = "development",
redirectUri = redirectUri,
scope = "openid profile citizen_identity"
)let par = try await backend.getRequestUri(
codeChallenge: pkce.codeChallenge,
state: state,
nonce: nonce,
environment: .development,
redirectUri: redirectUri,
scope: "openid profile citizen_identity"
)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:
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'],
);
}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
}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
}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.
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);
}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 */ }
}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
}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:
final tokens = await backend.exchangeToken(
code: authResult.code!,
state: par.state ?? '',
codeVerifier: pkce.codeVerifier,
environment: 'development',
redirectUri: redirectUri,
);val tokens = backend.exchangeToken(
code = result.code,
state = result.state ?: "",
codeVerifier = pkce.codeVerifier,
environment = "development",
redirectUri = redirectUri
)let tokens = try await backend.exchangeToken(
code: response.code,
state: response.state ?? "",
codeVerifier: pkce.codeVerifier,
environment: .development,
redirectUri: redirectUri
)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.
iOS: Universal Links
- Open your target in Xcode → Signing & Capabilities
- Add Associated Domains capability
- Add:
applinks:app-link.example.com(your redirect host) - Host an Apple App Site Association (AASA) file at
https://app-link.example.com/.well-known/apple-app-site-association - Forward callbacks to the SDK:
@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) }
}
}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
{
"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:
{
"expo": {
"plugins": ["krdpass-auth-react-native"],
"ios": {
"associatedDomains": ["applinks:app-link.example.com"]
}
}
}Then run npx expo prebuild.
Bare React Native apps:
npx install-expo-modules@latest
npx pod-installBare RN native config you need to apply manually
<!-- 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><!-- Info.plist -->
<key>LSApplicationQueriesSchemes</key>
<array><string>krdpass</string></array>// 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.
final tokens = await KrdpassAuth.instance.signIn(
scopes: [KrdpassScopes.openid, KrdpassScopes.profile],
);val tokens = KrdpassAuth.signIn(listOf("openid", "profile"))let tokens = try await auth.signIn(scopes: ["openid", "profile"])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 profilerefreshTokens(refreshToken)— refresh expired access tokensrevokeToken(token)— revoke a tokenverifyToken(token)— verify token validity
Example App Configuration
Each SDK has a working example app. Configuration files:
| SDK | Config file | Template |
|---|---|---|
| Flutter | example/.env | example/env.example |
| Android | example/config.properties | example/config.properties.example |
| iOS | Xcode scheme env vars | example/env.example |
| React Native | example/.env + example/app.json | example/.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
