From f07d213327fd5659f7f4471d9079916c819ae5e7 Mon Sep 17 00:00:00 2001 From: "workos-tars[bot]" <269013284+workos-tars[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:05:15 +0000 Subject: [PATCH 1/2] docs: Lead Android quickstart with PublicClient --- README.md | 202 +++++++++++------------------------------------ build.gradle.kts | 2 +- 2 files changed, 46 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 294cf2f..9a6d51a 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,73 @@ -# WorkOS Android (Kotlin) SDK +# WorkOS Android SDK -Official Kotlin SDK for the WorkOS API, targeting Android. +The WorkOS Android SDK provides access to AuthKit from Kotlin applications. Use +`PublicClient` to sign users in with PKCE using only your application's client ID. -> **Status: pre-release.** The spec-driven surface is complete and tested, and the -> hand-maintained helper layer is implemented apart from the two SSO PKCE helpers -> that the OpenAPI spec cannot yet express — see [Helper coverage](#helper-coverage). +> **Never include a WorkOS API key or client secret in an Android app.** Anything +> shipped in an APK is extractable. API keys belong only in trusted server +> environments. -## Installation - -```kotlin -dependencies { - implementation("com.workos:workos-android:0.1.0") -} -``` - -## Usage - -```kotlin -import com.workos.android.WorkOSClient - -val client = WorkOSClient(apiKey = "sk_test_...") - -val org = client.organizations.create(name = "Acme") -val page = client.organizations.list(limit = 10) - -// Auto-pagination walks every page. -client.organizations.listAutoPaging().collect { println(it.name) } -``` +## Requirements -Every resource method is a `suspend fun` and takes an optional `RequestOptions`: +- Android 7.0 (API level 24) or later +- Kotlin 2.4 or later +- JDK 17 -```kotlin -client.organizations.get( - id = "org_123", - requestOptions = RequestOptions( - headers = mapOf("X-Trace-Id" to traceId), - timeoutSeconds = 10, - maxRetries = 0, - idempotencyKey = key, - ), -) -``` +## Installation -Errors are typed — the runtime never leaks OkHttp exceptions: +Add Maven Central to your repositories, then add the SDK to your app module's +`build.gradle.kts`: ```kotlin -try { - client.organizations.get(id = "org_missing") -} catch (e: NotFoundException) { - println("${e.statusCode} ${e.code} ${e.requestId}") +dependencies { + implementation("com.workos:workos-android:0.2.1") } ``` -## Generated vs. hand-maintained - -Most of this repository is generated by [oagen](https://github.com/workos/oagen) -from the WorkOS OpenAPI spec. **Do not edit generated files** — a CI check blocks -PRs that do. Fix the emitter in `workos/oagen-emitters` (`src/android/`) and -regenerate. - -| Generated | Hand-maintained (`@oagen-ignore-file`) | -| -------------------------------- | ---------------------------------------------------------- | -| `models/`, `enums/`, `resources/` | `WorkOSClient.kt`, `Configuration.kt`, `RequestOptions.kt` | -| `WorkOSClientResources.kt` | `Page.kt`, `WorkOSException.kt` | -| `*Test.kt` per resource | `internal/` (Transport, Json, JsonBody, PathEncoding, AutoPaging) | -| `ModelRoundTripTest.kt` | `helpers/`, `src/test/.../support/`, `TransportBehaviorTest.kt` | - -## Android target - -This module currently builds with the Kotlin **JVM** plugin, not -`com.android.library`. Nothing in the generated SDK touches an Android API — it is -OkHttp + kotlinx.serialization + kotlinx.datetime — so the artifact is consumable -from Android today, and building it needs no Android SDK. Publishing an AAR is a -build-file change, not a source change. - -## Helper coverage - -Six capabilities that were once hand-maintained are now **in the OpenAPI spec** and -therefore generated — verified against the live resolved operation table, not assumed: - -| ID | Capability | Generated as | -| --- | -------------------------------- | -------------------------------------------------- | -| H09 | `authkit_authorization_url` | `userManagement.getAuthorizationUrl` | -| H11 | `authkit_pkce_code_exchange` | `userManagement.authenticateWithCode(codeVerifier)` | -| H12 | `authkit_device_flow` | `createDevice` + `authenticateWithDeviceCode` | -| H13 | `jwks_helper` | `userManagement.getJwks` | -| H14 | `sso_authorization_url` | `sso.getAuthorizationUrl` | -| H17 | `sso_logout_helper` | `sso.getLogoutUrl` + `sso.authorizeLogout` | - -Hand-maintained in `helpers/`: - -| ID | Capability | Status | -| --- | ----------------------- | ------ | -| — | Passwordless | ✅ `passwordless.createSession` / `sendSession` | -| H08 | `pkce_utilities` | ✅ `pkce.generate()` / `generateCodeVerifier` / `generateCodeChallenge` | -| H10 | `authkit_pkce_authorization_url` | ✅ `userManagement.getAuthorizationUrlWithPkce` | -| H19 | `public_client_factory` | ✅ `PublicClient.create(clientId)` | -| H01 | `webhook_verify` | ✅ `WebhookVerification().constructEvent(...)` | -| H02 | `webhook_signature_primitives` | ✅ `verifyHeader` / `createSignature` | -| H03 | `actions_helper` | ✅ `client.actions` — verify + sign | -| H04 | `session_cookie_object` | ✅ `client.session.loadSealedSession(...)` — `authenticate` / `refresh` / `getLogoutUrl` | -| H05 | `session_cookie_inline` | ✅ `client.session.authenticateWithSessionCookie` / `refreshSession` | -| H06 | `session_cookie_raw_seal` | ✅ `Iron.seal` / `Iron.unseal` (Fe26.2, workos-node interop verified) | -| H07 | `auth_response_session_sealing` | ✅ `client.session.sealAuthResponse(response, password)` | -| H15 | `sso_pkce_authorization_url` | ✅ `sso.getAuthorizationUrlWithPkce(...)` | -| H16 | `sso_pkce_code_exchange` | ⛔ blocked — `POST /sso/token` requires `client_secret`, which an app cannot hold (see [SSO PKCE](#sso-pkce)) | -| H18 | `vault_local_crypto` | ✅ `client.vaultCrypto.encrypt` / `.decrypt` | - -H01-H07 and H18 are **wire-compatibility-critical**: their sealing and signing -schemes have to interoperate with the Node/Python/Kotlin SDKs, so each is verified -against a cross-SDK fixture rather than written from spec text alone. `IronTest` -opens a seal produced by `workos-node`, and `SessionTest` decodes both the -`workos-node` and `workos-kotlin` cookie payload shapes. - -### Session cookies - -`client.session` needs a `clientId` on the client, because verifying a session's -access token means fetching the environment's JWKS from -`{baseUrl}/sso/jwks/{clientId}`. +## Quickstart + +Create a [WorkOS account](https://dashboard.workos.com/), copy your application's +client ID, and register a redirect URI in the Dashboard. Use the same URI in your +app's callback intent filter and when starting sign-in. ```kotlin -val client = WorkOSClient(apiKey = "sk_test_...", clientId = "client_123") +import com.workos.android.helpers.PublicClient -when (val result = client.session.authenticateWithSessionCookie(cookie, cookiePassword)) { - is AuthenticateSessionResult.Success -> result.user // verified - is AuthenticateSessionResult.Failure -> result.reason // why not -} +val workos = PublicClient.create(clientId = "client_...") +val authorization = workos.getAuthorizationUrlWithPkce( + redirectUri = "com.example.myapp://callback", +) ``` -`authenticate()` and `refresh()` are `suspend` functions — unlike `workos-kotlin`, -where they block. JWKS retrieval is network I/O, and on Android that must not run on -the main thread. - -Cookies are **written** in the camelCase shape `workos-node` produces, at every -level including inside `user`. They are **read** permissively, accepting either -camelCase or snake_case keys, so cookies sealed by `workos-kotlin` (which emits -snake_case inside `user`) also open here. - -### SSO PKCE - -`sso.getAuthorizationUrlWithPkce` adds `code_challenge` and `code_challenge_method` -to the SSO authorization URL. Both are accepted and S256-enforced by the API but are -`@ApiHideProperty()`, so they never reach the OpenAPI document and cannot be -generated — hence a hand-maintained helper, as in every other WorkOS SDK. +Securely persist `authorization.codeVerifier` and `authorization.state` before +opening `authorization.url` in a Custom Tab, not a WebView. Both values must +survive Android process death while the browser is open. -**The exchange leg does not exist, deliberately.** `POST /sso/token` requires -`client_secret`, validated before any other check: - -``` -POST /sso/token {grant_type, client_id, code} -> 422 - {"errors":[{"field":"client_secret","code":"client_secret must be a string"}]} -``` - -An Android app cannot hold a client secret, so there is no secret-less SSO code -exchange to wrap. Until the API accepts `code_verifier` in place of -`client_secret`, **use AuthKit for a complete PKCE flow**: +When the callback arrives, handle cancellation and errors, and reject a missing +or mismatched `state`. After validating it against the saved state, exchange the +authorization code using the saved verifier from a coroutine: ```kotlin -val start = client.userManagement.getAuthorizationUrlWithPkce(redirectUri = "app://callback") -// persist start.codeVerifier across process death, open start.url, then: -val auth = client.userManagement.authenticateWithCode(code = code, codeVerifier = start.codeVerifier) +val authentication = workos.authenticateWithCode( + code = authorizationCode, + codeVerifier = savedCodeVerifier, +) ``` -### Public-client usage (Android) +Clear the pending verifier and state after the exchange, including on failure. +Keep access tokens in memory and refresh tokens in Keystore-backed storage; never +log either token. -An Android binary cannot hold a WorkOS API key — anything in the APK is -extractable. Use `PublicClient`, which carries an empty key so the full service -surface fails loudly rather than appearing to work in development: +## Next steps -```kotlin -val public = PublicClient.create(clientId = "client_123") -val start = public.getAuthorizationUrlWithPkce(redirectUri = "app://callback") -// persist start.codeVerifier across process death, then open start.url -val auth = public.authenticateWithCode(code = code, codeVerifier = start.codeVerifier) -``` +- [Android sign-in tutorial](https://workos.com/blog/sign-in-to-an-android-app-with-the-workos-android-sdk): callback handling, secure storage, token refresh, and sign-out. +- [AuthKit documentation](https://workos.com/docs/authkit) +- [Android SDK API reference](https://workos.github.io/workos-android/) +- [GitHub Releases](https://github.com/workos/workos-android/releases): review the release notes before upgrading. -## Development +## Contributing -```bash -./script/ci # ktlint + tests -``` +See [CONTRIBUTING.md](CONTRIBUTING.md) for development and generated-code guidance. ## License diff --git a/build.gradle.kts b/build.gradle.kts index 03218ae..4e78692 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ plugins { // SDK touches an Android API — it is OkHttp + kotlinx only — so a plain JVM // library is consumable from Android and buildable without the Android SDK. // Switching to `com.android.library` to publish an AAR is a build-file change, - // not a source change. See the "Android target" section of README.md. + // not a source change. See the "Requirements" section of README.md. id("org.jetbrains.kotlin.jvm") version "2.4.0" id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0" From 42f825a360e58f5b16830be8b5de5fa3a730c18b Mon Sep 17 00:00:00 2001 From: "workos-tars[bot]" <269013284+workos-tars[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:08:32 +0000 Subject: [PATCH 2/2] docs: Require API 26 for the PKCE quickstart --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a6d51a..95b9b8d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The WorkOS Android SDK provides access to AuthKit from Kotlin applications. Use ## Requirements -- Android 7.0 (API level 24) or later +- Android 8.0 (API level 26) or later - Kotlin 2.4 or later - JDK 17