diene_auth_engine 1.0.2 copy "diene_auth_engine: ^1.0.2" to clipboard
diene_auth_engine: ^1.0.2 copied to clipboard

Logto auth flows, per-resource tokens, claims-first per-backend onboarding, deferred login, and Doc B landscape selection for Flutter applications.

example/diene_auth_engine_example.dart

import 'package:diene_auth_engine/diene_auth_engine.dart';
import 'package:diene_auth_engine/test_helper.dart';
import 'package:diene_result/diene_result.dart';

/// The engine-owned `authEngine` config block, as the `config` lib would hand
/// it over after merging YAML layers and `--dart-define` overrides. The OIDC
/// `issuer` is BAKED build-time config — it never arrives from an edge doc.
final AuthEngineConfig config = AuthEngineConfig.fromBlock(<String, Object?>{
  'issuer': 'https://api.lithium.lapras.cluster.atomi.cloud',
  'endpoint': 'https://logto.lithium.lapras.cluster.atomi.cloud',
  'appId': 'example-app',
  'redirectUri': 'https://app.lithium.lapras.cluster.atomi.cloud/callback',
  'scopes': <String>['openid', 'offline_access'],
});

/// The one backend this app talks to. `resourceName` occupies the M slot of the
/// LPSM coordinate, so the JWT `aud` is
/// `https://root.api.lithium.lapras.cluster.atomi.cloud`.
final ResourceKey apiResource = ResourceKey(
  platform: 'lithium',
  landscape: 'lapras',
  service: 'api',
  resourceName: 'root',
);

/// Redeems a store carrier into the `signIn(extraParams:)` payload. A missing
/// or expired carrier is not an error — it simply means "log in normally".
Future<Map<String, String>> deferredLoginParams(
  AppHandoffApi api,
  InstallReferrerSource referrer,
) async {
  final DeferredLoginClient client = DeferredLoginClient(
    api: api,
    device: const DeviceInfo(platform: 'android', appVersion: '1.0.0'),
    referrer: referrer,
  );
  final DeferredLoginOutcome outcome = await client.prepare();
  return switch (outcome) {
    DeferredLoginReady(:final Map<String, String> extraParams) => extraParams,
    DeferredLoginFallback() => const <String, String>{},
  };
}

Future<void> main() async {
  final DateTime now = DateTime.utc(2026, 7, 26);
  const String nonce = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';

  // --- Deferred login: read the carrier, redeem it, get the extra params ----
  final FakeInstallReferrerSource referrer = FakeInstallReferrerSource(
    'utm_source=play&app_handoff=atomi-app-handoff%3Av1%3A$nonce',
  );
  final FakeAppHandoffApi handoff = FakeAppHandoffApi(
    result: const Ok<RedeemResult>(
      RedeemResult(token: 'one-time', email: 'user@example.com'),
    ),
  );
  final Map<String, String> extraParams = await deferredLoginParams(
    handoff,
    referrer,
  );
  assert(extraParams['one_time_token'] == 'one-time', 'carrier redeemed');
  assert(referrer.processed, 'the referrer is marked processed before redeem');

  // --- The IdP seam --------------------------------------------------------
  // A JWT carrying the exact `lithium_api = "true"` registration claim plus the
  // authoritative `home_landscape` claim.
  final String registered = AuthFixtures.registeredJwt(
    apiResource,
    extra: <String, Object?>{Claims.homeLandscape: 'lapras'},
  );
  final FakeAuthProvider provider = FakeAuthProvider(
    onSignIn: () => AuthFixtures.sessionTokens(now: now),
    resourceTokens: <String, ResourceToken>{
      apiResource.mapKey: AuthFixtures.resourceToken(
        now: now,
        jwtToken: registered,
      ),
    },
    idTokenValue: registered,
  );

  // --- Session lifecycle (C0 §12: 10-minute access, 14-day rotating refresh)
  final SessionController session = SessionController(
    provider: provider,
    now: () => now,
  );
  final SessionTokens tokens = AuthExpect.ok(
    await session.signIn(extraParams: extraParams),
  );
  assert(tokens.accessValidAt(now), 'the issued access token is live');
  assert(session.status == SessionStatus.authenticated, 'session is up');

  // --- Per-resource tokens through the IAuth seam --------------------------
  final IAuth auth = AuthCoordinator(provider: provider, now: () => now);
  final ResourceToken apiToken = AuthExpect.ok(
    await auth.tokenFor(apiResource),
  );
  assert(apiToken.token == registered, 'the resource token is cached per key');

  // --- Per-backend claims-first onboarding (C0 §8) -------------------------
  final BackendRegistry registry = BackendRegistry(<RegisteredBackend>[
    RegisteredBackend(
      backendId: 'lithium-api',
      resources: <ResourceKey>[apiResource],
      onboardingResource: apiResource,
    ),
  ]);
  final MultiBackendOnboarding onboarding = MultiBackendOnboarding(
    registry: registry,
    auth: auth,
    directory: FakeUserDirectory(),
    idToken: provider.idToken,
  );
  final Map<String, Result<OnboardingPhase>> phases = await onboarding.runAll();
  AuthExpect.phase(
    AuthExpect.ok(phases['lithium-api']!),
    OnboardingPhase.ready,
  );

  // --- Home landscape: the JWT claim decides, Doc B only when it is absent -
  final HomeClaimResolver homeResolver = HomeClaimResolver(
    claimReader: jwtHomeClaimReader(provider.idToken),
    selector: LandscapeSelectorClient(
      source: FakeLandscapeSelectorSource(
        doc: const LandscapeSelectorDoc(
          platform: 'lithium',
          tier: 'prod',
          landscapes: <LandscapeEntry>[
            LandscapeEntry(name: 'lapras', region: 'sgp'),
          ],
        ),
      ),
      pinger: FakeRegionPinger(<String, Duration?>{
        'lapras': const Duration(milliseconds: 12),
      }),
    ),
    store: MemoryHomeClaimStore(),
  );
  final HomeResolution home = AuthExpect.ok(await homeResolver.resolve());
  assert(home.landscape == 'lapras', 'the claim decided the home landscape');
  assert(home.kind == HomeResolutionKind.fromClaim, 'Doc B never ran');

  // --- returnTo: resume the exact protected route after login --------------
  final Uri target = Uri.parse('/orders/42?tab=history');
  final Uri redirect = AuthExpect.ok(
    ReturnTo.buildLoginRedirect(Uri.parse('/login'), target),
  );
  assert(
    redirect.queryParameters[ReturnTo.queryKey] == '/orders/42?tab=history',
    'path AND query survive the round trip',
  );
  final Uri resumed = ReturnTo.continueFrom(redirect, fallback: Uri.parse('/'));
  assert(resumed.toString() == target.toString(), 'resumed the exact route');

  // An absolute target is an open redirect and is refused, not followed.
  AuthExpect.errType(
    ReturnTo.capture(Uri.parse('https://evil.example/steal')),
    'urn:diene:problem:return-to',
  );

  // --- The whole flow in one call ------------------------------------------
  final SignInResult result = AuthExpect.ok(
    await SignInCoordinator(
      session: session,
      homeResolver: homeResolver,
      onboarding: onboarding,
    ).signIn(extraParams: extraParams, returnTo: target),
  );
  assert(result.home.landscape == 'lapras', 'routed to the claimed home');
  assert(result.continueTo == target, 'the deeplink target is preserved');
  assert(config.redeemPath == '/app-handoff/redeem', 'no doubled mount path');
}
0
likes
160
points
615
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Logto auth flows, per-resource tokens, claims-first per-backend onboarding, deferred login, and Doc B landscape selection for Flutter applications.

Repository (GitHub)
View/report issues

Topics

#authentication #logto #oidc #onboarding #flutter

License

MIT (license)

Dependencies

diene_problems, diene_result, flutter, http, logto_dart_sdk, meta

More

Packages that depend on diene_auth_engine