oidc 4.0.0
oidc: ^4.0.0 copied to clipboard
A comprehensive OpenIdConnect plugin that works on all platforms (android, ios, windows, linux, web, macos)
oidc #
An OpenId Connect RP (Relying Party) plugin for flutter.
Make sure you read the for extra information.
Table Of Contents #
Introduction ✨ #
This federated plugin builds on top of to add platform-specific handling which is required by the spec (e.g. launching a browser, listening for redirect, etc...).
Installation 💻 #
❗ In order to start using this plugin you must have the Flutter SDK installed on your machine.
Add to your pubspec.yaml:
dart pub add oidc oidc_default_store
Usage 🛠️ #
After following the Getting Started steps, it's as easy as:
//1. create the manager:
final manager = OidcUserManager.lazy(
discoveryDocumentUri: OidcUtils.getOpenIdConfigWellKnownUri(
Uri.parse('https://server.example.com'),
),
// TODO: add other settings
);
//2. init()
await manager.init();
//3. listen to user changes
manager.userChanges().listen((user) {
print('currentUser changed to $user');
});
//4. login
final newUser = await manager.loginAuthorizationCodeFlow();
//5. logout
await manager.logout();
Features 📚 #
- 🧩 Cross platform: most features work on all platforms that can run flutter (Android, Ios, macos, web, windows, linux).
- 🧰 High maintenance: everyone hates having to fix an unmaintained package. you can trust that we will solve issues as soon as they pop up. especially since we use this package in all our production apps.
- ⚙️ Customizability: you can customize everything; Where to store the data, provide your own http client, extend requests/responses with your own data; whatever you want, you can do.
- 🚀 Easy to use: you mainly need to concern yourself with the
OidcUserManagerclass, which is very well documented and has a simple interface.
📜 Conformance #
- this package is an OpenID Connect Certified Relying Party, and is additionally tested against multiple conformance profiles.
Implemented specs #
- OpenId Connect Core 1.0.
- authorization code, hybrid (
loginHybridFlow), and implicit flows.
- authorization code, hybrid (
- OpenId Connect Discovery.
- §4 Provider Configuration: the
.well-known/openid-configurationdocument (also RFC 8414.well-known/oauth-authorization-server), withissuervalidation and RFC 8414signed_metadataverification. - §2 OpenID Provider Issuer Discovery: WebFinger via
OidcEndpoints.getIssuerViaWebFinger('joe@example.com'), including §2.1 identifier normalization (OidcUtils.normalizeWebFingerIdentifier), HTTPS-only transport with https-only redirects, and client-siderelfiltering. Resolve the identifier to an issuer first, then pass that issuer toOidcUserManager— the manager does not run WebFinger on your behalf. Internationalized host names are not supported: convert them to their punycode A-label (RFC 5891) before calling, or normalization throws.
- §4 Provider Configuration: the
- RP Initiated logout.
- Front-Channel Logout.
- Authorization code grant with PKCE.
- Resource Owner Password Credentials Grant.
- Automatic Refresh Token rotation.
- OAuth 2.0 For Native Apps
- OAuth 2.0 Device Authorization Grant
- Session Management (Web only)
- Dynamic Client Registration (RFC 7591).
- Set
OidcUserManagerSettings.dynamicClientRegistrationandinit()registers the client at the discovery document'sregistration_endpoint(optionally with an initial access token), then runs as the ISSUED credentials — theclientCredentialsyou pass to the constructor become a pre-registration seed. The issuedtoken_endpoint_auth_methodis what every back-channel call then uses, and it cannot be overridden — OpenID Connect Core §3.1.3.1 and §12.1 both require a client to "authenticate to the Token Endpoint using the authentication method registered for itsclient_id".preferredTokenEndpointAuthMethoddecides only when the OP states none, where RFC 7591 §2 would otherwise pickclient_secret_basicunaided. - The manager registers once per issuer and then leaves it alone. One immutable record is persisted under one key; every later launch restores it with zero network calls. That record is re-issued only when the request this build would send no longer matches the one it was issued for, or when its
client_secrethas expired. Nothing else in the library ever re-derives, rotates or replaces the client identity — see the non-goals below, which name theOidcEndpointscall you make instead. - Public clients are supported. RFC 7591 §3.2.1 makes
client_secretOPTIONAL, so an OP that issues aclient_idalone (and states notoken_endpoint_auth_method) yields a public client authenticating withnone— §2'sclient_secret_basicdefault only applies when a secret WAS issued. - The manager fills the metadata it can derive from your settings when your
buildRequestleaves it null:redirect_uris,scope,post_logout_redirect_uris,grant_types/response_types(which default toauthorization_code+refresh_token/code, because RFC 7591 §2 would otherwise register the client forauthorization_codealone and break the automatic refresh), andapplication_type. Flows you opt into must be declared: addOidcConstants_GrantType.deviceCodefor the device-code flow, orimplicitplus the matchingresponseTypesfor implicit/hybrid. application_typeis derived from your redirect uris, not from the platform. OpenID Connect Registration §2 states it as a client-side rule: anativeclient "MUST only registerredirect_urisusing custom URI schemes or loopback URLs using thehttpscheme", and awebclient MUST usehttpsand MUST NOT uselocalhost. So an Android or iOS app whose redirect is an App Link / Universal Link (https://app.example.com/callback) registers asweb— declaring itnativebecause it runs on a phone authors a request a conformant OP must reject. SetapplicationTypein yourbuildRequestto override.- RFC 7591 §3.2.1 lets the OP substitute its own values; the
scope,redirect_urisandpost_logout_redirect_urisit returns — not the ones that were requested — are what later authorization / token / end-session requests use. Theclient_idandtoken_endpoint_auth_methodfrom the response likewise become the credentials every back-channel call presents. - Loopback redirects are compared without their port (RFC 8252 §7.3: "the authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs"). A desktop app whose listener binds an ephemeral port therefore matches its stored registration on every launch instead of minting a new OP-side client each run, and its authorization request keeps the live port even when the OP registered a different one. Only loopback
httpis normalized; every other uri, includinghttps, compares byte-for-byte (RFC 3986 §6.2.1). - Web is always registered as a PUBLIC client. A browser app has nowhere to keep a secret (OAuth 2.0 for Browser-Based Apps §6.1): the registration lives in
OidcStoreNamespace.secureTokens, which on web is browser storage (localStorageviashared_preferenceswhen noFlutterSecureStorageis passed toOidcDefaultStore), readable by any injected script. So on web the manager asks fortoken_endpoint_auth_method: "none"unless yourbuildRequestsets one — RFC 7591 §2 would otherwise default the client to the confidentialclient_secret_basic— and refuses a response that issues aclient_secretanyway, with a typedOidcExceptionthrown before anything is written. A provider that insists on issuing one is telling you the client belongs on a backend. The same refusal applies to a persisted registration carrying a secret, so a client registered on another platform is never replayed in a browser. - The record is persisted per-issuer in the secure store and re-used on every later launch — including across
forgetUser(), since it is app-instance identity, not user identity. Staleness is measured against the request that was actually registered (after the defaults above are applied), not against your settings alone, so a change made insidebuildRequestcounts too: the fingerprint coversredirect_uris,post_logout_redirect_uris,scope,grant_types,response_typesandapplication_type. It deliberately ignoressoftware_statement,jwksand every purely descriptive member (client_name,logo_uri, …) — those are re-signed or edited on their own schedule, and RFC 7592 §2.2 update is their remedy, not orphaning the OP-side client for a fresh one. - Nothing is ever deleted to make room for something that does not exist yet. There is exactly one write of the registration key and exactly one removal —
forgetClientRegistration(), which you call. A record that cannot be read (a locked keychain, an Android key invalidated by a biometric re-enrolment), cannot be parsed, or cannot be converted into credentials is treated as a cache miss and left exactly where it is; the replacement overwrites it in place once it has been issued and persisted. So an app update that changes the fingerprint and then launches offline keeps running as the previously-issued client — theclient_idand the RFC 7592registration_access_tokensurvive — instead of being stranded with nothing. The write comes before the in-memory swap for the same reason: RFC 7591 registration is not idempotent, so a registration that could not be persisted is reported as a typedOidcExceptionrather than silently minting another OP-side client on every launch. - Non-goals, each with the clause that makes it optional and the call you make instead. All of them are reachable today: the full RFC 7592 client-management surface already ships as
OidcEndpoints.registerClient/readClientConfiguration/updateClientConfiguration/deleteClientConfiguration, andOidcUserManagerBase.clientRegistrationhands you theregistration_client_uriandregistration_access_tokenthey need.- No automatic
client_secretrotation. RFC 7592 App. A.1: "the authorization server decides the frequency of the credential rotation and not the client" — the §2.1 read is an OP-driven affordance, not a client obligation, and an OP that conformantly returns the registration verbatim leaves a client-driven rotation loop with nothing to make progress on. Against an OP that does rotate on a schedule (Zitadel, Keycloak, Okta can be configured to) the session breaks at expiry with a typedOidcException; writecatch → OidcEndpoints.readClientConfiguration(...), orcatch → forgetClientRegistration() → build a new manager and init(). The cost of the latter is a re-login and one orphaned OP-side client. - No automatic re-registration when the provider disowns the client. RFC 6749 §5.2 makes
invalid_clientthree-way ambiguous ("unknown client, no client authentication included, or unsupported authentication method") and OpenID Connect Registration §4.4 forbids the 404 that would disambiguate it, so there is no reliable trigger. The rejection is surfaced to you untouched — nothing is re-registered, nothing is deleted — and the sameforgetClientRegistration()handler applies. 0 of 7 mature RP libraries automate this. - No RFC 7592
PUT/DELETEfrom the manager, and no orphan tracking or reaping. RFC 7591 §5 assigns cleanup of registered-but-unused clients to the authorization server. Orphaned OP-side clients therefore accumulate untracked: one per cache miss, fingerprint change, expired-secret restore, storage fault, cross-manager race and web page load. Nothing counts, caps or reaps them, so an OP with a per-tenant client quota will eventually reject registrations with no hint that quota is the cause. CallOidcEndpoints.deleteClientConfiguration(§2.3) beforeforgetClientRegistration()if you want the OP-side client retired, andupdateClientConfiguration(§2.2) to edit one in place. - No cross-manager, cross-tab or cross-process coordination.
OidcStorehas no compare-and-set, so nothing can be promised: two managers, two browser tabs, or twoOidcStoreobjects over one backing store each mint a client and the last writer wins the key. Give managers that are meant to be independent distinctmanagerIds. - No detection of a
client_idthe OP rotated on its own, and no recovery from a POST whose response was lost in transit (that mints a client no design on any transport can ever name). private_key_jwtand mutual-TLS registrations are not automated, because their credentials cannot be derived from a registration response — buildOidcClientAuthentication.privateKeyJwtGenerated(or the mTLS variants) yourself and register the client out of band.
- No automatic
- Why the non-goals are stated as non-goals rather than gaps: every one of them needs durable control state — a retry budget, an attempt counter, a rejection verdict, a generation number, a ledger of minted clients — and every such measure can be destroyed or reset by the very action it gates. There is none here. The store holds exactly one immutable record per issuer, containing only the OP's response and a digest of the request it was issued for, and
packages/oidc_core/test/dcr_invariants_test.dartcounts that mechanically: one write call site, one removal call site, one read call site, in three different methods, with no read-modify-write anywhere.
- Set
Plugin Generated by the Very Good CLI 🤖
