@instawork/oauth-login
v1.2.0
Published
Easily add Login with Instawork OAuth2 PKCE flow to your app.
Readme
@instawork/oauth-login
Easily add "Login with Instawork" OAuth2 PKCE flow to your web application. This package provides a secure, TypeScript-first implementation of OAuth 2.0 with PKCE (Proof Key for Code Exchange) for client-side applications.
✨ Features
- 🔒 Secure PKCE Implementation - Protects against authorization code interception attacks
- 📘 TypeScript First - Full type safety with comprehensive interfaces and JSDoc
- ⚛️ React Ready - Pre-built React components and hooks
- 🎨 Customizable UI - Styled button component with customization options
- 🚀 Easy Integration - Simple API with sensible defaults
- 🛡️ Error Handling - Comprehensive error handling with detailed messages
- 📱 Browser Support - Works in all modern browsers with Web Crypto API
📦 Installation
npm install @instawork/oauth-login🚀 Quick Start
Generate client id
Follow instructions here: https://instawork.atlassian.net/wiki/spaces/EN/pages/4194697219/OAuth+Setup
React Component (Recommended)
import { InstaworkLoginButton, setupOAuthListener, handleOAuthRedirect } from '@instawork/oauth-login';
import { useEffect } from 'react';
function LoginPage() {
// Set up listener for popup OAuth callback
useEffect(() => {
const removeListener = setupOAuthListener(
(tokens) => {
// Store tokens and redirect user
localStorage.setItem('access_token', tokens.access_token);
window.location.href = '/dashboard';
},
(error) => console.error('Login failed:', error)
);
return removeListener; // Cleanup on unmount
}, []);
return (
<InstaworkLoginButton
clientId="YOUR_CLIENT_ID"
redirectUri="https://yourapp.com/oauth/callback"
onError={(error) => console.error('Login failed:', error)}
/>
);
}
// On your callback page (handles popup callback)
function CallbackPage() {
React.useEffect(() => {
handleOAuthRedirect({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
serverCallbackUrl: 'https://yourapp.com/api/oauth/callback'
}).catch(error => {
console.error('OAuth callback failed:', error);
});
}, []);
return <div>Processing login...</div>;
}Vanilla JavaScript/TypeScript
import { loginWithInstawork, setupOAuthListener, handleOAuthRedirect, isOAuthCallback } from '@instawork/oauth-login';
// Set up listener for popup OAuth callback (on main page)
setupOAuthListener(
(tokens) => {
console.log('Login successful!', tokens);
localStorage.setItem('access_token', tokens.access_token);
window.location.href = '/dashboard';
},
(error) => {
console.error('Login failed:', error);
}
);
// Initiate login
document.getElementById('login-btn')?.addEventListener('click', async () => {
try {
await loginWithInstawork({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback'
// scope: 'openid profile email' // Optional - only include if needed
});
} catch (error) {
console.error('Login initiation failed:', error);
}
});
// Handle callback (on your redirect page - callback.html)
if (isOAuthCallback()) {
handleOAuthRedirect({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
serverCallbackUrl: 'https://yourapp.com/api/oauth/callback'
}).catch(error => {
console.error('OAuth callback failed:', error);
});
}📚 API Reference
loginWithInstawork(config)
Initiates the OAuth login flow by opening Instawork's authorization server in a popup window.
Note: The login page opens in a popup window to preserve the application state and localStorage. Make sure your application allows popups.
interface OAuthLoginConfig {
clientId: string; // Your OAuth client ID
redirectUri: string; // Registered redirect URI
scope?: string; // OAuth scopes (optional - no default)
baseUrl?: string; // Custom base URL (optional, defaults to https://www.instawork.com)
}
// Optional: Listen for authentication success in the parent window
window.addEventListener('message', (event) => {
if (event.origin === window.location.origin && event.data.type === 'oauth-success') {
console.log('User authenticated!', event.data.tokens);
// Update your application state here
}
});handleOAuthRedirect(config)
Handles the OAuth callback and sends the authorization code to your server for token exchange.
Important: This function now requires a server-side endpoint to handle the actual token exchange with Instawork. This is more secure as it keeps your OAuth flow server-side.
interface OAuthRedirectConfig {
clientId: string; // Your OAuth client ID
redirectUri: string; // Same redirect URI used in login
serverCallbackUrl: string; // Your server endpoint to handle token exchange
baseUrl?: string; // Custom base URL (optional, defaults to https://www.instawork.com)
}
interface OAuthTokenResponse {
access_token: string; // API access token
token_type: string; // Token type (usually 'Bearer')
expires_in: number; // Token expiration in seconds
refresh_token?: string; // Refresh token (if available)
id_token?: string; // JWT ID token with user info
scope?: string; // Granted scopes
}InstaworkLoginButton
Pre-styled React button component with built-in OAuth handling.
interface InstaworkLoginButtonProps {
clientId: string;
redirectUri: string;
scope?: string; // Optional OAuth scopes
baseUrl?: string; // Custom base URL (optional, defaults to https://www.instawork.com)
children?: React.ReactNode; // Button text
className?: string; // CSS class
style?: React.CSSProperties; // Inline styles
disabled?: boolean; // Disabled state
loading?: boolean; // Loading state
onClick?: () => void | Promise<void>; // Pre-login callback
onError?: (error: Error) => void; // Error handler
}useInstaworkLogin() Hook
React hook for managing OAuth login state.
const { login, isLoading, error, clearError } = useInstaworkLogin();
// Usage
const handleLogin = () => {
login({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
});
};setupOAuthListener(onSuccess, onError?)
Set up a listener to handle OAuth callbacks from popup windows. Call this once when your application initializes.
function setupOAuthListener(
onSuccess: (tokens: OAuthTokenResponse) => void,
onError?: (error: Error) => void
): () => void
// Usage
const removeListener = setupOAuthListener(
(tokens) => {
console.log('Authenticated!', tokens);
// Handle successful authentication
},
(error) => {
console.error('Auth failed:', error);
// Handle authentication error
}
);
// Returns a cleanup function to remove the listener
// Call this when your component unmounts
removeListener();isOAuthCallback()
Utility function to check if the current page is handling an OAuth callback.
if (isOAuthCallback()) {
// Handle the OAuth callback
}🎨 Customization
Custom Button Styling
<InstaworkLoginButton
clientId="YOUR_CLIENT_ID"
redirectUri="https://yourapp.com/oauth/callback"
style={{
backgroundColor: '#custom-color',
borderRadius: '8px',
fontSize: '16px',
padding: '16px 32px'
}}
className="my-custom-class"
>
Sign in with Instawork
</InstaworkLoginButton>Error Handling
const handleOAuthError = (error: Error) => {
if (error.message.includes('Invalid state')) {
// Handle CSRF attack attempt
alert('Security error detected. Please try again.');
} else if (error.message.includes('Token exchange failed')) {
// Handle token exchange errors
alert('Login failed. Please try again.');
} else {
// Handle other errors
console.error('OAuth error:', error);
}
};🔧 Configuration
Environment Setup
- Register your application with Instawork to get your
clientId - Configure your redirect URI in the Instawork OAuth application settings
- That's it! The OAuth URLs are automatically configured for Instawork
Custom Base URL Configuration
For staging, development, or custom environments, you can specify a custom base URL:
// Using custom base URL for staging
await loginWithInstawork({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
baseUrl: 'https://staging.instawork.com'
});
// Using custom base URL with port
await loginWithInstawork({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
baseUrl: 'https://staging.instawork.com:8080'
});
// Also for callback handling (must use same base URL)
const tokens = await handleOAuthRedirect({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://yourapp.com/oauth/callback',
baseUrl: 'https://staging.instawork.com:8080' // Same as login
});Server-Side Implementation
The handleOAuthRedirect function sends the authorization code to your server for token exchange. You need to implement a server endpoint to handle this.
Request from Client:
POST /api/oauth/callback
Content-Type: application/json
{
"code": "authorization_code_from_oauth",
"redirect_uri": "https://yourapp.com/oauth/callback",
"client_id": "your_client_id",
"code_verifier": "pkce_code_verifier",
"state": "state_value",
"base_url": "https://www.instawork.com"
}Example Server Implementation (Node.js/Express):
app.post('/api/oauth/callback', async (req, res) => {
try {
const { code, redirect_uri, client_id, code_verifier, base_url } = req.body;
// Exchange authorization code for tokens
const tokenUrl = `${base_url}/oauth2/token/`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri,
client_id,
code_verifier,
// client_secret: process.env.OAUTH_CLIENT_SECRET, // If using confidential client
})
});
if (!response.ok) {
const errorText = await response.text();
return res.status(response.status).json({
error: 'Token exchange failed',
details: errorText
});
}
const tokens = await response.json();
// Store tokens securely (e.g., in session, database)
// Create user session
req.session.accessToken = tokens.access_token;
req.session.userId = parseJWT(tokens.id_token).sub;
// Return tokens to client (or just success status)
res.json(tokens);
} catch (error) {
console.error('OAuth callback error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Example Server Implementation (Python/Flask):
@app.route('/api/oauth/callback', methods=['POST'])
def oauth_callback():
try:
data = request.get_json()
code = data['code']
redirect_uri = data['redirect_uri']
client_id = data['client_id']
code_verifier = data['code_verifier']
base_url = data['base_url']
# Exchange authorization code for tokens
token_url = f"{base_url}/oauth2/token/"
response = requests.post(token_url, data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri,
'client_id': client_id,
'code_verifier': code_verifier,
# 'client_secret': os.environ.get('OAUTH_CLIENT_SECRET'), # If using confidential client
}, headers={
'Accept': 'application/json'
})
if not response.ok:
return jsonify({'error': 'Token exchange failed', 'details': response.text}), response.status_code
tokens = response.json()
# Store tokens securely (e.g., in session, database)
session['access_token'] = tokens['access_token']
session['user_id'] = parse_jwt(tokens['id_token'])['sub']
# Return tokens to client
return jsonify(tokens)
except Exception as e:
print(f'OAuth callback error: {e}')
return jsonify({'error': 'Internal server error'}), 500Popup Window Behavior
The OAuth login flow opens in a popup window to preserve your application's state and localStorage. This ensures:
- ✅ PKCE codes remain accessible in the parent window
- ✅ Your application state remains intact
- ✅ Better user experience with automatic popup closure after authentication
- ✅ Token exchange happens in the parent window context with proper localStorage access
Handling Popup Blockers:
If a popup blocker prevents the login window from opening, the library will throw an error. You can handle this gracefully:
try {
await loginWithInstawork({ clientId, redirectUri, serverCallbackUrl });
} catch (error) {
if (error.message.includes('popup')) {
alert('Please allow popups to continue with login');
}
}Listening for Authentication Success:
Use the setupOAuthListener function to handle OAuth callbacks from the popup:
import { setupOAuthListener } from '@instawork/oauth-login';
// Set up the listener when your app initializes
const removeListener = setupOAuthListener(
(tokens) => {
console.log('User authenticated!', tokens);
// Update your application state, redirect user, etc.
localStorage.setItem('access_token', tokens.access_token);
window.location.href = '/dashboard';
},
(error) => {
console.error('Authentication failed:', error);
alert('Login failed: ' + error.message);
}
);
// Clean up when needed (e.g., component unmount)
// removeListener();How It Works:
- Parent window calls
loginWithInstawork()- Generates PKCE codes and state
- Stores session in in-memory Map (indexed by state parameter)
- Also stores in localStorage as fallback
- Popup window opens for OAuth authentication
- OAuth provider redirects to callback URL in the popup
- Popup detects
oauth_popup_modeflag and extracts authorization code - Popup sends code + state to parent via
BroadcastChannel(orwindow.postMessageas fallback) - Popup closes automatically
- Parent's
setupOAuthListenerreceives the callback data - Parent looks up session from in-memory Map using state parameter
- Parent exchanges code for tokens using the in-memory PKCE codes
- Your callback receives the tokens
- Session is cleaned up from memory automatically
Why In-Memory Storage?
Popup windows and parent windows have separate localStorage contexts in many browsers, even on the same origin. By storing PKCE codes in the parent window's memory (using a Map), we ensure they're always accessible when needed. The state parameter acts as the key to look up the correct session.
Benefits:
- ✅ Works regardless of localStorage sharing behavior
- ✅ More secure (data only in memory, never persisted)
- ✅ Automatic cleanup of old sessions (>10 minutes)
- ✅ No race conditions or timing issues
Security Considerations
- Always validate the
stateparameter to prevent CSRF attacks (handled automatically by the client library) - Never expose your client secret - keep it on the server side only
- Store tokens securely on the server (use secure HTTP-only cookies or server-side sessions)
- Use HTTPS in production for all redirect URIs and API endpoints
- The PKCE flow provides additional security for client-side applications
- Implement proper session management and token refresh logic on your server
- Consider implementing rate limiting on your OAuth callback endpoint
- Validate postMessage origins - Always verify
event.originwhen listening for OAuth success messages
🛠️ Development
# Install dependencies
npm install
# Build the package
npm run build
# Development mode with file watching
npm run dev📚 Examples
Check out the comprehensive examples in the examples/ directory:
- Vanilla JavaScript - Complete HTML/JS implementation
- React + TypeScript - Modern React app with hooks and components
Quick Start
# Choose your example
cd examples/vanilla-js # or examples/react
# Install dependencies (auto-copies package files)
npm install
# Start development server
npm start
# Visit http://localhost:3000Both examples include:
- ✅ Complete setup instructions
- ✅ Working OAuth login and callback flows
- ✅ Error handling and validation
- ✅ Production deployment guidance
- ✅ Comprehensive documentation
📄 License
MIT © Instawork
🤝 Contributing
Issues and pull requests are welcome! Please read our contributing guidelines before submitting.
📞 Support
For OAuth application setup and API questions, please contact Instawork support.
