Providers

OAuth2 Provider

Generic OAuth 2.0 Authorization Code provider with PKCE for any identity provider.

OAuth2Provider implements the full browser-based Authorization Code flow with optional PKCE. Use it directly for any identity provider not covered by the prebuilt subclasses (GoogleProvider, GitHubProvider). For Sign in with Apple, see Apple Provider → instead, it uses a native SDK, not a browser-based OAuth2 flow.

Package: authyra_flutter


How it works

App                              Identity Provider
 │── build authorization URL ───────────────────►│
 │── open external browser ──────────────────────►│
 │                                               │ (user authenticates)
 │◄── deep-link callback (?code=…&state=…) ──────│
 │── exchange code for tokens ───────────────────►│
 │◄── { access_token, refresh_token, expires_in } │
 │── GET /userinfo (Bearer) ─────────────────────►│
 │◄── { sub, email, name, … } ───────────────────│

PKCE (RFC 7636) is enabled by default and is strongly recommended for mobile and desktop apps that cannot safely store a client secret.


Constructor

OAuth2Provider({
  required OAuth2Config config,
  Dio? dio,
})
ParameterTypeDescription
configOAuth2ConfigEndpoints, credentials, scopes, and user extractor
dioDio?Optional Dio instance, share interceptors or inject in tests

OAuth2Config

OAuth2Config is an immutable value object that describes everything the provider needs.

OAuth2Config({
  required String providerName,
  required String clientId,
  required String authorizationEndpoint,
  required String tokenEndpoint,
  required String userInfoEndpoint,
  required String redirectUri,
  required List<String> scopes,
  required AuthUser Function(Map<String, dynamic>) userExtractor,
  String? clientSecret,
  bool usePkce = true,
  Map<String, String> additionalAuthParams = const {},
  Map<String, String> additionalTokenParams = const {},
  Map<String, String>? tokenHeaders,
  Map<String, String>? userInfoHeaders,
  Duration timeout = const Duration(minutes: 5),
})
FieldDefaultDescription
providerNamerequiredLowercase slug, used as AuthProvider.id
clientIdrequiredClient ID from the identity provider
authorizationEndpointrequiredURL the browser opens for user consent
tokenEndpointrequiredURL to exchange the code for tokens
userInfoEndpointrequiredURL to fetch the user profile
redirectUrirequiredCustom URI scheme the browser redirects back to
scopesrequiredOAuth 2.0 scopes to request
userExtractorrequiredMaps the userinfo JSON to an AuthUser
clientSecretnullRequired for confidential clients; omit for PKCE flows
usePkcetruePKCE recommended for all public clients
additionalAuthParams{}Extra query params on the authorization URL
additionalTokenParams{}Extra body params on the token request
tokenHeadersnullCustom headers on the token request
userInfoHeadersnullCustom headers on the userinfo request
timeout5 minMax wait before throwing AuthenticationCancelledException
Never embed a clientSecret in a Flutter / mobile app. Use usePkce: true (default) or delegate the flow to your backend with ProxyOAuthProvider.

Quick example: Discord

lib/auth/discord_provider.dart
import 'package:authyra_flutter/authyra_flutter.dart';

final discordProvider = OAuth2Provider(
  config: OAuth2Config(
    providerName:          'discord',
    clientId:              'YOUR_DISCORD_CLIENT_ID',
    authorizationEndpoint: 'https://discord.com/oauth2/authorize',
    tokenEndpoint:         'https://discord.com/api/oauth2/token',
    userInfoEndpoint:      'https://discord.com/api/users/@me',
    redirectUri:           'myapp://auth/callback',
    scopes:                ['identify', 'email'],
    userExtractor: (json) => AuthUser(
      id:        json['id']       as String,
      email:     json['email']    as String?,
      name:      json['username'] as String?,
      avatarUrl: json['avatar'] != null
          ? 'https://cdn.discordapp.com/avatars/${json['id']}/${json['avatar']}.png'
          : null,
    ),
  ),
);

Registration and initialization

lib/main.dart
import 'package:app_links/app_links.dart';
import 'package:authyra_flutter/authyra_flutter.dart';

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

  // 1. Forward all incoming links. No per-provider registration needed:
  //    each sign-in attempt registers its own CSRF `state` for routing.
  AppLinks().uriLinkStream.listen(OAuth2CallbackHandler.handleCallback);

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

  runApp(const MyApp());
}

Register your custom URI scheme with each platform:

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>

Sign in

try {
  final user = await Authyra.instance.signIn('discord');
  print('Signed in as ${user.name}');
} on AuthenticationCancelledException {
  // User closed the browser
} on AuthenticationFailedException catch (e) {
  print('Sign-in failed: $e');
}

Token refresh

OAuth2Provider sets supportsRefresh: true. When the active session is within the refresh window (controlled by AuthConfig.refreshBeforeExpiry), AuthyraClient calls provider.refreshToken(refreshToken) automatically.

Override refreshToken in a subclass if the provider uses a non-standard refresh flow.


Advanced: additional auth params

Some providers require extra parameters on the authorization URL:

OAuth2Config(
  // ...
  additionalAuthParams: const {
    'access_type': 'offline',  // Google: request refresh token
    'prompt':      'consent',  // Google: always show consent screen
  },
)

Advanced: custom scopes at sign-in time

Pass scopes on OAuth2SignInParams to override the configured scopes for a single call:

await Authyra.instance.signIn('discord', params: OAuth2SignInParams(
  scopes: ['identify', 'email', 'guilds'],
));

copyWith

OAuth2Config exposes copyWith for environment-specific overrides or tests:

final stagingConfig = prodConfig.copyWith(
  redirectUri: 'myapp-staging://auth/callback',
);
Copyright © 2026