@hamak/auth
v0.5.1
Published
Auth - Complete authentication plugin with password, OAuth2, and Keycloak support
Maintainers
Readme
@hamak/auth
A complete authentication system for the Hamak App Framework, supporting password-based login, OAuth2, and Keycloak authentication.
Features
- 🔐 Multiple Authentication Strategies - Password, OAuth2, Keycloak
- 🔄 Automatic Token Refresh - Proactive refresh before expiry
- 💾 Flexible Token Storage - localStorage, sessionStorage, or memory
- 🛡️ PKCE Support - Secure OAuth2 for public clients
- ⚛️ React Integration - Providers, hooks, and guard components
- 🏪 Redux Integration - Auth state in your store
- 🔌 Plugin Architecture - Integrates with microkernel plugin system
Packages
| Package | Description |
|---------|-------------|
| @hamak/auth-api | Interfaces, types, and DI tokens |
| @hamak/auth-spi | Extension points for custom strategies |
| @hamak/auth-impl | Core implementations and plugin factory |
| @hamak/auth-templates | React providers, hooks, and components |
Installation
npm install @hamak/auth-api @hamak/auth-spi @hamak/auth-impl @hamak/auth-templatesQuick Start
1. Configure the Auth Plugin
import { createAuthPlugin } from '@hamak/auth-impl';
// Password-based authentication
const authPlugin = createAuthPlugin({
strategy: 'password',
password: {
loginEndpoint: '/api/auth/login',
refreshEndpoint: '/api/auth/refresh',
logoutEndpoint: '/api/auth/logout',
userInfoEndpoint: '/api/auth/me'
},
tokenStorage: 'localStorage',
autoRefresh: {
enabled: true,
threshold: 60000 // Refresh 1 min before expiry
}
});
// Or OAuth2
const oauthPlugin = createAuthPlugin({
strategy: 'oauth2',
oauth2: {
clientId: 'my-app',
authorizationUrl: 'https://auth.example.com/authorize',
tokenUrl: 'https://auth.example.com/token',
userInfoUrl: 'https://auth.example.com/userinfo',
redirectUri: 'http://localhost:3000/callback',
scope: ['openid', 'profile', 'email']
}
});
// Or Keycloak
const keycloakPlugin = createAuthPlugin({
strategy: 'keycloak',
keycloak: {
realm: 'my-realm',
serverUrl: 'https://keycloak.example.com',
clientId: 'my-app',
redirectUri: 'http://localhost:3000/callback',
enableDirectGrant: true // Allow password login
}
});2. Register the Plugin
import { Host } from '@hamak/microkernel-impl';
const host = new Host();
host.registerPlugin('auth', manifest, authPlugin);
await host.bootstrapAllAtRoot();3. Setup React Provider
import { AuthProvider } from '@hamak/auth-templates';
import { AUTH_SERVICE_TOKEN } from '@hamak/auth-api';
function App() {
const [authService, setAuthService] = useState(null);
useEffect(() => {
host.bootstrapAllAtRoot().then(() => {
const ctx = host.rootActivationCtx;
setAuthService(ctx.resolve(AUTH_SERVICE_TOKEN));
});
}, []);
if (!authService) return <Loading />;
return (
<AuthProvider authService={authService}>
<YourApp />
</AuthProvider>
);
}4. Use Auth in Components
import { useAuth, useLogin, useLogout, RequireAuth, RequireRole } from '@hamak/auth-templates';
// Login form
function LoginPage() {
const { loginWithPassword, loading, error } = useLogin();
const handleSubmit = async (e) => {
e.preventDefault();
const result = await loginWithPassword(username, password);
if (result.success) {
navigate('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
{error && <Alert>{error.message}</Alert>}
<input name="username" />
<input name="password" type="password" />
<button disabled={loading}>Login</button>
</form>
);
}
// Protected content
function Dashboard() {
const { user } = useAuth();
const { logout } = useLogout();
return (
<RequireAuth fallback={<Loading />}>
<h1>Welcome, {user?.name}</h1>
<RequireRole role="admin">
<AdminPanel />
</RequireRole>
<button onClick={logout}>Logout</button>
</RequireAuth>
);
}Available Hooks
| Hook | Description |
|------|-------------|
| useAuth() | Full auth context with state and operations |
| useAuthState() | Auth state only (isAuthenticated, user, loading) |
| useUser() | Current user or null |
| useLogin() | Login operations with loading/error state |
| useLogout() | Logout operation with loading state |
| useRoles() | Role checking utilities |
| usePermissions() | Permission checking utilities |
| useOAuthCallback() | Handle OAuth redirect callbacks |
Guard Components
// Require authentication
<RequireAuth fallback={<Loading />}>
<ProtectedContent />
</RequireAuth>
// Require specific role
<RequireRole role="admin" unauthorizedContent={<AccessDenied />}>
<AdminPanel />
</RequireRole>
// Require any of multiple roles
<RequireRole roles={['admin', 'moderator']}>
<ModeratorTools />
</RequireRole>
// Require permission
<RequirePermission permission="document:write">
<EditButton />
</RequirePermission>Higher-Order Components
import { withAuth, withRole, withPermission } from '@hamak/auth-templates';
// Protect a component
const ProtectedDashboard = withAuth(Dashboard, {
fallback: <Loading />,
onUnauthenticated: () => navigate('/login')
});
// Require role
const AdminPanel = withRole(Panel, {
role: 'admin',
unauthorizedComponent: AccessDenied
});Plugin Commands
The auth plugin registers these commands:
ctx.commands.run('auth.login', credentials);
ctx.commands.run('auth.logout');
ctx.commands.run('auth.refresh');
ctx.commands.run('auth.isAuthenticated');
ctx.commands.run('auth.getCurrentUser');
ctx.commands.run('auth.hasRole', 'admin');
ctx.commands.run('auth.hasPermission', 'document:write');
ctx.commands.run('auth.initiateOAuth', 'keycloak');Plugin Events
Subscribe to auth events via hooks:
ctx.hooks.on('auth:login-success', ({ user }) => {
console.log('User logged in:', user);
});
ctx.hooks.on('auth:logout', () => {
console.log('User logged out');
});
ctx.hooks.on('auth:session-expired', () => {
console.log('Session expired');
});
ctx.hooks.on('auth:token-refresh', ({ user }) => {
console.log('Token refreshed');
});Configuration Options
interface AuthPluginConfig {
// Primary strategy
strategy: 'password' | 'oauth2' | 'keycloak';
// Password strategy config
password?: {
loginEndpoint: string;
refreshEndpoint: string;
logoutEndpoint: string;
userInfoEndpoint?: string;
};
// OAuth2 config
oauth2?: {
clientId: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl?: string;
redirectUri: string;
scope: string[];
usePkce?: boolean; // default: true
};
// Keycloak config
keycloak?: {
realm: string;
serverUrl: string;
clientId: string;
redirectUri: string;
scope?: string[];
enableDirectGrant?: boolean;
};
// Storage
tokenStorage?: 'localStorage' | 'sessionStorage' | 'memory';
storageKeyPrefix?: string;
// Auto refresh
autoRefresh?: {
enabled: boolean;
threshold?: number; // ms before expiry
checkInterval?: number; // ms between checks
};
// Routes
routes?: {
afterLogin?: string;
afterLogout?: string;
login?: string;
unauthorized?: string;
};
debug?: boolean;
}Custom Strategy
Implement your own authentication strategy:
import type { IAuthStrategy } from '@hamak/auth-spi';
class CustomStrategy implements IAuthStrategy {
readonly type = 'custom';
readonly name = 'my-custom-auth';
async authenticate(credentials) {
// Your authentication logic
return { success: true, user, accessToken, refreshToken };
}
async refreshToken(refreshToken) {
// Your refresh logic
return { success: true, accessToken };
}
async logout(accessToken) {
// Your logout logic
}
}Security Considerations
- PKCE: Enabled by default for OAuth2 flows
- State Parameter: CSRF protection for OAuth callbacks
- Token Storage: Use
memoryfor highest security (tokens lost on refresh) - Auto Refresh: Prevents session expiry during active use
- JWT Validation: Server-side validation required; client-side is for UX only
Demo
Run the demo app to see auth in action:
npm run demo:devNavigate to the Auth Demo tab to test:
- Login with
demo/demo123(user + editor roles) - Login with
admin/admin123(all roles) - Login with
guest/guest123(guest role only)
API Reference
See DESIGN.md for detailed architecture documentation.
License
MIT
