PlatformDPoPKey class

The app's DPoP key (RFC 9449) in the safest place the platform has, kept across restarts: the provider binds the tokens to it, so a token stolen from the app is useless without the device.

With DPoP (a registration's dpop property; auto by default, on when the provider announces it) every token request and API call carries a proof signed by this key. The OAuth2 client's default key lives only as long as the app runs, so after a restart the stored tokens would be dead; this one survives, and restore() keeps the user logged in. The package's auto-configuration declares it as the app's DPoPKey bean (platformDPoPKey) wherever a registration is configured, except on the web; the token clients, the user services and the Feign interceptor use it. Most apps never touch it.

Where the key lives:

  • iOS, macOS: an EC P-256 key made in the Secure Enclave, where it never leaves the chip (kSecAttrTokenIDSecureEnclave, permanent, tagged <bundle id>.spring_dpop_key, usable after the first unlock, on this device only). Where there is no Secure Enclave (the iOS simulator, Macs without one) or it cannot be used (a macOS build without the data protection keychain's entitlements), a software key of the Keychain with the same tag: the app's code never handles it, but the system's Security framework loads it into the app's process to sign, so it is as safe as the app's memory.
  • Android: an EC P-256 key of the Android Keystore (PURPOSE_SIGN, SHA-256), in the StrongBox chip when the phone has one, else in the trusted execution environment.
  • Windows, Linux: a key made in Dart, kept as a private JWK in the SecretStore (oauth2.dpop-key: DPAPI, the Secret Service), signing in the app's memory (SecretStoreDPoPKey).
  • Web: none (the constructor throws); a page logs in on its server, which holds the tokens.

storage tells which, after the first proof.

The key is made at its first use, not at startup. When it disappears (the Keystore wiped, the app reinstalled on Android, the app restored on another device: the key never leaves the device), the next proof makes a new key; the stored tokens are bound to the old one, so they are dropped (SecretStoreOAuth2AuthorizedClientService checks the key's thumbprint) and the user logs in again. A Keychain key outlives the app on iOS (deleting the app keeps it) and is in the device's encrypted backups (restored only to the same device); the package removes it, with the secret store's values, at the first start after a reinstall (the install id in the app's container is gone while the Keychain kept its items), so a reinstalled app starts logged out (not one deleted under 0.4.x, which kept no id).

OAuth2LoginService.logout() deletes the key (delete), so the next login gets a new one: RFC 9449 lets a key live across logins, but a new key per login means nothing bound to the old login works any more, and two logins of the device cannot be linked by their key. SecretStore.deleteAll() removes the desktop key (it is a value of the store), not the Secure Enclave's or the Keystore's key.

// Where did the key end up? (After the first proof.)
final key = context.getBean<PlatformDPoPKey>();
log.info('DPoP key in ${key.storage?.name}');

Spring has no such type (Spring Security has no DPoP on the client's side, and Java has no apps).

Implemented types

Constructors

PlatformDPoPKey(SecretStore secretStore)
The key of the current platform; on Windows and Linux kept in secretStore.

Properties

algorithm → String
ES256.
no setteroverride
hashCode → int
The hash code for this object.
no setterinherited
persistent → bool
Kept outside the program, so tokens bound to it survive a restart.
no setteroverride
publicJwk → Map<String, Object?>
The public key as a JWK (kty, crv, x, y); throws a StateError before load.
no setteroverride
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited
storage → DPoPKeyStorage?
Where the key is, once loaded; null before.
no setter

Methods

delete() → Future<void>
Removes the key: tokens bound to it can no longer be used, and the next proof makes a new key. OAuth2LoginService.logout() calls it.
load() → Future<void>
Makes the key, or reads the one made earlier. DPoPProofBuilder calls it before every proof; only the first call asks the platform.
override
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
sign(Uint8List input) → Future<Uint8List>
The raw R || S signature of input (the platform's DER signature converted).
override
toString() → String
The algorithm, the public thumbprint once loaded and the storage; never the private key.
override

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Properties

isSupported → bool
Whether the current platform has a place for the key: false on the web and on Fuchsia.
no setter