Proxy OAuth Provider
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, … } │ │
- App POSTs to your backend's initiation endpoint.
- Backend returns the full authorization URL (including any client secret it needs internally).
- App opens the URL in the browser.
- User authenticates with the identity provider.
- Identity provider redirects to your backend callback URL.
- Backend exchanges the code, creates a session, then deep-links the app with a short-lived custom token.
- App calls its own backend callback endpoint with that token to exchange it for the full session.
Constructor
ProxyOAuthProvider({
required ProxyOAuthConfig config,
Dio? dio,
})
| Parameter | Description |
|---|---|
config | ProxyOAuthConfig — backend endpoints and callback scheme |
dio | Optional 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),
})
| Field | Default | Description |
|---|---|---|
providerName | required | Lowercase slug — used as AuthProvider.id (e.g. 'google') |
initiationEndpoint | required | POST — returns { "authorization_url": "…" } |
callbackEndpoint | required | POST { "token": "…" } — returns user + session data |
backendRedirectUri | required | HTTPS URI the identity provider redirects to on your backend |
appCallbackScheme | required | Custom URI prefix the backend deep-links to after exchange (e.g. myapp://auth/callback) |
userExtractor | required | Maps the callbackEndpoint response JSON to an AuthUser |
userInfoEndpoint | null | Optional — GET with Bearer token instead of using callbackEndpoint for user data |
headers | null | Extra headers attached to every backend request |
timeout | 5 min | Max 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
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'},
),
);
2. Wire deep links and initialize
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());
}
3. Platform deep-link config
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('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
- Confirm
appCallbackSchemeexactly matches what your backend redirects to after the OAuth exchange. - Confirm the scheme is registered in
AndroidManifest.xml/Info.plist. - Confirm
OAuth2CallbackHandler.registerProvideris called beforeAuthyra.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
- OAuth2Provider → — direct PKCE flow (no backend required)
- GitHubOAuth2Provider → — direct flow with client secret
- Flutter Setup → — deep-link wiring