Skip to content

@msflib/react-auth

Standalone authentication module for React applications. Built on TanStack Query v5. Supports real-time multi-tenant switching via @msflib/core.


Table of Contents


Installation

pnpm add @msflib/react-auth @msflib/core @msflib/react-shared

Peer Dependencies

Package Version
react >= 18
react-dom >= 18
@tanstack/react-query >= 5

Quick Start

1. Configure the application once (e.g. in your app's entry point):

// app/config.ts
import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: process.env.NEXT_PUBLIC_API_URL!,
  accessTokenKey: 'access_token',
  endpoints: {
    auth: {
      login: '/auth/login',
      me: '/auth/me',
      logout: '/auth/logout',
    },
  },
});

2. Wrap your app with QueryClientProvider and AuthProvider:

// app/providers.tsx
'use client'; // Next.js App Router

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@msflib/react-auth';

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>{children}</AuthProvider>
    </QueryClientProvider>
  );
}

3. Use the useAuth hook in any child component:

import { useAuth } from '@msflib/react-auth';

export function LoginForm() {
  const { login, loading, status } = useAuth();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const form = new FormData(e.currentTarget);

    await login(
      {
        email: form.get('email') as string,
        password: form.get('password') as string,
      },
      { onSuccess: () => console.log('Logged in!') },
    );
  };

  if (status === 'authenticated') return <p>Already logged in.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit" disabled={loading.login}>
        {loading.login ? 'Logging in…' : 'Login'}
      </button>
    </form>
  );
}

Configuration

Application Config

Call configureApplication once before rendering your app. It is idempotent — subsequent calls after the first are silently ignored.

import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token', // localStorage key for the JWT
  workspace: 'acme', // optional: initial active workspace
  endpoints: {
    auth: {
      /* override defaults here */
    },
  },
});

Custom Endpoints

All endpoint keys are optional. Omitted keys fall back to the defaults listed in Endpoint Defaults.

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'my_token',
  endpoints: {
    auth: {
      login: '/v2/auth/login',
      register: '/v2/auth/register',
      me: '/v2/auth/me',
      logout: '/v2/auth/logout',
      recoverPassword: '/v2/auth/recover',
      verifyToken: '/v2/auth/verify',
      resetPassword: '/v2/auth/reset',
      resendCode: '/v2/auth/resend',
      verifyAvailability: '/v2/auth/verify-availability',
      authenticate: '/v2/auth/authenticate',
      verifyOtp: '/v2/auth/verify-otp',
      ssoLogin: '/v2/auth/sso-login',
      ssoRedirect: '/v2/auth/sso-redirect',
      ssoRetrieve: '/v2/auth/sso-retrieve',
    },
  },
});

API Reference

AuthProvider

A React context provider that manages authentication state. Must be rendered inside a QueryClientProvider.

import { AuthProvider } from '@msflib/react-auth';

<QueryClientProvider client={queryClient}>
  <AuthProvider>{/* your app */}</AuthProvider>
</QueryClientProvider>;

Props

Prop Type Required Description
children React.ReactNode Yes Child component tree.
options object No Configuration options for the provider. See below for details.

options

Key Type Default Description
isWorkspaceScoped boolean true If true, the session is isolated per workspace.
requireAuth boolean false If true, the /me query only runs when isAuthenticated is true.
isAuthenticated boolean false The authentication status of the user, used when requireAuth is true.

Behaviour

  • On mount, AuthProvider automatically calls GET /me to restore the user session.
  • The result is cached for 60 seconds via TanStack Query (staleTime: 60_000).
  • When login() succeeds, the access token is stored via @msflib/core's storage and the query cache is populated immediately — no second round-trip to /me required.
  • When logout() succeeds, the token is removed from storage, the query cache is cleared, and status transitions to 'unauthenticated'.

useAuth

Returns the authentication context. Must be called inside a component tree wrapped by AuthProvider.

import { useAuth } from '@msflib/react-auth';

const {
  status,
  me,
  login,
  register,
  recoverPassword,
  verifyToken,
  resetPassword,
  resendCode,
  logout,
  updateMe,
  verifyAvailability,
  authenticate,
  verifyOtp,
  ssoLogin,
  ssoRedirect,
  ssoRetrieve,
  loading,
} = useAuth();

status

Value Meaning
'loading' The initial /me fetch is in progress.
'authenticated' A user session is active.
'unauthenticated' No session — user is logged out or /me returned an error.

me

Me | null — The current user object. Is null when unauthenticated.

login(data, options?)

Authenticates the user. Sends x-www-form-urlencoded — maps email → username automatically.

await login({ email: 'user@example.com', password: 'secret' });

Returns Promise<AuthUser>.

register(data, options?)

Creates a new user account.

await register({
  username: 'john',
  email: 'john@example.com',
  password: 'secret',
  phone: '555-1234',
});

Returns Promise<RegisterResponse>.

recoverPassword(data, options?)

Sends a password recovery email.

await recoverPassword({ email: 'user@example.com' });

Returns Promise<{ msg: string }>.

verifyToken(data, options?)

Verifies an OTP or recovery token.

await verifyToken({ email: 'user@example.com', token: '123456' });

Returns Promise<{ msg: string }>.

resetPassword(data, options?)

Resets the user's password using a valid token.

await resetPassword({
  token: 'abc123',
  email: 'user@example.com',
  new_password: 'newSecret',
});

Returns Promise<{ msg: string }>.

resendCode(data, options?)

Resends the verification code.

await resendCode({ email: 'user@example.com' });

Returns Promise<{ sent: boolean }>.

logout(options?)

Calls POST /logout, removes the stored access token, and clears the cached user.

await logout();

Returns Promise<void>.

updateMe(data, options?)

Updates the current user's account fields.

await updateMe({ username: 'new_name' });

Returns Promise<User>.

verifyAvailability(data, options?)

Checks whether an email, username, or phone is already taken — useful for inline signup-form validation before submitting.

await verifyAvailability({ field: 'email', value: 'user@example.com' });

Returns Promise<{ msg: string }>.

authenticate(data, options?)

Step 1 of the OTP login flow: verifies the email/password and sends a one-time code to the user's email. Does not establish a session by itself — follow up with verifyOtp.

await authenticate({ email: 'user@example.com', password: 'secret' });

Returns Promise<{ msg: string }>.

verifyOtp(data, options?)

Step 2 of the OTP login flow: verifies the code sent by authenticate and completes the login. On success, stores the access token and refreshes me — same as login.

await verifyOtp({ code: '123456', email: 'user@example.com' });

Returns Promise<AuthUser>.

ssoLogin(data, options?)

Exchanges an SSO token (typically read from a redirect query param) for a session in one call. On success, stores the access token and refreshes me.

await ssoLogin({ token: ssoTokenFromQueryParam });

Returns Promise<AuthUser>.

ssoRedirect(data, options?)

Hands an SSO token off to the backend (application/x-www-form-urlencoded) as part of the SSO handshake, optionally carrying a path_location to return to afterwards. Does not itself change auth state — pair with ssoRetrieve to pull the resulting session.

await ssoRedirect({ token: ssoToken, path_location: '/dashboard' });

Returns Promise<unknown> (the backend's response shape isn't fixed).

ssoRetrieve(options?)

Retrieves the session established by a prior ssoRedirect (no arguments — the backend resolves it from the request itself, e.g. a cookie). On success, stores the access token and refreshes me.

const session = await ssoRetrieve();
router.push(session.path_location || '/');

Returns Promise<AuthUser & { path_location?: string }>.

loading

An object of booleans indicating in-flight mutations:

loading.login;
loading.register;
loading.recoverPassword;
loading.verifyToken;
loading.resendCode;
loading.resetPassword;
loading.logout;
loading.updateMe;
loading.verifyAvailability;
loading.authenticate;
loading.verifyOtp;
loading.ssoLogin;
loading.ssoRedirect;
loading.ssoRetrieve;

Multi-Tenancy Support

@msflib/react-auth integrates with the workspace runtime in @msflib/core, but unlike most other msflib modules, AuthProvider does not subscribe to workspace changes itself.

How it works

  1. Endpoint paths — The API layer is created via createAuthApi(options?.isWorkspaceScoped), which uses configuredApiClient() from @msflib/core. Scoped requests are prefixed/headered for the active workspace at request time, same as every other module.
  2. Query cache — The me query uses a fixed key (authQueryKeys.me()), not one derived from the active workspace. AuthProvider does not call useActiveWorkspace(), so changing the active workspace via setActiveWorkspace() does not by itself trigger a re-fetch of /me.

If your app needs the session to refresh when the user switches workspace, invalidate it explicitly:

import { useQueryClient } from '@tanstack/react-query';
import { setActiveWorkspace } from '@msflib/core';
import { authQueryKeys } from '@msflib/react-auth';

const queryClient = useQueryClient();

setActiveWorkspace('new-workspace');
queryClient.invalidateQueries({ queryKey: authQueryKeys.me() });

Setting the initial workspace

configureApplication({
  baseURL: 'https://api.example.com',
  accessTokenKey: 'access_token',
  workspace: 'acme-corp', // seeds the active workspace on startup
});

Clearing the workspace

setActiveWorkspace(null); // or setActiveWorkspace('')

TypeScript Types

export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';

export type Me = {
  id: number;
  created_at: string;
  updated_at: string;
  username: string;
  email: string;
  phone?: string;
  status: string;
  role: string;
  data?: Record<string, unknown>;
  [k: string]: unknown;
};

export type UpdateMePayload = Partial<Me> & {
  current_password?: string;
  new_password?: string;
};

export type Profile = {
  firstname: string;
  lastname: string;
  date_of_birth: string;
  gender: string;
  marital_status: string;
  [k: string]: unknown;
};

export type AuthUser = {
  access_token: string;
  token_type: string;
  expires: string;
  account?: Me;
};

export type LoginPayload = {
  email: string;
  password: string;
};

export type RegisterPayload = {
  username?: string;
  email?: string;
  password: string;
  phone?: string;
  profile?: Omit<Profile, 'id' | 'email'>;
  [key: string]: unknown;
};

export interface RegisterResponse extends Me {
  profile: Profile;
}

export interface RecoverPasswordPayload {
  email: string;
}

export interface VerifyTokenPayload {
  email: string;
  token: string;
}

export interface ResendCodePayload {
  email: string | null;
}

export interface ResetPasswordPayload {
  token: string;
  email: string;
  new_password: string;
}

export type VerifyAvailabilityField = 'email' | 'username' | 'phone';

export interface VerifyAvailabilityPayload {
  field: VerifyAvailabilityField;
  value: string;
}

export interface AuthenticatePayload {
  email: string;
  password: string;
}

export interface VerifyOtpPayload {
  code: string;
  email: string;
}

export interface SsoLoginParams {
  token: string;
}

export interface SsoRedirectPayload {
  token: string;
  path_location?: string;
}

export type SsoRetrieveResponse = AuthUser & {
  path_location?: string;
};

Endpoint Defaults

Key Default HTTP Method
login /login POST (form)
register /open POST
recoverPassword /password-recovery POST
verifyToken /verify-token POST
resendCode /resend-code POST
resetPassword /reset-password POST
me /me GET
logout /logout POST
verifyAvailability /verify-availability POST
authenticate /authenticate POST
verifyOtp /verify-otp POST
ssoLogin /sso-login GET
ssoRedirect /sso-redirect POST (form)
ssoRetrieve /sso-retrieve GET

Override any key via configureApplication({ endpoints: { auth: { ... } } }).

Notes: - verifyAvailability, authenticate, verifyOtp are the pre-login OTP flow (accounts/otp_accounts on the backend) and don't require an access token. - ssoLogin, ssoRedirect, ssoRetrieve cover the SSO handshake (sso-auth on the backend) and don't require an access token either. - ssoRedirect sends application/x-www-form-urlencoded (not JSON), matching the backend contract.