oidc 5.0.2 copy "oidc: ^5.0.2" to clipboard
oidc: ^5.0.2 copied to clipboard

A comprehensive OpenIdConnect plugin that works on all platforms (android, ios, windows, linux, web, macos)

oidc #

openid certification

Pub Version last commit codecov style: very good analysis License: MIT

An OpenId Connect RP (Relying Party) plugin for flutter.

Make sure you read the Wiki for extra information.

Table Of Contents #

Introduction ✨ #

This federated plugin builds on top of oidc_core 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 OidcUserManager class, which is very well documented and has a simple interface.

📜 Conformance #

Implemented specs #

  • OpenId Connect Core 1.0.
    • authorization code, hybrid (loginHybridFlow), and implicit flows.
  • OpenId Connect Discovery.
    • §4 Provider Configuration: the .well-known/openid-configuration document (also RFC 8414 .well-known/oauth-authorization-server), with issuer validation and RFC 8414 signed_metadata verification.
    • §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-side rel filtering. Resolve the identifier to an issuer first, then pass that issuer to OidcUserManager — 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.
  • 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).
    • Web only, by design — not a gap that will be filled on native platforms. The RP-side check embeds check_session_iframe in a frame the RP controls, posts "$clientId $sessionState" into it, and reads back the OP's answer; that answer is computed from the OP session cookie held by whichever browser is loading the iframe. RFC 8252 §8.12 requires native apps (Android, iOS, macOS, Linux, Windows) to perform login in an external user-agent, not an embedded WebView, so there is no app-controlled browser surface left to host that frame in. Even granting a SEPARATE, app-controlled WebView purely for session polling would not help: OpenID Connect Session Management 1.0 §3.2 ties the answer to the cookie in the user-agent that did the §8.12 login, which a second, freshly-created WebView never has — it could only ever answer "changed", which is not a meaningful check.
    • On native platforms, detect a session that ended at the OP through other signals instead: OidcTokenRefreshFailedEvent (a terminal automatic-refresh failure, e.g. invalid_grant, surfaces the next time the token is refreshed), OidcUserInfoFailedEvent (a 401 from the userinfo endpoint, surfaces the next time it's called) — both on manager.events() — Front-Channel Logout or Back-Channel Logout if your OP can push the logout to the app, or OpenID Connect Native SSO for Mobile Apps if multiple apps from the same vendor need to share a signed-out state. None of these are instant like check_session_iframe polling — they fire on the next token use, not the moment the OP session ends.
  • Dynamic Client Registration (RFC 7591).
    • Set OidcUserManagerSettings.dynamicClientRegistration and init() registers the client at the discovery document's registration_endpoint (optionally with an initial access token), then runs as the ISSUED credentials — the clientCredentials you pass to the constructor become a pre-registration seed. The issued token_endpoint_auth_method is 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 its client_id". preferredTokenEndpointAuthMethod decides only when the OP states none, where RFC 7591 §2 would otherwise pick client_secret_basic unaided.
    • 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_secret has expired. Nothing else in the library ever re-derives, rotates or replaces the client identity on its own — the only other way it moves is an RFC 7592 call you make through the manager (below). See the non-goals further down for what is deliberately left to you.
    • RFC 7592 management of the manager's own client, with rotation. manager.updateClientRegistration(edit: (req) => req..clientName = 'New name') sends the RFC 7592 §2.2 PUT — starting from every member the OP previously returned (§2.2: values replace, not augment), with your edit applied — and manager.readClientRegistration() sends the §2.1 GET. Both adopt the response as the manager's identity and persist it: §2.2 says that when the response carries a new client_secret and/or registration_access_token "the client MUST immediately discard its previous" ones, so the next back-channel call already presents the rotated secret and the next launch restores it instead of the retired one. Members the response leaves out (registration_access_token, registration_client_uri, and client_secret unless the client became public) are carried over rather than dropped. The record keeps the fingerprint it was issued for, so an update never triggers a re-registration. A response for a different client_id is refused, and calls are serialized so a second call presents the token the first one rotated in. If the store cannot take the write, the manager still switches to the rotated credentials (the old ones are dead) and throws a typed OidcException. Calling OidcEndpoints.updateClientConfiguration directly still works, but the manager and the store never see what it rotates.
    • Where the registration is stored, and why there is no new OidcStoreNamespace. The record (including registration_access_token / registration_client_uri) lives in OidcStoreNamespace.secureTokens under the key client_registration.<issuer>, so it gets the same secure-storage guarantees as the tokens. A dedicated enum value was considered and rejected: OidcStoreNamespace is an enum that every third-party OidcStore switches over or maps to a storage location, so adding a value is a breaking change for those implementations, and the new namespace would have needed exactly the guarantees secureTokens already has. A key prefix in secureTokens is non-breaking, and forgetUser() spares that prefix because the registration identifies the app instance, not the user.
    • Registered out of band instead? If you call OidcEndpoints.registerClient yourself (or a backend hands you the response), leave dynamicClientRegistration unset and pass clientCredentials: OidcClientAuthentication.fromRegistrationResponse(response) to the manager's constructor. Keeping that response, and its RFC 7592 credentials, is then up to you.
    • Public clients are supported. RFC 7591 §3.2.1 makes client_secret OPTIONAL, so an OP that issues a client_id alone (and states no token_endpoint_auth_method) yields a public client authenticating with none — §2's client_secret_basic default only applies when a secret WAS issued.
    • The manager fills the metadata it can derive from your settings when your buildRequest leaves it null: redirect_uris, scope, post_logout_redirect_uris, grant_types / response_types (which default to authorization_code + refresh_token / code, because RFC 7591 §2 would otherwise register the client for authorization_code alone and break the automatic refresh), and application_type. Flows you opt into must be declared: add OidcConstants_GrantType.deviceCode for the device-code flow, or implicit plus the matching responseTypes for implicit/hybrid.
    • application_type is derived from your redirect uris, not from the platform. OpenID Connect Registration §2 states it as a client-side rule: a native client "MUST only register redirect_uris using custom URI schemes or loopback URLs using the http scheme", and a web client MUST use https and MUST NOT use localhost. So an Android or iOS app whose redirect is an App Link / Universal Link (https://app.example.com/callback) registers as web — declaring it native because it runs on a phone authors a request a conformant OP must reject. Set applicationType in your buildRequest to override.
    • RFC 7591 §3.2.1 lets the OP substitute its own values; the scope, redirect_uris and post_logout_redirect_uris it returns — not the ones that were requested — are what later authorization / token / end-session requests use. The client_id and token_endpoint_auth_method from 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 http is normalized; every other uri, including https, 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 (localStorage via shared_preferences when no FlutterSecureStorage is passed to OidcDefaultStore), readable by any injected script. So on web the manager asks for token_endpoint_auth_method: "none" unless your buildRequest sets one — RFC 7591 §2 would otherwise default the client to the confidential client_secret_basic — and refuses a response that issues a client_secret anyway, with a typed OidcException thrown 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 inside buildRequest counts too: the fingerprint covers redirect_uris, post_logout_redirect_uris, scope, grant_types, response_types and application_type. It deliberately ignores software_statement, jwks and 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 — the client_id and the RFC 7592 registration_access_token survive — 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 typed OidcException rather 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: updateClientRegistration() / readClientRegistration() on the manager, the full RFC 7592 client-management surface as OidcEndpoints.registerClient / readClientConfiguration / updateClientConfiguration / deleteClientConfiguration, and OidcUserManagerBase.clientRegistration, which hands you the registration_client_uri and registration_access_token they need.
      • No automatic client_secret rotation. 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 typed OidcException; write catch → manager.readClientRegistration() (which persists the rotated credentials), or catch → 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_client three-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 same forgetClientRegistration() handler applies. 0 of 7 mature RP libraries automate this.
      • No automatic RFC 7592 PUT/DELETE from 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. Call OidcEndpoints.deleteClientConfiguration (§2.3) before forgetClientRegistration() if you want the OP-side client retired, and manager.updateClientRegistration() (§2.2) to edit one in place.
      • No cross-manager, cross-tab or cross-process coordination. OidcStore has no compare-and-set, so nothing can be promised: two managers, two browser tabs, or two OidcStore objects over one backing store each mint a client and the last writer wins the key. Give managers that are meant to be independent distinct managerIds.
      • No detection of a client_id the 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_jwt registrations are not automated, because the signing key cannot be derived from a registration response — build OidcClientAuthentication.privateKeyJwtGenerated yourself and register the client out of band. Mutual-TLS registrations (tls_client_auth / self_signed_tls_client_auth, RFC 8705) are derived automatically, because they need only the client_id. The certificate comes from the cert-bearing httpClient (OidcMtls.createHttpClient), and on web the derived mTLS credentials are rejected with an UnsupportedError.
    • 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.dart counts that mechanically: one write call site, one removal call site, one read call site, in three different methods, with no read-modify-write anywhere.

Plugin Generated by the Very Good CLI 🤖

51
likes
155
points
25.8k
downloads

Documentation

API reference

Publisher

verified publisherbdaya-dev.com

Weekly Downloads

A comprehensive OpenIdConnect plugin that works on all platforms (android, ios, windows, linux, web, macos)

Homepage
Repository (GitHub)
View/report issues

Topics

#oidc #openidconnect #oauth #authentication

License

MIT (license)

Dependencies

flutter, jose_plus, oidc_android, oidc_core, oidc_darwin, oidc_linux, oidc_platform_interface, oidc_web, oidc_windows

More

Packages that depend on oidc

Packages that implement oidc