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, GitHubOAuth2Provider, AppleProvider).

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. Register the provider for deep-link routing
  OAuth2CallbackHandler.registerProvider('myapp', discordProvider);

  // 2. Forward all incoming links
  AppLinks().uriLinkStream.listen(OAuth2CallbackHandler.handleCallback);

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

  runApp(const MyApp());
}

Register your custom URI scheme with each platform:

Androidandroid/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>

iOSios/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 scope in the params map to override the configured scopes for a single call:

await Authyra.instance.signIn('discord', params: {
  'scope': 'identify email guilds',
});

copyWith

OAuth2Config exposes copyWith for environment-specific overrides or tests:

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

See also

Copyright © 2026