BIO.RE
Authentication

Link OAuth Provider

Attach an OAuth provider (Google / Apple / X) to the currently authenticated account. Lets users add Sign-in-with after registering with email.

POST /api/v1/auth/oauth/link — 🔑 User-auth (Bearer JWT) · Rate limit: 20 req / hour

Links an OAuth identity to the current user. Useful when a user registered with email + password and later wants to add Google/Apple/X login as an alternative sign-in.

The provider account is unique per platform: an OAuth identity that's already linked to another user returns 409 conflict. Server-side verification of the token (same OAuthVerifierService as /auth/oauth/login) prevents claim forgery.

Request

Headers

HeaderValueNotes
AuthorizationBearer <accessToken>Required
Content-Typeapplication/jsonRequired

Body — OAuthLinkDto

Same shape as OAuthLoginDto minus referralCode:

FieldTypeRequiredNotes
provider'google' | 'apple' | 'x'IsIn(OAUTH_PROVIDERS)
idTokenstringconditionalGoogle / Apple flow
codestringconditionalCode flow
codeVerifierstringrequired for XPKCE

Response

200 OKApiResponseOf<MessageResponseDto>

{ "success": true, "data": { "message": "Provider linked successfully" } }

Errors

HTTPcode / i18nKeyReason
400auth.oauth.already_linkedThis user already has the provider linked
400auth.oauth.provider_disabledAdmin disabled this platform
400(DTO validation)Missing both idToken and code; missing codeVerifier for X
401(no JWT or invalid)Not authenticated OR token verification failed
409auth.oauth.linked_to_other_userThis OAuth identity is already linked to a different user
429(throttle)Rate limit exceeded (20 req/hour)

Side effects

  1. Verify token via OAuthVerifierService.verify().
  2. Check existing SocialAccount for (provider, providerUserId):
    • If linked to current user → return 400 already_linked.
    • If linked to another user → return 409 linked_to_other_user.
  3. Insert SocialAccount row tying current user to provider profile.
  4. Audit log: auth.oauth.link.success.
  5. Send notification email: "{Provider} account linked to your BIO.RE login on {date}".

Code samples

curl -X POST https://api.bio.re/api/v1/auth/oauth/link \
  -H 'Authorization: Bearer <accessToken>' \
  -H 'Content-Type: application/json' \
  -d '{
    "provider": "google",
    "idToken": "<google_id_token>"
  }'
type OAuthLinkInput = {
  provider: 'google' | 'apple' | 'x';
  idToken?: string;
  code?: string;
  codeVerifier?: string;
};

async function linkOAuth(input: OAuthLinkInput, accessToken: string): Promise<{ message: string }> {
  const res = await fetch('https://api.bio.re/api/v1/auth/oauth/link', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,
    },
    body: JSON.stringify(input),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Link failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { meKeys } from './use-me';

export function useOAuthLink() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (input: OAuthLinkInput) => {
      const res = await fetch('/api/v1/auth/oauth/link', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${getAccessToken()}`,
        },
        body: JSON.stringify(input),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Link failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: meKeys.identity });
      toast.success(t('auth.oauth.linked'));
    },
  });
}

Try it

POST
/api/v1/auth/oauth/link
AuthorizationBearer <token>

In: header

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/link" \  -H "Content-Type: application/json" \  -d '{    "provider": "google"  }'
{
  "success": true,
  "data": {
    "message": "Operation completed successfully"
  }
}
{
  "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.ts67–88 (link)
DTO (request)apps/api-core/src/modules/auth/dto/oauth.dto.ts35–57 (OAuthLinkDto)
Service (verify)apps/api-core/src/modules/auth/oauth-verifier.service.tsverify()
Service (link)apps/api-core/src/modules/auth/oauth.service.tslinkProvider()
Prisma modelpackages/prisma/prisma/schema.prismaSocialAccount, User

On this page