starti.app
How-to Guides

Biometric Login with OAuth

Use Face ID or Touch ID to let returning users skip the full OAuth login flow

Biometric Login with OAuth

After a user signs in with an OAuth provider, you can store a session token behind Face ID or Touch ID so returning users skip the full login flow.

Prerequisites

  • The starti.app SDK is installed and initialized
  • An OAuth provider is configured for your app in the starti.app manager
  • The device has biometric hardware (Face ID or fingerprint)

This pattern works with any OAuth provider. The examples below use MitID, but you can substitute Google, Apple, or any other supported provider.

Steps

Check biometric availability

Before offering biometric login, check that the device supports it:

const biometricType = await startiapp.Biometrics.getAuthenticationType();
const hasBiometrics = biometricType !== "none";

If biometricType is "none", skip the biometric flow and always use the full OAuth login.

First login: authenticate and store the session

After a successful OAuth sign-in, send the authorization code to your backend as usual. When your backend returns a session token, store it with setSecuredContent:

// 1. Sign in with the OAuth provider
const result = await startiapp.Auth.signIn("signaturgruppenmitid");

if (!result.isSuccess) {
  console.error("Sign in failed:", result.errorMessage);
  return;
}

// 2. Exchange the code on your backend
const response = await fetch("/api/auth/mitid", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    authorizationCode: result.authorizationCode,
    codeVerifier: result.codeVerifier,
    redirectUri: result.redirectUri,
  }),
});

const session = await response.json();

// 3. Store the session token behind biometrics
await startiapp.Biometrics.setSecuredContent(session.token);

Saving does not trigger a biometric prompt.

Return visit: retrieve the session with biometrics

On subsequent visits, check if a stored session exists. If it does, retrieve it with a biometric prompt instead of starting a full OAuth login:

const hasSession = await startiapp.Biometrics.hasSecuredContent();

if (hasSession) {
  const token = await startiapp.Biometrics.getSecuredContent(
    "Log in",
    "Verify your identity"
  );

  if (token) {
    // Use the stored token with your backend
    const response = await fetch("/api/auth/session", {
      headers: { Authorization: `Bearer ${token}` },
    });

    if (response.ok) {
      console.log("Resumed session");
    } else {
      // Token expired or invalid — clear and do full login
      await startiapp.Biometrics.removeSecuredContent();
      await doFullLogin();
    }
  } else {
    // User cancelled biometrics or scan failed — do full login
    await doFullLogin();
  }
} else {
  await doFullLogin();
}

Sign out: clear stored data

When the user signs out, remove the stored session token alongside your normal sign-out logic:

await startiapp.Biometrics.removeSecuredContent();
await startiapp.Auth.signOut();

Handle expired tokens gracefully. If getSecuredContent returns a token but your backend rejects it, call removeSecuredContent() and fall back to a full OAuth login. Note that getSecuredContent returns null both when nothing is stored and when the user cancels the biometric prompt — always treat null as "no stored session".

Complete example

await startiapp.initialize();

const biometricType = await startiapp.Biometrics.getAuthenticationType();
const hasBiometrics = biometricType !== "none";

async function login() {
  // Try biometric resume first
  if (hasBiometrics) {
    const hasSession = await startiapp.Biometrics.hasSecuredContent();

    if (hasSession) {
      const token = await startiapp.Biometrics.getSecuredContent(
        "Log in",
        "Verify your identity"
      );

      if (token) {
        const response = await fetch("/api/auth/session", {
          headers: { Authorization: `Bearer ${token}` },
        });

        if (response.ok) {
          const user = await response.json();
          console.log("Welcome back,", user.name);
          return;
        }

        // Token expired — clear and continue to full login
        await startiapp.Biometrics.removeSecuredContent();
      }
    }
  }

  // Full OAuth login
  const result = await startiapp.Auth.signIn("signaturgruppenmitid");

  if (!result.isSuccess) {
    alert("Login failed: " + result.errorMessage);
    return;
  }

  const response = await fetch("/api/auth/mitid", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      authorizationCode: result.authorizationCode,
      codeVerifier: result.codeVerifier,
      redirectUri: result.redirectUri,
    }),
  });

  const session = await response.json();
  console.log("Logged in:", session.name);

  // Store for biometric resume next time
  if (hasBiometrics) {
    await startiapp.Biometrics.setSecuredContent(session.token);
  }
}

async function logout() {
  await startiapp.Biometrics.removeSecuredContent();
  await startiapp.Auth.signOut();
  console.log("Logged out");
}

See also

On this page