Guides

Deep Links

Wire OAuth2CallbackHandler to receive the browser redirect for OAuth2Provider and its prebuilt subclasses.

Any OAuth2Provider (or a prebuilt subclass: GoogleProvider, GitHubProvider) opens an external browser for the authorization step, then needs the resulting redirect routed back into your app. OAuth2CallbackHandler is the dispatcher that does that routing.

Two providers are the exception and need no OAuth2CallbackHandler wiring at all:

  • AppleProvider (authyra_apple), the native SDK resolves the sign-in directly, no deep link involved. See Apple Provider →.
  • ProxyOAuthProvider, it manages its own deep-link routing via handleDeepLink. See ProxyOAuthProvider →.

1. Wire the dispatcher, once

No per-provider registration step exists. Each OAuth2Provider.signIn() call registers its own CSRF state with OAuth2CallbackHandler for the duration of the attempt and unregisters it when it settles. You only wire the dispatcher itself, before Authyra.initialize():

import 'package:app_links/app_links.dart';

AppLinks().uriLinkStream.listen(OAuth2CallbackHandler.handleCallback);

Or forward manually from wherever your app already sees the URI (a custom onGenerateRoute, a GoRouter redirect):

final incoming = Uri.parse(link);
OAuth2CallbackHandler.handleCallback(incoming);

handleCallback reads the state query parameter (falling back to the URI fragment for implicit-flow redirects), routes it to the matching pending sign-in, and logs a warning (does not throw) if state is missing or matches no pending attempt.

Routing by state rather than by URI scheme means two OAuth2Providers can safely share the same redirect scheme (e.g. myapp://auth/callback for both GitHub and a custom Discord provider). state is a fresh, cryptographically random value per sign-in attempt, so there is no collision risk to manage yourself.

2. Initialize Authyra

lib/main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  AppLinks().uriLinkStream.listen(OAuth2CallbackHandler.handleCallback);

  await Authyra.initialize(
    client: AuthyraClient(
      providers: [discordProvider],
      storage: SecureAuthStorage(),
    ),
  );

  runApp(const MyApp());
}

Platform setup

Register the custom URI scheme with each platform, independent of Authyra:

Android, android/app/src/main/AndroidManifest.xml:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" android:host="auth" android:pathPrefix="/callback" />
</intent-filter>

iOS, ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

Cleanup

Each OAuth2Provider.signIn() call unregisters its own pending state once it settles, there is nothing to clean up manually in application code. In tests, clear every pending registration between cases:

tearDown(OAuth2CallbackHandler.clearAll);

Troubleshooting

See Troubleshooting → OAuth2 callback never arrives for the full checklist (scheme mismatch, missing registration, redirect URI mismatch, timeout).

Copyright © 2026