authWithOAuth2 method

Future<RecordAuth> authWithOAuth2(
  1. String providerName,
  2. OAuth2UrlCallbackFunc urlCallback, {
  3. List<String> scopes = const [],
  4. Map<String, dynamic> createData = const {},
  5. String? expand,
  6. String? fields,
})

Authenticate a single auth collection record with OAuth2 without custom redirects, deeplinks or even page reload.

This method initializes a one-off realtime subscription and will call urlCallback with the OAuth2 vendor url to authenticate. Once the external OAuth2 sign-in/sign-up flow is completed, the popup window will be automatically closed and the OAuth2 data sent back to the user through the previously established realtime connection.

On success this method automatically updates the client's AuthStore.

Example:

await pb.collection('users').authWithOAuth2('google', (url) async {
  await launchUrl(url);
});

Site-note: when creating the OAuth2 app in the provider dashboard you have to configure https://yourdomain.com/api/oauth2-redirect as redirect URL.

Implementation

Future<RecordAuth> authWithOAuth2(
  String providerName,
  OAuth2UrlCallbackFunc urlCallback, {
  List<String> scopes = const [],
  Map<String, dynamic> createData = const {},
  String? expand,
  String? fields,
}) async {
  final authMethods = await listAuthMethods();

  final AuthMethodProvider provider;
  try {
    provider =
        authMethods.authProviders.firstWhere((p) => p.name == providerName);
  } catch (_) {
    throw ClientException(
      originalError: Exception("missing provider $providerName"),
    );
  }

  final redirectUrl = client.buildUrl("/api/oauth2-redirect");

  final completer = Completer<RecordAuth>();

  Future<void> Function()? unsubscribeFunc;

  try {
    unsubscribeFunc = await client.realtime.subscribe("@oauth2", (e) async {
      final oldState = client.realtime.clientId;

      try {
        final eventData = e.jsonData();
        final code = eventData["code"] as String? ?? "";
        final state = eventData["state"] as String? ?? "";
        final error = eventData["error"] as String? ?? "";

        if (state.isEmpty || state != oldState) {
          throw StateError("State parameters don't match.");
        }

        if (error.isNotEmpty || code.isEmpty) {
          throw StateError("OAuth2 redirect error or missing code.");
        }

        final auth = await authWithOAuth2Code(
          provider.name,
          code,
          provider.codeVerifier,
          redirectUrl.toString(),
          createData: createData,
          expand: expand,
          fields: fields,
        );

        completer.complete(auth);

        if (unsubscribeFunc != null) {
          unawaited(unsubscribeFunc());
        }
      } catch (err) {
        if (err is ClientException) {
          completer.completeError(err);
        } else {
          completer.completeError(ClientException(originalError: err));
        }
      }
    });

    final authUrl = Uri.parse(provider.authUrl + redirectUrl.toString());

    final queryParameters = Map<String, String>.of(authUrl.queryParameters);
    queryParameters["state"] = client.realtime.clientId;

    // set custom scopes (if any)
    if (scopes.isNotEmpty) {
      queryParameters["scope"] = scopes.join(" ");
    }

    urlCallback(authUrl.replace(queryParameters: queryParameters));
  } catch (err) {
    if (err is ClientException) {
      completer.completeError(err);
    } else {
      completer.completeError(ClientException(originalError: err));
    }
  }

  return completer.future;
}