BIO.RE
Authentication

Unlink OAuth Provider

Remove an OAuth provider link from the current account. Refuses to unlink the only sign-in method (anti-lockout).

DELETE /api/v1/auth/oauth/unlink/{provider} — 🔑 User-auth (Bearer JWT) · Rate limit: 20 req / hour

Detaches an OAuth identity from the current user. Refuses to unlink if it's the user's only sign-in method (no password + no other OAuth) — protects against accidental lockout.

Anti-lockout guard: if the user has no User.passwordHash AND only this provider is linked, the request returns 400 only_auth_method. Set a password first via the password reset flow if you want to unlink the only OAuth login.

Request

Headers

HeaderValueNotes
AuthorizationBearer <accessToken>Required

Path parameters

ParamTypeValidationNotes
provider'google' | 'apple' | 'x'Allowlisted in controller (validProviders)Hardcoded check — non-login providers (discord, github, etc.) rejected

No body, no query.

Response

200 OKApiResponseOf<MessageResponseDto>

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

Errors

HTTPcode / i18nKeyReason
400auth.oauth.provider_disabledprovider not in ['google', 'apple', 'x']
400auth.oauth.not_linkedCurrent user has no SocialAccount row for this provider
400auth.oauth.only_auth_methodAnti-lockout — no password + this is the last linked provider
401(no JWT or invalid)Not authenticated
429(throttle)Rate limit exceeded (20 req/hour)

Side effects

  1. Validate provider against allowlist (['google', 'apple', 'x']).
  2. Look up SocialAccount row for (userId, provider); verify exists.
  3. Anti-lockout check: count user's SocialAccount rows + check User.passwordHash. If unlinking would leave zero sign-in methods → reject 400 only_auth_method.
  4. Delete SocialAccount row.
  5. Audit log: auth.oauth.unlink.success.
  6. Send notification email: "{Provider} unlinked from your BIO.RE login on {date}".

Code samples

curl -X DELETE https://api.bio.re/api/v1/auth/oauth/unlink/google \
  -H 'Authorization: Bearer <accessToken>'
async function unlinkOAuth(provider: 'google' | 'apple' | 'x', accessToken: string): Promise<{ message: string }> {
  const res = await fetch(`https://api.bio.re/api/v1/auth/oauth/unlink/${provider}`, {
    method: 'DELETE',
    headers: { 'Authorization': `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Unlink failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { meKeys } from './use-me';

export function useOAuthUnlink() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (provider: 'google' | 'apple' | 'x') => {
      const res = await fetch(`/api/v1/auth/oauth/unlink/${provider}`, {
        method: 'DELETE',
        headers: { 'Authorization': `Bearer ${getAccessToken()}` },
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Unlink failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: meKeys.identity });
    },
  });
}

Try it

DELETE
/api/v1/auth/oauth/unlink/{provider}
AuthorizationBearer <token>

In: header

Path Parameters

provider*string

Response Body

application/json

application/json

application/json

application/json

curl -X DELETE "https://loading/api/v1/auth/oauth/unlink/string"
{
  "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"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/auth/oauth.controller.ts92–110 (unlink)
Serviceapps/api-core/src/modules/auth/oauth.service.tsunlinkProvider()
Prisma modelpackages/prisma/prisma/schema.prismaSocialAccount, User.passwordHash

On this page