DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase Auth Advanced: OAuth, Magic Link, MFA, and RLS

Supabase Auth Advanced: OAuth, Magic Link, MFA, and RLS

Beyond .signInWithPassword() — the auth patterns that matter in production.

OAuth (Google / GitHub)

// Google OAuth — PKCE flow (Supabase Flutter SDK v2)
Future<void> signInWithGoogle() async {
  await supabase.auth.signInWithOAuth(
    OAuthProvider.google,
    redirectTo: kIsWeb
        ? null
        : 'io.supabase.myapp://login-callback',
    authScreenLaunchMode: LaunchMode.externalApplication,
  );
}

// Dashboard: Authentication > Providers > Google
// → Client ID / Secret from Google Cloud Console
// → Add https://<project>.supabase.co/auth/v1/callback as redirect URI
Enter fullscreen mode Exit fullscreen mode

Flutter Deep Link (android/app/src/main/AndroidManifest.xml):

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="io.supabase.myapp" android:host="login-callback" />
</intent-filter>
Enter fullscreen mode Exit fullscreen mode

Magic Link (Passwordless)

// Send magic link
await supabase.auth.signInWithOtp(
  email: email,
  emailRedirectTo: 'io.supabase.myapp://login-callback',
);

// After the user taps the link, receive the session
supabase.auth.onAuthStateChange.listen((data) {
  if (data.event == AuthChangeEvent.signedIn) {
    Navigator.pushReplacementNamed(context, '/home');
  }
});
Enter fullscreen mode Exit fullscreen mode

Customize the email template in Dashboard > Authentication > Email Templates.
Default link expiry is 1 hour (configurable).

MFA (TOTP)

// Step 1: Enroll
final response = await supabase.auth.mfa.enroll(
  factorType: FactorType.totp,
  issuer: 'MyApp',
);
final qrCode = response.totp?.qrCode;  // base64 PNG for display
final secret = response.totp?.secret;  // manual entry fallback

// Step 2: Challenge + verify with 6-digit code
final challengeResponse = await supabase.auth.mfa.challenge(
  factorId: response.id,
);
await supabase.auth.mfa.verify(
  factorId: response.id,
  challengeId: challengeResponse.id,
  code: userInputCode,
);
Enter fullscreen mode Exit fullscreen mode

Auth + RLS Integration

-- auth.uid() returns the currently authenticated user's ID
CREATE POLICY "users see own tasks"
ON tasks FOR SELECT
USING (user_id = auth.uid());

-- auth.jwt() exposes custom claims (e.g., premium plan check)
CREATE POLICY "premium only"
ON ai_results FOR INSERT
WITH CHECK (
  (auth.jwt() -> 'user_metadata' ->> 'plan') = 'premium'
);
Enter fullscreen mode Exit fullscreen mode

Update user_metadata from Flutter:

await supabase.auth.updateUser(
  UserAttributes(
    data: {'plan': 'premium', 'onboarded': true},
  ),
);
Enter fullscreen mode Exit fullscreen mode

Session Management

Future<void> initAuth() async {
  final session = supabase.auth.currentSession;
  if (session == null) return; // → login screen

  if (session.isExpired) {
    await supabase.auth.refreshSession(); // SDK handles this automatically
  }
}

await supabase.auth.signOut();
Enter fullscreen mode Exit fullscreen mode

Summary

OAuth        → Google/GitHub via PKCE + Deep Link
Magic Link   → passwordless; receive session via onAuthStateChange
MFA (TOTP)   → enroll → challenge → verify (3 steps)
RLS          → auth.uid() / auth.jwt() in row-level policies
Enter fullscreen mode Exit fullscreen mode

Because Supabase Auth integrates directly with Postgres, enabling RLS
eliminates most backend authorization code entirely.

Top comments (0)