Skip to content

Commit 81b4d65

Browse files
committed
android pkce scenario init
1 parent 5b1c34a commit 81b4d65

30 files changed

Lines changed: 1460 additions & 112 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ target/
7575
**/build/
7676
**/Pods/
7777
**/.gradle/
78+
**/.kotlin/
7879
**/.cxx/
7980
**/.xcode.env.local
8081
**/*.xcworkspace/
8182
**/local.properties
82-
**/debug.keystore
83+
**/*.keystore
84+
**/captures/
8385
samples/ios/**/Config.xcconfig

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ samples/ # One folder per framework
1212
node/ # Node.js server samples (OIDC + SAML)
1313
java/ # Java/Spring Boot server samples (OIDC + SAML)
1414
dotnet/ # .NET server samples (OIDC + SAML)
15+
android/ # Native Android samples (Kotlin + Compose + AppAuth-Android)
1516
ios/ # Native iOS samples (Swift + SwiftUI + AppAuth-iOS)
1617
react-native/ # React Native mobile samples (PKCE + token refresh)
1718
scripts/ # Extraction and validation tools
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Android — Login with PKCE
2+
3+
Minimal Android app demonstrating OIDC login using Authorization Code + PKCE via [AppAuth-Android](https://github.com/openid/AppAuth-Android). Chrome Custom Tabs (the system browser) handles the auth UI — never an embedded `WebView`.
4+
5+
## Prerequisites
6+
7+
- JDK 21 (`java -version` reports 21)
8+
- Android SDK with platform-android-36 and build-tools-36 installed (Android Studio's SDK Manager, or `sdkmanager`)
9+
- `ANDROID_HOME` (or `ANDROID_SDK_ROOT`) env var pointing at the SDK root
10+
- An emulator runtime or a USB-debugging-enabled physical device
11+
12+
## Setup
13+
14+
1. Copy `local.example.properties` to `local.properties` and fill in your SecureAuth values:
15+
- `CLIENT_ID` — the OAuth client ID
16+
- `ISSUER_HOST` — host (and port if needed) of your issuer (e.g. `your-tenant.us.connect.secureauth.com`)
17+
- `ISSUER_PATH` — workspace path of your issuer (e.g. `your-workspace`). Leave empty if the issuer has no workspace component (typical for local dev).
18+
- `REDIRECT_SCHEME` — leave as `com.secureauth.quickstart.android.login` (must match the scheme used by AppAuth-Android's intent filter and your CIAM-side redirect URI)
19+
- `SCOPES` — space-separated list (e.g. `openid profile email`)
20+
2. In CIAM admin UI, register the redirect URI exactly: `com.secureauth.quickstart.android.login://oauthredirect`
21+
3. `./gradlew :app:installDebug` (or open the project in Android Studio and click Run)
22+
4. Launch the app from the device/emulator launcher
23+
24+
## What this demonstrates
25+
26+
- OIDC configuration with PKCE on a public mobile client (no client secret)
27+
- System-browser-driven login (Chrome Custom Tabs — no embedded WebView)
28+
- Auth Code + PKCE token exchange
29+
- Local logout via `revoke()` + clearing app state
30+
- Access-token expiry detection that flips the UI to "Session expired" without re-prompting silently
31+
32+
## Notes
33+
34+
- `local.properties` is read at gradle configure time. Editing it requires a rebuild (`./gradlew :app:installDebug`) — values are baked into `BuildConfig.CIAM_*`.
35+
- The redirect URI's custom URL scheme is set in two places that must match: `manifestPlaceholders["appAuthRedirectScheme"]` in `app/build.gradle.kts` (which expands `${appAuthRedirectScheme}` in `AndroidManifest.xml`), and your CIAM-side redirect URI registration. Both come from the same source — the `REDIRECT_SCHEME` entry in `local.properties` — so editing one place keeps them in sync.
36+
- AppAuth-Android refuses to launch in an embedded `WebView` and explicitly opens Chrome Custom Tabs. Devices and emulator images without Chrome (or another Custom-Tabs-supporting browser) will fail to sign in. Most modern emulator images include Chrome.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import java.util.Properties
2+
3+
plugins {
4+
alias(libs.plugins.android.application)
5+
alias(libs.plugins.kotlin.compose)
6+
}
7+
8+
// Read local.properties at configure time so we can surface CIAM_* fields into
9+
// BuildConfig (read at runtime) and into AndroidManifest.xml (intent filter).
10+
val localProps = Properties().apply {
11+
val f = rootProject.file("local.properties")
12+
if (f.exists()) f.inputStream().use { load(it) }
13+
}
14+
fun localProp(key: String): String = localProps.getProperty(key, "")
15+
16+
android {
17+
namespace = "com.secureauth.quickstart"
18+
compileSdk = 36
19+
20+
defaultConfig {
21+
applicationId = "com.secureauth.quickstart.android.login"
22+
minSdk = 26
23+
targetSdk = 36
24+
versionCode = 1
25+
versionName = "1.0"
26+
27+
buildConfigField("String", "CIAM_CLIENT_ID", "\"${localProp("CLIENT_ID")}\"")
28+
buildConfigField("String", "CIAM_ISSUER_HOST", "\"${localProp("ISSUER_HOST")}\"")
29+
buildConfigField("String", "CIAM_ISSUER_PATH", "\"${localProp("ISSUER_PATH")}\"")
30+
buildConfigField("String", "CIAM_REDIRECT_SCHEME", "\"${localProp("REDIRECT_SCHEME")}\"")
31+
buildConfigField("String", "CIAM_SCOPES", "\"${localProp("SCOPES")}\"")
32+
33+
// AppAuth-Android's RedirectUriReceiverActivity intent filter is parameterised
34+
// on ${appAuthRedirectScheme}. Tying both to the same source value (the
35+
// REDIRECT_SCHEME entry in local.properties) prevents drift between code and
36+
// manifest.
37+
manifestPlaceholders["appAuthRedirectScheme"] = localProp("REDIRECT_SCHEME")
38+
}
39+
40+
buildFeatures {
41+
compose = true
42+
buildConfig = true
43+
}
44+
45+
compileOptions {
46+
sourceCompatibility = JavaVersion.VERSION_21
47+
targetCompatibility = JavaVersion.VERSION_21
48+
isCoreLibraryDesugaringEnabled = false
49+
}
50+
51+
buildTypes {
52+
debug { isMinifyEnabled = false }
53+
release {
54+
isMinifyEnabled = false
55+
proguardFiles(
56+
getDefaultProguardFile("proguard-android-optimize.txt"),
57+
"proguard-rules.pro",
58+
)
59+
}
60+
}
61+
}
62+
63+
dependencies {
64+
implementation(libs.appauth)
65+
66+
implementation(libs.androidx.core.ktx)
67+
implementation(libs.androidx.activity.compose)
68+
implementation(libs.androidx.lifecycle.viewmodel.compose)
69+
70+
val composeBom = platform(libs.androidx.compose.bom)
71+
implementation(composeBom)
72+
implementation(libs.androidx.compose.ui)
73+
implementation(libs.androidx.compose.ui.tooling.preview)
74+
implementation(libs.androidx.compose.material3)
75+
debugImplementation(libs.androidx.compose.ui.tooling)
76+
77+
testImplementation(libs.junit)
78+
// org.json.JSONObject is part of the Android framework (android.jar) but stubbed
79+
// out in pure-JVM unit tests. The standalone Apache org.json JAR provides a
80+
// working implementation for unit tests; production code uses the framework one.
81+
testImplementation(libs.org.json)
82+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# ProGuard / R8 rules go here. Empty for the quickstart — minifyEnabled is off
2+
# for both debug and release builds. If you turn minify on for release, add
3+
# AppAuth-Android's keep rules from
4+
# https://github.com/openid/AppAuth-Android#proguard
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
7+
<application
8+
android:label="@string/app_name"
9+
android:theme="@android:style/Theme.DeviceDefault.Light.NoActionBar"
10+
android:allowBackup="false"
11+
tools:targetApi="33">
12+
13+
<activity
14+
android:name=".MainActivity"
15+
android:exported="true">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
<category android:name="android.intent.category.LAUNCHER" />
19+
</intent-filter>
20+
</activity>
21+
22+
<!-- AppAuth-Android catches the OAuth redirect on this activity. The scheme
23+
is injected from build.gradle.kts via manifestPlaceholders so the
24+
redirect URI registered in CIAM admin matches the one the OS routes back. -->
25+
<activity
26+
android:name="net.openid.appauth.RedirectUriReceiverActivity"
27+
android:exported="true"
28+
tools:node="replace">
29+
<intent-filter>
30+
<action android:name="android.intent.action.VIEW" />
31+
<category android:name="android.intent.category.DEFAULT" />
32+
<category android:name="android.intent.category.BROWSABLE" />
33+
<data android:scheme="${appAuthRedirectScheme}" />
34+
</intent-filter>
35+
</activity>
36+
</application>
37+
</manifest>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.secureauth.quickstart
2+
3+
object AuthExpiry {
4+
/**
5+
* `Valid(remainingMs)` — token is not expired. `remainingMs` is the number of
6+
* milliseconds until expiry, or `null` if no expiry was provided (treat as
7+
* non-expiring; matches the iOS sample's "no timer when no date" behavior).
8+
* `Expired` — the provided expiry has already elapsed.
9+
*/
10+
sealed class Result {
11+
data class Valid(val remainingMs: Long?) : Result()
12+
data object Expired : Result()
13+
}
14+
15+
fun evaluate(expiresAt: Long?): Result {
16+
if (expiresAt == null) return Result.Valid(null)
17+
val remaining = expiresAt - System.currentTimeMillis()
18+
return if (remaining <= 0L) Result.Expired else Result.Valid(remaining)
19+
}
20+
}

0 commit comments

Comments
 (0)