Experimental Android credential provider for atproto accounts
Kotlin 94%
Shell 5%
Batchfile 1%

README.md

Atmosphere Credential Provider for Android #

This repository contains an experimental Android app that makes an existing AT Protocol account available as a device account. It runs on stock Android 14 or newer and does not require an Android or GrapheneOS fork.

Atmosphere accounts showing @scottlanoue.com and its PDS on stock Android

The project contains three small modules:

  • provider signs in through AT Protocol OAuth, encrypts its session with an Android Keystore key, registers the public account identity with Android AccountManager, and offers that identity through Credential Manager.
  • atmosphere-credential defines the versioned custom credential used by the provider and participating apps.
  • sample-app shows the complete request and decode flow.

The credential contains only the selected account's DID, handle, PDS origin, and primary-account flag. The provider's access token, refresh token, and DPoP private key never cross the app boundary. A consuming app must create and own a separate OAuth grant.

Requirements #

  • JDK 17 or newer, using Android Studio or command-line Gradle
  • Android SDK 37 for the provider and sample app. The credential contract compiles against SDK 36.
  • Android 14 or newer for the provider and picker
  • Public AT Protocol native-client metadata served over HTTPS and an exact matching callback URI
  • A PDS implementing the experimental /oauth/device-approval/local extension for the optional passwordless, same-device flow

The checked-in defaults reproduce the prototype using:

  • client ID: https://auth.lanoue.dev/oauth/comail-os/client-metadata.json
  • redirect URI: dev.lanoue.auth:/oauth/callback

Override them for another OAuth client identity:

atmosphereOAuthClientId=https://example.com/oauth/client-metadata.json
atmosphereOAuthRedirectUri=com.example:/oauth/callback

The client ID must be an HTTPS URL without an explicit port. The redirect URI must use the reversed client hostname as its custom scheme, followed by one slash and a non-empty path. It cannot contain a query or fragment. For example, https://example.com/... pairs with com.example:/....

Put these values in your user-level Gradle properties file or pass them with -P. They are public configuration and are embedded in the APK. Never commit signing material or actual secrets.

Build #

./gradlew --dependency-verification strict test lint assembleDebug

Install provider/build/outputs/apk/debug/provider-debug.apk and sign in with an AT Protocol handle, a did:plc, or a hostname-level did:web. Android may ask you to enable the provider in Credential Manager settings before another app can request it.

See Adding support to an AT Protocol app for the consumer-side code.

Remove an account #

Open Atmosphere accounts and tap Remove from this device, or remove the account from Android's account settings. Either path removes the Android account and deletes the provider's local OAuth tokens and DPoP key.

Removing the device account does not sign out apps that already received their own OAuth grants, and it does not delete the AT Protocol account or any PDS data. The provider does not call the PDS revocation endpoint during removal. Deleting the DPoP key means the provider can no longer use that grant, though the PDS may retain the grant record until it expires or the user revokes it there.

Security boundary #

  • AT Protocol OAuth uses PAR, PKCE, and DPoP with server-issued nonces.
  • The token response sub must match the DID resolved before authorization.
  • Pending OAuth state and provider sessions are AES-GCM encrypted with a non-exportable Android Keystore key.
  • Each OAuth session has a separate non-exportable P-256 DPoP key.
  • AccountManager and the custom credential carry public identity metadata only. They never contain OAuth tokens.
  • Selecting a device account does not authorize PDS access. The consuming app must run its own OAuth flow and request its own scopes.
  • The optional same-device handler accepts only the exact enrolled PDS or authorization-server origin. It uses the provider's DPoP-bound session to authenticate the browser sign-in. The PDS then asks the user to approve the app's scopes and issues a separate grant.

The provider stores its initial OAuth session as encrypted device state. This session never leaves the provider. The optional same-device handler refreshes it when needed, but only while talking to the matching account server.

The passwordless handler uses the experimental /oauth/device-approval/local PDS endpoint from this prototype. It is not part of Android Credential Manager or the current AT Protocol OAuth specification. Without that PDS extension, Credential Manager still solves account discovery, but the user completes the PDS's normal authentication and consent screens.

Status #

This is a research prototype without a professional security audit. The repository does not distribute a compiled APK.

License #

The provider and repository-level project files use the GNU Affero General Public License v3.0 or later. The credential contract, sample app, and integration guide use the Apache License 2.0 so other apps can adopt the contract without adopting the provider's license. See the license map for the exact paths.