Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 45 additions & 157 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 8.0 (API level 26) 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

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading