BIO.RE
Authentication

Login or Register via OAuth

One-step login OR auto-register via Google, Apple, or X (Twitter). No captcha (provider handles bot protection). Server-side token verification.

POST /api/v1/auth/oauth/login — 🌐 Public · Rate limit: 10 req / hour · Captcha: NOT required (provider handles)

Single endpoint for OAuth flows: if the provider's user is unknown, a fresh BIO.RE account is auto-created from the verified profile. If known, the existing account is logged in. Returns accessToken + sets httpOnly refresh cookie.

Server-side verification is mandatory. The frontend obtains an idToken (Google/Apple) or code (any provider) from the SDK, sends it here; the backend re-verifies against the provider's public keys via OAuthVerifierService — never trusts the frontend's claim blindly.

Three login providers: google, apple, x. Despite 15 OAuth platforms in admin (OAuthProvider table), only these three are accepted for login. The other 12 (discord, github, instagram, linkedin, pinterest, reddit, spotify, threads, tiktok, twitch, youtube, facebook) are verify-only — used by creator/social/connect to attach social accounts, not to log in.

Request

Body — OAuthLoginDto

FieldTypeRequiredValidationNotes
provider'google' | 'apple' | 'x'IsIn(OAUTH_PROVIDERS)Active login provider
idTokenstringconditionalmax 5000 charsRequired for Google + Apple flows (alternative to code)
codestringconditionalmax 2000 charsAuthorization code from any provider (alternative to idToken)
codeVerifierstringrequired for Xmax 256 charsPKCE code verifier — mandatory for X flow
referralCodestringAttribution if a brand-new account is auto-created

At least one of idToken or code MUST be present. PKCE (codeVerifier) is X-specific.

Response

200 OKApiResponseOf<OAuthLoginResponseDto>

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "expiresIn": 900,
    "isNewUser": false
  }
}
FieldTypeNotes
accessTokenstringShort-lived JWT (~15 min)
expiresInnumberSeconds to access token expiry
isNewUserbooleantrue if this call auto-created a fresh BIO.RE account; client may show onboarding

Refresh token is set as httpOnly cookie scoped to .bio.re.

Errors

HTTPcode / i18nKeyReason
400auth.oauth.provider_disabledprovider not in active login set OR admin disabled the platform
400(DTO validation)Missing both idToken and code; missing codeVerifier for X
401auth.oauth.token_invalidProvider rejected the token (signature, audience, expiry)
409auth.oauth.email_existsAccount already exists under that email; response includes hasPassword + hasOAuth flags so client can prompt linking via POST /auth/oauth/link
429(throttle)Rate limit exceeded (10 req/hour)

Side effects

  1. OAuthVerifierService.verify(provider, idToken, code, codeVerifier) — exchanges code for tokens (when code flow) and verifies idToken against provider's JWK set.
  2. Lookup existing SocialAccount by (provider, providerUserId). If found → load User, log in.
  3. If not found, lookup User by verified email from provider profile. If found → conflict (email_exists) — client must call /auth/oauth/link.
  4. If neither match → auto-register: insert User (verified email, no password), SocialAccount, optional referredBy resolution, ConsentRecord (terms + privacy implicitly accepted via OAuth flow per platform policy).
  5. Issue access + refresh JWT pair, set httpOnly cookie.
  6. Audit log: auth.oauth.login.success or .register.success.

Code samples

# Google flow (idToken from Google Identity Services)
curl -X POST https://api.bio.re/api/v1/auth/oauth/login \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{
    "provider": "google",
    "idToken": "<google_id_token>"
  }'

# X flow (code + PKCE verifier)
curl -X POST https://api.bio.re/api/v1/auth/oauth/login \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{
    "provider": "x",
    "code": "<authorization_code>",
    "codeVerifier": "<pkce_code_verifier>"
  }'
type OAuthLoginInput = {
  provider: 'google' | 'apple' | 'x';
  idToken?: string;
  code?: string;
  codeVerifier?: string;
  referralCode?: string;
};

type OAuthLoginResponse = {
  accessToken: string;
  expiresIn: number;
  isNewUser: boolean;
};

async function oauthLogin(input: OAuthLoginInput): Promise<OAuthLoginResponse> {
  const res = await fetch('https://api.bio.re/api/v1/auth/oauth/login', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(input),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'OAuth failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation } from '@tanstack/react-query';

export function useOAuthLogin() {
  return useMutation({
    mutationFn: async (input: OAuthLoginInput) => {
      const res = await fetch('/api/v1/auth/oauth/login', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'OAuth failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as OAuthLoginResponse;
    },
    onSuccess: (data) => {
      router.push(data.isNewUser ? '/onboarding' : '/dashboard');
    },
  });
}

Try it

POST
/api/v1/auth/oauth/login

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/auth/oauth/login" \  -H "Content-Type: application/json" \  -d '{    "provider": "google"  }'
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "expiresIn": 900,
    "isNewUser": false
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/auth/oauth.controller.ts30–63 (login)
DTO (request)apps/api-core/src/modules/auth/dto/oauth.dto.ts6–33 (OAuthLoginDto)
DTO (response)apps/api-core/src/modules/auth/dto/response.dto.ts36–45 (OAuthLoginResponseDto)
Service (verify)apps/api-core/src/modules/auth/oauth-verifier.service.tsverify()
Service (login/register)apps/api-core/src/modules/auth/oauth.service.tsloginOrRegister()
Prisma modelspackages/prisma/prisma/schema.prismaUser, SocialAccount, Session, ConsentRecord

On this page