Providers

Proxy OAuth Provider

Backend-delegated OAuth — the client secret never leaves your server.

ProxyOAuthProvider delegates the entire OAuth code exchange to your backend. The app opens the authorization URL and handles the return deep link — your server does everything else: holds the client secret, exchanges the code, creates the session, and sends a custom token back to the app.

Package: authyra_flutter

Use this when:

  • You cannot safely embed a client secret in the app (GitHub, Facebook, or any provider that doesn't support PKCE).
  • You want full control over the session lifecycle on the server.
  • You are already running a backend for your app.

How it works

App                Backend                  Identity Provider
 │──POST /initiate──►│                              │
 │◄── { authorization_url } ───────────────────────│
 │                   │                              │
 │── opens browser ──►                              │
 │                                                 │
 │                   │◄── GET /oauth-callback?code ─│
 │                   │── exchange code ─────────────►│
 │                   │◄── { access_token, … } ──────│
 │                   │── create session              │
 │◄── myapp://callback?token=CUSTOM_JWT ────────────│
 │                   │                              │
 │──POST /callback { token }──►│                    │
 │◄── { user, accessToken, … } │                    │
  1. App POSTs to your backend's initiation endpoint.
  2. Backend returns the full authorization URL (including any client secret it needs internally).
  3. App opens the URL in the browser.
  4. User authenticates with the identity provider.
  5. Identity provider redirects to your backend callback URL.
  6. Backend exchanges the code, creates a session, then deep-links the app with a short-lived custom token.
  7. App calls its own backend callback endpoint with that token to exchange it for the full session.

Constructor

ProxyOAuthProvider({
  required ProxyOAuthConfig config,
  Dio? dio,
})
ParameterDescription
configProxyOAuthConfig — backend endpoints and callback scheme
dioOptional Dio instance for testing or shared interceptors

ProxyOAuthConfig

ProxyOAuthConfig({
  required String providerName,
  required String initiationEndpoint,
  required String callbackEndpoint,
  required String backendRedirectUri,
  required String appCallbackScheme,
  required AuthUser Function(Map<String, dynamic>) userExtractor,
  String? userInfoEndpoint,
  Map<String, String>? headers,
  Duration timeout = const Duration(minutes: 5),
})
FieldDefaultDescription
providerNamerequiredLowercase slug — used as AuthProvider.id (e.g. 'google')
initiationEndpointrequiredPOST — returns { "authorization_url": "…" }
callbackEndpointrequiredPOST { "token": "…" } — returns user + session data
backendRedirectUrirequiredHTTPS URI the identity provider redirects to on your backend
appCallbackSchemerequiredCustom URI prefix the backend deep-links to after exchange (e.g. myapp://auth/callback)
userExtractorrequiredMaps the callbackEndpoint response JSON to an AuthUser
userInfoEndpointnullOptional — GET with Bearer token instead of using callbackEndpoint for user data
headersnullExtra headers attached to every backend request
timeout5 minMax wait before throwing AuthenticationCancelledException

Backend contract

Initiation endpoint

POST <initiationEndpoint>

Request body: { "provider": "<providerName>", "redirect_uri": "<backendRedirectUri>" }

Expected response:

{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}

Callback endpoint

POST <callbackEndpoint>

Request body: { "token": "<CUSTOM_JWT>" }

Expected response (passed to userExtractor):

{
  "id":           "usr_01j...",
  "email":        "alice@example.com",
  "name":         "Alice",
  "accessToken":  "eyJhbGci...",
  "refreshToken": "dGhpcyBp...",
  "expiresAt":    "2026-06-01T12:00:00.000Z"
}

Setup

1. Configure the provider

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

final googleProxyProvider = ProxyOAuthProvider(
  config: ProxyOAuthConfig(
    providerName:       'google',
    initiationEndpoint: 'https://api.example.com/auth/google/initiate',
    callbackEndpoint:   'https://api.example.com/auth/google/callback',
    backendRedirectUri: 'https://api.example.com/auth/google/oauth-callback',
    appCallbackScheme:  'myapp://auth/callback',
    userExtractor: (json) => AuthUser(
      id:    json['id']    as String,
      email: json['email'] as String?,
      name:  json['name']  as String?,
    ),
    // Pass any auth header your backend requires:
    headers: {'X-App-Version': '1.0.0'},
  ),
);
lib/main.dart
import 'package:app_links/app_links.dart';
import 'package:authyra_flutter/authyra_flutter.dart';

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

  // Register before Authyra.initialize
  OAuth2CallbackHandler.registerProvider('myapp', googleProxyProvider);
  AppLinks().uriLinkStream.listen(OAuth2CallbackHandler.handleCallback);

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

  runApp(const MyApp());
}

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('google');
  print('Hello, ${user.name}!');
} on AuthenticationCancelledException {
  // User closed the browser
} on AuthenticationFailedException catch (e) {
  print('Sign-in failed: $e');
}

Full session with tokens

If your backend returns accessToken, refreshToken, and expiresAt in the callback response, capture them in userExtractor via AuthSignInResult:

// Note: userExtractor returns AuthUser, but if you need tokens, implement
// the full AuthProvider interface directly and return AuthSignInResult
// from signIn(). ProxyOAuthProvider handles AuthSignInResult internally
// when the callbackEndpoint response includes token fields.

The provider automatically reads accessToken, refreshToken, and expiresAt fields from the callback response JSON and stores them in the AuthSession. No extra configuration is needed if your backend returns these standard field names.


Using with multiple providers

Register each proxy provider under its own callback scheme:

final googleProxy = ProxyOAuthProvider(
  config: ProxyOAuthConfig(
    providerName:      'google',
    appCallbackScheme: 'myapp://auth/google/callback',
    // ...
  ),
);

final githubProxy = ProxyOAuthProvider(
  config: ProxyOAuthConfig(
    providerName:      'github',
    appCallbackScheme: 'myapp://auth/github/callback',
    // ...
  ),
);

OAuth2CallbackHandler.registerProvider('myapp', googleProxy);
OAuth2CallbackHandler.registerProvider('myapp', githubProxy);

OAuth2CallbackHandler routes by scheme + path, so both share the myapp scheme with different paths.


Troubleshooting

App never receives the deep link

  1. Confirm appCallbackScheme exactly matches what your backend redirects to after the OAuth exchange.
  2. Confirm the scheme is registered in AndroidManifest.xml / Info.plist.
  3. Confirm OAuth2CallbackHandler.registerProvider is called before Authyra.initialize.

callbackEndpoint returns 401 / 403

The custom token your backend encoded in the deep link has expired or been consumed. The token should be short-lived (30–60 seconds) and single-use — add a check on the backend.

AuthenticationFailedException after deep link arrives

The callbackEndpoint returned an unexpected response. Log the raw JSON to confirm it matches the structure expected by userExtractor.


See also

Copyright © 2026