@tailor-platform/auth-browser-client
v0.3.0
Published
Browser client library for Tailor Platform authentication
Readme
@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-clientBASIC 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';
// Use default User type (no type arguments)
const authClient = createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev',
redirectUri: 'https://your-app.com/callback', // Optional: defaults to current origin
scope: 'openid profile email'
});
// Default meQuery is used: 'query { me { id } }'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';
// Define custom user type
interface CustomUser {
id: string;
email: string;
name?: string;
}
// Specify custom type and set corresponding meQuery
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 corresponding to custom type
meQuery: `query {
me {
id
email
name
}
}`
});Correspondence Between Custom Type and meQuery
⚠️ IMPORTANT: When using custom user types, you MUST provide a corresponding
meQuerythat 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
meQueryfields 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 correspondingmeQuerythat fetches all the fields defined in your custom type interface.
Why this matters:
- The library uses the
meQueryto 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
meQuerymust be a valid GraphQL query structure
🚨 WARNING: Ignoring this dependency will cause your application to behave unexpectedly. User properties may be
undefinedeven 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 = () => {
// Use default type (no type arguments)
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 = () => {
// Specify custom type and set corresponding meQuery
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'
});
// Listen for auth state changes
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';
}
}
});
// Login button
document.getElementById('login-btn').addEventListener('click', () => {
authClient.login();
});
// Logout button
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.
// Built-in User interface (default type)
interface User {
id: string;
[key: string]: any;
}
// Usage with default User type
const authClient = createAuthClient({
clientId: 'your-client-id',
appUri: 'https://your-tailor-app-xxxxxxxx.erp.dev'
});
// Usage with custom type
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; // OAuth client ID (required)
appUri: string; // Tailor Platform App URI (required)
redirectUri?: string; // Callback URL after authentication (optional, defaults to current origin)
scope?: string; // OAuth scopes (optional, default: 'openid profile email')
audience?: string; // OAuth audience (optional)
meQuery?: string; // Custom GraphQL query for user profile (optional, default: 'query { me { id } }')
}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);
});
// Later, unsubscribe
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.
// Manual token refresh
await authClient.refreshTokens();
// Listen for token refresh events
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_refreshevent 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
meQuerythat 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',
// Custom query to fetch additional user fields
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:
// Basic user info
meQuery: `query { me { id email name } }`
// Extended user profile
meQuery: `query {
me {
id
email
firstName
lastName
createdAt
updatedAt
profile {
avatar
bio
}
}
}`
// User with roles and permissions
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
mefield - 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
meQueryfor custom types will result in incomplete user data and runtime errors
🚨 DEPENDENCY WARNING: This is not an optional configuration when using custom types. The
meQueryand your user type interface are tightly coupled and must be kept in sync.
Types
AuthState
interface AuthState<TUser> {
user: TUser | null; // Current user data
isLoading: boolean; // Loading state indicator
isAuthenticated: boolean; // Authentication status
accessToken: string | null; // Access token (when available)
error: string | null; // Error message (if any)
}User Interface Example
interface User {
id: string;
email: string;
name?: string;
[key: string]: any; // Additional user properties
}Type-Safe Usage with Custom User Types
The createAuthClient function requires explicit type parameters for enhanced type safety:
// Define your application-specific user type
interface MyUser {
id: string;
email: string;
name?: string;
avatar?: string;
roles?: string[];
department?: string;
}
// Create a type-safe AuthClient
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 corresponding to MyUser interface
meQuery: `query {
me {
id
email
name
avatar
roles
department
}
}`
});
// Now all methods return the correct types
const user: MyUser | null = authClient.getUser();
const state: AuthState<MyUser> = authClient.getState();
// Type-safe React hook example
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) {
// TypeScript knows user has MyUser properties
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 occurauth_loading: Fired when authentication operations start/endtoken_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);
// event.data = { success: true }
}
});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.devGraphQL Integration
The client uses HTTPOnly cookies for authentication, allowing you to use any GraphQL client library:
// Example with a popular GraphQL client (Apollo Client, urql, etc.)
// The authentication cookies will be automatically included
const query = `
query GetUserData {
me {
id
email
name
}
userProjects {
id
name
createdAt
}
}
`;
// Example with fetch
const response = await fetch('https://your-tailor-app-xxxxxxxx.erp.dev/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tailor-Nonce': crypto.randomUUID() // CSRF protection
},
credentials: 'include', // Important: includes HTTPOnly cookies
body: JSON.stringify({ query })
});
// The auth-browser-client handles authentication,
// while you're free to use any GraphQL client for API callsSecurity 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.
X-Tailor-Nonce Headers
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_refreshevents - Transparent Operation: Token refresh occurs transparently without user intervention
- Fallback Handling: If automatic refresh fails, the user is marked as unauthenticated
// Automatic refresh is triggered during these operations:
await authClient.checkAuthStatus(); // Checks auth status and refreshes if needed
// Manual refresh can be called explicitly:
await authClient.refreshTokens(); // Manually refreshes tokens
// Both operations emit the same event:
authClient.addEventListener((event) => {
if (event.type === 'token_refresh') {
// Handle successful 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);
}
}
});
// Error handling in state
const { error } = authClient.getState();
if (error) {
// Handle authentication 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
// ✅ RECOMMENDED: Define your user type first
interface AppUser {
id: string;
email: string;
firstName?: string;
lastName?: string;
avatar?: string;
roles: string[];
department: {
id: string;
name: string;
};
}
// Then create the corresponding meQuery
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:
// Utility function to verify type-query alignment during development
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');
}
}
// Usage in development
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) {
// Only run in development
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
// ✅ Start with basic fields and gradually expand
// Phase 1: Basic user info
interface BasicUser {
id: string;
email: string;
}
// Phase 2: Add profile information
interface ProfileUser extends BasicUser {
firstName?: string;
lastName?: string;
avatar?: string;
}
// Phase 3: Add organizational data
interface FullUser extends ProfileUser {
roles: string[];
department: {
id: string;
name: string;
};
}4. Handle Optional vs Required Fields
// ✅ Be explicit about optional vs required fields
interface WellDefinedUser {
// Required fields (always present in your GraphQL schema)
id: string;
email: string;
createdAt: string;
// Optional fields (may be null/undefined)
firstName?: string;
lastName?: string;
avatar?: string;
// Arrays (provide default empty array structure)
roles: string[];
// Nested objects (define clear structure)
profile: {
bio?: string;
location?: string;
} | null;
}5. Testing Your Configuration
// ✅ Create a test configuration to verify setup
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
}
}`
});
// Test the meQuery independently
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);
// Verify the structure matches your type
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);
}
}
// Run during development
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
# Install dependencies
npm install
# Build CommonJS and ES Module versions
npm run build
# Clean build directory
npm run cleanProject 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 fileBrowser 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.
