Skip to content

Authentication

Every sign-in method resolves to an AuthState — it never throws across the public surface. Check the returned (or streamed) state rather than wrapping calls in try/catch. See Errors for the one exception (programmer errors).

All of the flows below operate on the RakomiAuth instance for the enclosing RakomiProvider, reached via RakomiProvider.of(context) (throws if there is no enclosing provider) or RakomiProvider.maybeOf(context) (nullable).

final auth = RakomiProvider.of(context);
final state = await auth.signIn(email: email, password: password);
// or
final state = await auth.signUp(email: email, password: password);

Both resolve to AuthState.authenticated(user, session) on success. A primary-login response that requires a second factor resolves to AuthState.error(SdkError(code: mfaStepUpRequired, …)) instead — see MFA (TOTP) step-up below.

await auth.signInWithMagicLink(email); // sends a sign-in link to `email`
await auth.signInWithEmailOtp(email); // sends a one-time code to `email`

Both requests only trigger the send — they do not themselves resolve to an authenticated state. The user completes the flow by following the emailed link or submitting the code through your own UI against the API’s canonical passwordless-completion endpoint.

Use the pre-built RakomiSignInButton / RakomiSignInForm widgets, or drive the flow programmatically with the top-level performSocialSignIn function:

import 'package:rakomi_flutter/rakomi_flutter.dart';
final state = await performSocialSignIn(
auth,
SocialProvider.google,
redirectUri: 'myapp://callback',
);

RakomiAuth.signInWithProvider(provider) (the method on the controller itself) is intentionally unimplemented — it throws UnsupportedError, because the OAuth flow needs a redirectUri that the controller does not own. Always go through performSocialSignIn, or through RakomiSignInButton / RakomiSignInForm, which read redirectUri from the enclosing RakomiProvider.

Supported providers (SocialProvider enum): google, github, microsoft, apple, discord, facebook, slack, twitter, gitlab, linkedin. An eudiWallet value is reserved as a forward-compatibility slot for the EU Digital Identity Wallet mandate and is not wired to a live flow today — switch statements over SocialProvider should stay non-exhaustive (use a default: case) so this and future additive values do not break your build.

The flow itself is standard Authorization Code + PKCE (RFC 8252): the SDK opens the system browser (never an in-app WebView), generates and persists a PKCE verifier/challenge and a CSRF state value, and on the redirect callback validates state (constant-time), the optional iss parameter (RFC 9700 §4.4 mix-up defense against the configured issuer), then exchanges the authorization code for tokens.

Redirect-URI scheme rules (enforced by performSocialSignIn before any network call):

  • On iOS/Android: redirectUri must use a custom URL scheme. http, https, javascript, data, file, vbscript, about are all rejected with an ArgumentError.
  • On Flutter Web: https:// is accepted (popup postMessage flow). The always-banned scheme list above still applies.

When a primary sign-in response carries next_action: "verify_mfa", signIn/signUp resolve to AuthState.error(SdkError(code: SdkErrorCode.mfaStepUpRequired, errorMessage: challengeId)). Complete the challenge with the 6-digit TOTP code:

final state = await auth.verifyMfaImpl(code, challengeId: challengeId);

RakomiAuth.verifyMfa(code) (without a challengeId) is, like signInWithProvider, intentionally unimplemented — it throws UnsupportedError pointing you at verifyMfaImpl (the extension method) or the pre-built RakomiSignInForm, which already wires this transition.

The code is validated client-side against ^\d{6}$ before any network call. A user with no TOTP factor configured gets back SdkErrorCode.mfaStepUpUnavailable.

await auth.signOut();

signOut() revokes the refresh token server-side (best-effort), then clears all locally persisted state for the current tenant — the refresh token, cached user snapshot, and the tenant’s JWKS cache — and emits AuthEventType.signedOut. Storage keys are tenant-namespaced, so signing out of one RakomiProvider does not disturb a sibling provider for a different tenant in the same widget tree.

final unlocked = await auth.unlockWithBiometric(reason: 'Unlock Rakomi');

Triggers the platform biometric prompt via the native adapter and, on success, re-runs session hydration. Returns true iff the resulting state is AuthStateAuthenticated. A failure (user cancellation, lockout, no enrolled biometric, or hardware unavailable) resolves to AuthState.error(SdkError(code: SdkErrorCode.biometricError, …)) with a reason identifying which case occurred — see Errors.

  • RakomiSignInForm — password + passwordless + social + MFA-TOTP in one widget, Material on Android/Web and Cupertino on iOS. Password and TOTP fields ship with input hardening (obscureText, disabled interactive selection/autocorrect/suggestions, autofillHints).
  • RakomiSignInButton — a single one-tap social sign-in CTA for one SocialProvider.
  • RakomiAuthGate — conditional renderer keyed off authStateChanges; supply signedIn and signedOut builders, with optional loading and error builders.
  • RakomiUserAvatar — avatar + tooltip (email, session expiry) with a built-in sign-out menu.
auth.authStateChanges.listen((state) {
state.when(
loading: () { /* ... */ },
unauthenticated: () { /* ... */ },
authenticated: (user, session) { /* ... */ },
error: (error) { /* ... */ },
);
});
auth.events.listen((event) {
// AuthEvent(type, severity, timestamp, ...) — an append-only lifecycle log,
// distinct from the current-snapshot `authStateChanges` stream.
});

events is a consumer-pull-only stream — the SDK does not forward events to any sink on its own. See Session & tokens for the audit-logging guidance.