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;
nullbefore.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.
DPoPProofBuildercalls 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 || Ssignature ofinput(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:
falseon the web and on Fuchsia.no setter