@tailor-platform/auth-browser-client
Browser client library for Tailor Platform authentication using the "browser client" authentication flow.
Overview
This library provides an authentication solution for browser-based applications integrating with the Tailor Platform. It implements a custom authentication flow designed specifically for the Tailor Platform's "browser client" type, as described in the Tailor Platform authentication documentation.
Important: This library is not a general-purpose OAuth 2.0/OpenID Connect client. It is specifically designed for Tailor Platform's browser client authentication flow, which uses HTTPOnly cookies for token management and includes Tailor-specific GraphQL integration features.
The library provides a functional approach with the createAuthClient
function and has been designed with a modular architecture for maintainability.
Installation
npm install @tailor-platform/auth-browser-client
BASIC Usage
Step 1: Basic Example Using Default Type (User)
Start with a basic setup using the default User
type. In this case, you don't need to specify type parameters.
import { createAuthClient } from '@tailor-platform/auth-browser-client';
const authClient = createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: 'https://your-app.com/callback',
scope: 'openid profile email'
});
Correspondence Between Default Type and meQuery
- Type:
User
(built-in default type - { id: string; [key: string]: any }
)
- meQuery:
query { me { id } }
(default)
- Fetched Data: User ID only
Step 2: Using Custom Types
When you need more user information, use custom types in combination with corresponding meQuery.
import { createAuthClient, type AuthClient } from '@tailor-platform/auth-browser-client';
interface CustomUser {
id: string;
email: string;
name?: string;
}
const authClient: AuthClient<CustomUser> = createAuthClient<CustomUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: 'https://your-app.com/callback',
scope: 'openid profile email',
meQuery: `query {
me {
id
email
name
}
}`
});
Correspondence Between Custom Type and meQuery
⚠️ IMPORTANT: When using custom user types, you MUST provide a corresponding meQuery
that matches your type definition. Failure to do so will result in incomplete or incorrect user data.
- Type:
CustomUser
(custom type - { id: string; email: string; name?: string }
)
- meQuery:
query { me { id email name } }
(Required - must match the type fields)
- Fetched Data: User ID, email, and name
📝 NOTE: The meQuery
fields must correspond to the properties defined in your custom user type interface. Missing or mismatched fields will cause type safety issues and runtime errors.
Important Notes
Custom User Type and meQuery Dependency
⚠️ CRITICAL: When using custom user types with createAuthClient<CustomType>()
, you MUST provide a corresponding meQuery
that fetches all the fields defined in your custom type interface.
Why this matters:
- The library uses the
meQuery
to fetch user profile data from the Tailor Platform GraphQL API
- If you define a custom type but don't provide a matching
meQuery
, the client will use the default query (query { me { id } }
)
- This mismatch will result in incomplete user data and potential runtime errors when accessing expected properties
Key Requirements:
- Type-Query Alignment: Every field in your custom user interface must have a corresponding field in the
meQuery
- Required Fields: Ensure all non-optional fields in your type are included in the query
- GraphQL Validity: The
meQuery
must be a valid GraphQL query structure
🚨 WARNING: Ignoring this dependency will cause your application to behave unexpectedly. User properties may be undefined
even when you expect them to have values, leading to UI errors and poor user experience.
React Integration Examples
Step 1 Case (Using Default Type)
import React, { useEffect, useState } from 'react';
import { createAuthClient, AuthState } from '@tailor-platform/auth-browser-client';
const App: React.FC = () => {
const [authClient] = useState(() => createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: window.location.origin + '/callback',
scope: 'openid profile email'
}));
const [authState, setAuthState] = useState(authClient.getState());
useEffect(() => {
const unsubscribe = authClient.addEventListener((event) => {
if (event.type === 'auth_state_changed') {
setAuthState(event.data);
}
});
return unsubscribe;
}, [authClient]);
if (authState.isLoading) {
return <div>Loading...</div>;
}
if (authState.error) {
return <div>Error: {authState.error}</div>;
}
if (!authState.isAuthenticated) {
return (
<div>
<h1>Welcome</h1>
<button onClick={() => authClient.login()}>
Log In
</button>
</div>
);
}
return (
<div>
<h1>Hello, User {authState.user?.id}</h1>
<button onClick={() => authClient.logout()}>
Log Out
</button>
</div>
);
};
export default App;
Step 2 Case (Using Custom Type)
import React, { useEffect, useState } from 'react';
import { createAuthClient, type AuthClient, AuthState } from '@tailor-platform/auth-browser-client';
interface CustomUser {
id: string;
email: string;
name?: string;
}
const App: React.FC = () => {
const [authClient] = useState(() => createAuthClient<CustomUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: window.location.origin + '/callback',
scope: 'openid profile email',
meQuery: `query { me { id email name } }`
}));
const [authState, setAuthState] = useState<AuthState<CustomUser>>(authClient.getState());
useEffect(() => {
const unsubscribe = authClient.addEventListener((event) => {
if (event.type === 'auth_state_changed') {
setAuthState(event.data);
}
});
return unsubscribe;
}, [authClient]);
if (authState.isLoading) {
return <div>Loading...</div>;
}
if (authState.error) {
return <div>Error: {authState.error}</div>;
}
if (!authState.isAuthenticated) {
return (
<div>
<h1>Welcome</h1>
<button onClick={() => authClient.login()}>
Log In
</button>
</div>
);
}
return (
<div>
<h1>Hello, {authState.user?.name || authState.user?.email}</h1>
<button onClick={() => authClient.logout()}>
Log Out
</button>
</div>
);
};
export default App;
Vanilla JavaScript Example
import { createAuthClient } from '@tailor-platform/auth-browser-client';
const authClient = createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: window.location.origin + '/callback',
scope: 'openid profile email'
});
authClient.addEventListener((event) => {
if (event.type === 'auth_state_changed') {
const { isAuthenticated, user, error } = event.data;
if (error) {
document.getElementById('error-info').textContent = `Error: ${error}`;
return;
}
if (isAuthenticated) {
document.getElementById('user-info').textContent =
`Welcome, ${user.name || user.email}`;
document.getElementById('login-btn').style.display = 'none';
document.getElementById('logout-btn').style.display = 'block';
} else {
document.getElementById('user-info').textContent = 'Not signed in';
document.getElementById('login-btn').style.display = 'block';
document.getElementById('logout-btn').style.display = 'none';
}
}
});
document.getElementById('login-btn').addEventListener('click', () => {
authClient.login();
});
document.getElementById('logout-btn').addEventListener('click', () => {
authClient.logout();
});
API Reference
createAuthClient
The main function for creating an authentication client.
Function Signature
createAuthClient<TUser = User>(config: AuthClientConfig): AuthClient<TUser>
Default Type: If no type parameter is provided, createAuthClient
uses the built-in User
interface as the default type. The User
interface includes an id
field and allows additional properties.
interface User {
id: string;
[key: string]: any;
}
const authClient = createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev'
});
interface CustomUser {
id: string;
email: string;
name?: string;
}
const customAuthClient = createAuthClient<CustomUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev'
});
Type Safety: You can provide explicit type parameters for enhanced type safety with custom user interfaces.
Configuration
interface AuthClientConfig {
clientId: string;
appUri: string;
redirectUri?: string;
scope?: string;
audience?: string;
meQuery?: string;
}
Methods
getUser(): TUser | null
Returns the current user information.
const user = authClient.getUser();
if (user) {
console.log('Current user:', user.email);
}
login(): Promise<void>
Initiates the OAuth authentication flow by redirecting to the authorization server.
await authClient.login();
logout(): Promise<void>
Logs out the user, revokes tokens, and clears authentication state.
await authClient.logout();
getState(): AuthState<TUser>
Returns the current authentication state.
const { isAuthenticated, user, isLoading, error, accessToken } = authClient.getState();
checkAuthStatus(): Promise<AuthState<TUser>>
Manually checks authentication status by verifying authentication cookies and fetching user profile.
const authState = await authClient.checkAuthStatus();
getAuthUrl(): Promise<string>
Generates an authentication URL without triggering redirect (useful for popup flows).
const authUrl = await authClient.getAuthUrl();
window.open(authUrl, 'auth-popup', 'width=500,height=600');
handleCallback(): Promise<void>
Handles OAuth callback after redirect (automatically called during initialization).
await authClient.handleCallback();
addEventListener(listener: AuthEventListener): () => void
Adds an event listener for authentication events. Returns an unsubscribe function.
const unsubscribe = authClient.addEventListener((event) => {
console.log('Auth event:', event.type, event.data);
});
unsubscribe();
configure(newConfig: Partial<AuthClientConfig>): void
Updates the client configuration.
authClient.configure({
scope: 'openid profile email offline_access'
});
refreshTokens(): Promise<void>
Manually refreshes the authentication tokens using the refresh token grant flow.
await authClient.refreshTokens();
authClient.addEventListener((event) => {
if (event.type === 'token_refresh') {
console.log('Tokens refreshed:', event.data);
}
});
Important Notes:
- Tokens are managed server-side using HTTPOnly cookies
- The method triggers a
token_refresh
event upon successful completion
- Authentication state is automatically updated after successful refresh
- Automatic token refresh occurs during
checkAuthStatus()
calls when tokens are expired
Custom User Profile Query
⚠️ CRITICAL REQUIREMENT: When using custom user types, you MUST provide a meQuery
that corresponds exactly to your type definition.
You can customize the GraphQL query used to fetch user profile data during authentication by providing a meQuery
option:
const authClient = createAuthClient<MyUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: 'https://your-app.com/callback',
scope: 'openid profile email',
meQuery: `query {
me {
id
email
name
createdAt
avatar
department
roles
}
}`
});
Default Query: If no meQuery
is specified, the client uses a minimal query: query { me { id } }
Custom Query Examples:
meQuery: `query { me { id email name } }`
meQuery: `query {
me {
id
email
firstName
lastName
createdAt
updatedAt
profile {
avatar
bio
}
}
}`
meQuery: `query {
me {
id
email
name
roles {
id
name
permissions
}
department {
id
name
}
}
}`
Important Notes:
- The query must be a valid GraphQL query that returns user data under the
me
field
- The returned data structure MUST match your user type interface exactly
- The query is executed during authentication status checks and after successful login
- Failure to provide a matching
meQuery
for custom types will result in incomplete user data and runtime errors
🚨 DEPENDENCY WARNING: This is not an optional configuration when using custom types. The meQuery
and your user type interface are tightly coupled and must be kept in sync.
Types
AuthState
interface AuthState<TUser> {
user: TUser | null;
isLoading: boolean;
isAuthenticated: boolean;
accessToken: string | null;
error: string | null;
}
User Interface Example
interface User {
id: string;
email: string;
name?: string;
[key: string]: any;
}
Type-Safe Usage with Custom User Types
The createAuthClient function requires explicit type parameters for enhanced type safety:
interface MyUser {
id: string;
email: string;
name?: string;
avatar?: string;
roles?: string[];
department?: string;
}
const authClient = createAuthClient<MyUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: window.location.origin + '/callback',
scope: 'openid profile email',
meQuery: `query {
me {
id
email
name
avatar
roles
department
}
}`
});
const user: MyUser | null = authClient.getUser();
const state: AuthState<MyUser> = authClient.getState();
function useAuth() {
const [user, setUser] = useState<MyUser | null>(null);
const [authClient] = useState(() => createAuthClient<MyUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
meQuery: `query {
me {
id
email
name
avatar
roles
department
}
}`
}));
useEffect(() => {
const unsubscribe = authClient.addEventListener((event) => {
if (event.type === 'auth_state_changed') {
const authState = event.data as AuthState<MyUser>;
if (authState.user) {
console.log('User department:', authState.user.department);
console.log('User roles:', authState.user.roles);
}
}
});
return unsubscribe;
}, [authClient]);
return { user, authClient };
}
AuthEvent
interface AuthEvent {
type: 'auth_state_changed' | 'auth_error' | 'auth_loading' | 'token_refresh';
data?: any;
}
Event Types:
auth_state_changed
: Fired when authentication state changes (login, logout, user data update)
auth_error
: Fired when authentication errors occur
auth_loading
: Fired when authentication operations start/end
token_refresh
: Fired when tokens are successfully refreshed (both manual and automatic)
Token Refresh Event Example:
authClient.addEventListener((event) => {
if (event.type === 'token_refresh') {
console.log('Token refresh successful:', event.data);
}
});
Tailor Platform Integration
Environment Setup
For Tailor Platform integration, you'll typically need these configuration values:
const authClient = createAuthClient<User>({
clientId: process.env.REACT_APP_TAILOR_CLIENT_ID!,
appUri: process.env.REACT_APP_TAILOR_APP_URI!,
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile email'
});
Environment Variables
Create a .env
file in your project root:
REACT_APP_TAILOR_CLIENT_ID=your-client-id
REACT_APP_TAILOR_APP_URI=https://your-tailor-app-xxxxxxxx.erp.dev
GraphQL Integration
The client uses HTTPOnly cookies for authentication, allowing you to use any GraphQL client library:
const query = `
query GetUserData {
me {
id
email
name
}
userProjects {
id
name
createdAt
}
}
`;
const response = await fetch('https://your-tailor-app-xxxxxxxx.erp.dev/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tailor-Nonce': crypto.randomUUID()
},
credentials: 'include',
body: JSON.stringify({ query })
});
Security Features
Tailor Platform Authentication Flow
The library implements the Tailor Platform's "browser client" authentication flow with PKCE-like parameters for enhanced security.
State Parameter Validation
Built-in CSRF protection through state parameter validation during authentication callback handling.
Automatic generation and inclusion of X-Tailor-Nonce
headers with each GraphQL request for additional CSRF protection.
HTTPOnly Cookies
Authentication tokens are stored in HTTPOnly cookies on the Tailor Platform server, preventing XSS attacks on token storage.
Server-side Token Management
Token refresh and management are handled server-side through the HTTPOnly cookie mechanism, eliminating client-side token handling.
Automatic Token Refresh
The library includes automatic token refresh functionality:
- Automatic Refresh: Tokens are automatically refreshed during
checkAuthStatus()
calls when expired
- Manual Refresh: Use
refreshTokens()
method for manual token refresh
- Event Notification: Both automatic and manual refresh operations emit
token_refresh
events
- Transparent Operation: Token refresh occurs transparently without user intervention
- Fallback Handling: If automatic refresh fails, the user is marked as unauthenticated
await authClient.checkAuthStatus();
await authClient.refreshTokens();
authClient.addEventListener((event) => {
if (event.type === 'token_refresh') {
console.log('Tokens have been refreshed');
}
});
Error Handling
The library provides comprehensive error handling with detailed error information:
authClient.addEventListener((event) => {
if (event.type === 'auth_error') {
const error = event.data;
if (error.error === 'access_denied') {
console.log('User cancelled authentication');
} else if (error.error === 'invalid_request') {
console.log('Invalid authentication request');
} else if (error.error === 'invalid_client') {
console.log('Client authentication failed');
} else {
console.error('Authentication error:', error);
}
}
});
const { error } = authClient.getState();
if (error) {
console.error('Auth error:', error);
}
Common Error Scenarios
- Invalid state parameter: CSRF protection triggered
- Token exchange failed: Network or server issues during authentication flow
- Authentication check failed: Issues validating existing sessions with Tailor Platform
- Invalid redirect_uri: Configuration mismatch with Tailor Platform browser client settings
Best Practices
Type and Query Consistency
1. Design Type-First Approach
interface AppUser {
id: string;
email: string;
firstName?: string;
lastName?: string;
avatar?: string;
roles: string[];
department: {
id: string;
name: string;
};
}
const meQuery = `query {
me {
id
email
firstName
lastName
avatar
roles
department {
id
name
}
}
}`;
2. Verify Type-Query Alignment
Create a utility function to validate the alignment:
function validateTypeQueryAlignment<T>(
sampleUser: T,
actualUser: any
): void {
const expectedFields = Object.keys(sampleUser as any);
const actualFields = Object.keys(actualUser || {});
const missingFields = expectedFields.filter(field => !actualFields.includes(field));
if (missingFields.length > 0) {
console.warn('⚠️ Type-Query mismatch detected!');
console.warn('Missing fields in query response:', missingFields);
console.warn('Update your meQuery to include these fields');
}
}
const authClient = createAuthClient<AppUser>({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
meQuery: `query {
me {
id
email
firstName
lastName
avatar
roles
department {
id
name
}
}
}`
});
authClient.addEventListener((event) => {
if (event.type === 'auth_state_changed' && event.data.user) {
if (process.env.NODE_ENV === 'development') {
const sampleUser: AppUser = {
id: '',
email: '',
firstName: '',
lastName: '',
avatar: '',
roles: [],
department: { id: '', name: '' }
};
validateTypeQueryAlignment(sampleUser, event.data.user);
}
}
});
3. Incremental Development Approach
interface BasicUser {
id: string;
email: string;
}
interface ProfileUser extends BasicUser {
firstName?: string;
lastName?: string;
avatar?: string;
}
interface FullUser extends ProfileUser {
roles: string[];
department: {
id: string;
name: string;
};
}
4. Handle Optional vs Required Fields
interface WellDefinedUser {
id: string;
email: string;
createdAt: string;
firstName?: string;
lastName?: string;
avatar?: string;
roles: string[];
profile: {
bio?: string;
location?: string;
} | null;
}
5. Testing Your Configuration
async function testAuthConfiguration() {
const authClient = createAuthClient<YourUserType>({
clientId: 'test-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
meQuery: `query {
me {
id
email
name
// Add other fields that match your YourUserType interface
}
}`
});
try {
const response = await fetch('https://your-workspace.tailorplatform.com/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
query: `your meQuery here`
})
});
const data = await response.json();
console.log('GraphQL query test result:', data);
if (data.data?.me) {
console.log('✅ Query executed successfully');
console.log('Available fields:', Object.keys(data.data.me));
}
} catch (error) {
console.error('❌ Query test failed:', error);
}
}
if (process.env.NODE_ENV === 'development') {
testAuthConfiguration();
}
Development Workflow
- Define User Type Interface: Start with the data structure you need
- Create Matching meQuery: Write a GraphQL query that fetches all required fields
- Test Query Independently: Verify the query works with your GraphQL endpoint
- Implement Auth Client: Configure with the type and query
- Add Debug Logging: Monitor the actual data received during development
- Validate in Production: Ensure consistency across different environments
Development
Building the Library
npm install
npm run build
npm run clean
Project Structure
auth-browser-client/
├── src/
│ ├── AuthClient.ts # Main authentication function (createAuthClient)
│ ├── index.ts # Entry point and exports
│ ├── types/ # Type definitions
│ │ ├── auth.ts # Authentication-related types
│ │ └── config.ts # Configuration types
│ ├── internal/ # Internal implementation modules
│ │ ├── store.ts # State management
│ │ ├── auth-operations.ts # Authentication operations
│ │ ├── event-system.ts # Event handling system
│ │ ├── config-management.ts # Configuration management
│ │ └── initialization.ts # Initialization logic
│ └── utils/ # Utility functions
├── dist/ # Built files (generated)
│ ├── index.js # CommonJS build
│ ├── index.d.ts # TypeScript definitions
│ └── esm/ # ES Modules build
├── package.json # Package configuration
├── tsconfig.json # TypeScript config (CommonJS)
├── tsconfig.esm.json # TypeScript config (ES Modules)
└── README.md # This file
Browser Compatibility
- Modern Browsers: Chrome 80+, Firefox 74+, Safari 13.1+, Edge 80+
- ES2022 Features: BigInt, Dynamic imports, Nullish coalescing, Optional chaining, Error.cause, crypto.randomUUID
- Required APIs: Fetch API, Crypto.subtle (for PKCE), URLSearchParams
Copyright © 2025 Tailor Inc.