Authentication Guide
ChatMinder provides flexible authentication options to support both web applications and mobile/API integrations.
Dual Authentication Support
ChatMinder supports both cookie-based and JWT-based authentication:
- Cookie Authentication: For web applications and Razor pages
- JWT Authentication: For mobile apps and API integrations
- Authorization Policies: Flexible policy system supporting both authentication methods
Option 1: Web Application Authentication (Identity UI)
For web applications, use the complete Identity UI with Google OAuth integration:
<!-- Login with built-in UI -->
<a href="/Identity/Account/Login">Sign In</a>
<!-- Key Identity pages available: -->
<!-- /Identity/Account/Register -->
<!-- /Identity/Account/Login -->
<!-- /Identity/Account/Logout -->
<!-- /Identity/Account/Manage/Index -->
Available Endpoints
- Registration:
/Identity/Account/Register - Login:
/Identity/Account/Login - Logout:
/Identity/Account/Logout - Profile Management:
/Identity/Account/Manage/Index - Two-Factor:
/Identity/Account/Manage/TwoFactorAuthentication
Option 2: Mobile/API Authentication (JWT)
For mobile apps and headless integrations:
User Registration
const registerResponse = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'SecurePassword123!',
name: 'John Doe'
})
});
const result = await registerResponse.json();
// Returns: { success: true, token: "jwt-token", user: { ... } }
if (result.success) {
console.log('User registered successfully');
}
User Login
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'user@example.com',
password: 'SecurePassword123!',
rememberMe: false
})
});
const { token, refreshToken } = await loginResponse.json();
localStorage.setItem('jwt_token', token);
Using JWT Tokens
// Include JWT token in API requests
const response = await fetch('/api/channels', {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('jwt_token')}`,
'Content-Type': 'application/json'
}
});
Token Refresh
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: localStorage.getItem('refresh_token')
})
});
const { token } = await refreshResponse.json();
localStorage.setItem('jwt_token', token);
Option 3: Google OAuth for Mobile
For mobile apps using Google OAuth with automatic PKCE security:
Step 1: Get Authorization URL
// Basic request (server auto-generates PKCE parameters)
const response = await fetch('/api/oauth/google/authorize-url?' +
new URLSearchParams({
redirectUri: 'myapp://oauth-callback'
}));
// Optional: Include custom state parameter for CSRF protection
const responseWithState = await fetch('/api/oauth/google/authorize-url?' +
new URLSearchParams({
redirectUri: 'myapp://oauth-callback',
state: 'custom-state-value'
}));
// Response includes auto-generated PKCE parameters for security
const { authorizationUrl, state, codeVerifier } = await response.json();
Step 2: Open Native Browser
// iOS: ASWebAuthenticationSession
// Android: Custom Tabs
// Web: Standard redirect
const authResult = await openAuthSession(authorizationUrl, 'myapp://oauth-callback');
// Extract authorization code from callback URL
// Example callback: myapp://oauth-callback?code=AUTH_CODE&state=STATE_VALUE
Step 3: Exchange Code for Token
const tokenResponse = await fetch('/api/oauth/google/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: authResult.code,
redirectUri: 'myapp://oauth-callback', // Must match Step 1
codeVerifier: codeVerifier, // From Step 1 response
state: state // Optional: for verification
})
});
const result = await tokenResponse.json();
// Returns: { success: true, token: "jwt-token", user: { ... } }
const jwtToken = result.token;
OAuth Provider Information
const providers = await fetch('/api/oauth/providers');
const providerData = await providers.json();
// Response format:
{
"providers": [
{
"name": "Google",
"displayName": "Google",
"authorizeEndpoint": "/api/oauth/google/authorize-url",
"callbackEndpoint": "/api/oauth/google/callback",
"requiredParameters": ["redirectUri"],
"optionalParameters": ["state", "codeChallenge", "codeChallengeMethod"],
"supportsPKCE": true,
"recommendedScopes": ["openid", "email", "profile"]
}
]
}
Advanced OAuth Options
Custom PKCE Parameters (Optional)
// For advanced security or custom implementations
const customPKCE = await fetch('/api/oauth/google/authorize-url?' +
new URLSearchParams({
redirectUri: 'myapp://oauth-callback',
codeChallenge: 'your-custom-challenge',
codeChallengeMethod: 'S256'
}));
Error Handling: For detailed OAuth error handling patterns and recovery strategies, see the Error Handling Guide.
Authentication Best Practices
Security Considerations
OAuth Security
- PKCE Always Required: ChatMinder automatically generates PKCE parameters for enhanced security
- Secure Redirect URIs: Use custom URL schemes for mobile apps (
myapp://oauth-callback) - State Parameter Validation: Always verify the state parameter to prevent CSRF attacks
- Code Verifier Storage: Securely store code verifiers during the OAuth flow
Token Management
- Secure Storage:
- iOS: Use Keychain Services for token storage
- Android: Use Android Keystore or EncryptedSharedPreferences
- Web: Use secure HttpOnly cookies or secure localStorage with encryption
- Token Expiration: Implement proper token lifecycle management
- Token Rotation: Plan for refresh token implementation (future feature)
Transport Security
- HTTPS Only: All authentication endpoints require HTTPS in production
- Certificate Pinning: Implement certificate pinning for mobile apps
- Request Validation: Validate all authentication responses for tampering
Implementation Patterns
Dual Authentication Support
// For web applications (cookie-based)
const webLogin = async (credentials) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include', // Include cookies
body: JSON.stringify(credentials)
});
// Cookies handled automatically
};
// For mobile/API (JWT-based)
const mobileLogin = async (credentials) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const { token } = await response.json();
await secureStorage.setItem('jwt_token', token);
};
Authentication State Management
class AuthManager {
constructor() {
this.token = null;
this.user = null;
}
async login(credentials) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
const error = await response.json();
throw new AuthError(error.message, error.errorCode);
}
const result = await response.json();
this.token = result.token;
this.user = result.user;
await this.secureStore(result.token);
return result;
} catch (error) {
throw this.handleAuthError(error);
}
}
async loginWithGoogle(redirectUri) {
// Step 1: Get authorization URL
const urlResponse = await fetch('/api/oauth/google/authorize-url?' +
new URLSearchParams({ redirectUri }));
const { authorizationUrl, state, codeVerifier } = await urlResponse.json();
// Step 2: Open browser and get code
const authResult = await this.openAuthBrowser(authorizationUrl, redirectUri);
// Step 3: Exchange code for token
const tokenResponse = await fetch('/api/oauth/google/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: authResult.code,
redirectUri: redirectUri,
codeVerifier: codeVerifier,
state: state
})
});
const result = await tokenResponse.json();
this.token = result.token;
this.user = result.user;
await this.secureStore(result.token);
return result;
}
async logout() {
try {
// Clear server session
await fetch('/api/auth/logout', {
method: 'POST',
headers: this.getAuthHeaders()
});
} finally {
// Always clear local state
this.token = null;
this.user = null;
await this.clearSecureStorage();
}
}
getAuthHeaders() {
return this.token ? {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
} : {};
}
handleAuthError(error) {
switch (error.errorCode) {
case 'MISSING_AUTH_CODE':
case 'MISSING_REDIRECT_URI':
return new Error('OAuth configuration error');
case 'TOKEN_EXCHANGE_FAILED':
return new Error('Failed to authenticate with provider');
case 'USER_CREATION_FAILED':
return new Error('Account creation failed');
case 'OAUTH_NOT_CONFIGURED':
return new Error('OAuth not available');
default:
return error;
}
}
}
Error Handling
For comprehensive authentication error handling, including error codes, recovery strategies, and implementation patterns, see the Error Handling Guide.
Quick reference for authentication-specific errors:
MISSING_AUTH_CODE: OAuth callback missing code parameterTOKEN_EXCHANGE_FAILED: Invalid or expired authorization codeUSER_INFO_FAILED: Provider API error during user info retrievalOAUTH_NOT_CONFIGURED: Server missing OAuth configuration
Protected User Authentication
When a guardian needs to act on behalf of a protected user:
// Switch to protected user context
const protectedUserLogin = await fetch(`/api/auth/login-protected-user/${protectedUserId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${guardianToken}`
}
});
if (protectedUserLogin.ok) {
// Guardian now has protected user context
// API calls will be made as the protected user
const protectedUserData = await fetch('/api/auth/user-data', {
headers: { 'Authorization': `Bearer ${guardianToken}` }
});
}
Logout Implementation
async function logout() {
try {
// Clear server-side session
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getCurrentToken()}`
}
});
} catch (error) {
// Log error but continue with local cleanup
console.warn('Server logout failed:', error);
} finally {
// Always clear local authentication state
await clearLocalAuthData();
}
}
async function clearLocalAuthData() {
// Clear secure storage
await secureStorage.removeItem('jwt_token');
await secureStorage.removeItem('refresh_token');
// Clear any cached user data
localStorage.removeItem('user_data');
// Reset application state
resetAuthenticationState();
}
Next Steps
- Core Concepts: Understand protection levels and user types
- Real-time Features: Set up SignalR with JWT authentication
- Architecture: Learn about the modular system design
Complete OAuth Integration Example
// Complete mobile OAuth implementation
class ChatMinderAuth {
async authenticateWithGoogle() {
try {
// 1. Get authorization URL with PKCE
const urlResponse = await fetch('/api/oauth/google/authorize-url?' +
new URLSearchParams({
redirectUri: 'com.yourapp.chatminder://oauth'
}));
const { authorizationUrl, state, codeVerifier } = await urlResponse.json();
// 2. Open native browser
const authResult = await this.openNativeBrowser(authorizationUrl);
// 3. Extract code from callback
const urlParams = new URLSearchParams(authResult.url.split('?')[1]);
const code = urlParams.get('code');
const returnedState = urlParams.get('state');
// 4. Validate state parameter
if (returnedState !== state) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// 5. Exchange code for JWT
const tokenResponse = await fetch('/api/oauth/google/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: code,
redirectUri: 'com.yourapp.chatminder://oauth',
codeVerifier: codeVerifier,
state: state
})
});
const result = await this.handleAuthenticationError(tokenResponse);
// 6. Store token securely and update app state
await this.storeAuthenticationData(result);
return result;
} catch (error) {
console.error('Google OAuth failed:', error);
throw error;
}
}
async openNativeBrowser(url) {
// Platform-specific implementations
if (Platform.OS === 'ios') {
return await InAppBrowser.openAuth(url, 'com.yourapp.chatminder://oauth');
} else {
return await InAppBrowser.openAuth(url, 'com.yourapp.chatminder://oauth');
}
}
async storeAuthenticationData(authData) {
await Keychain.setItem('jwt_token', authData.token);
await AsyncStorage.setItem('user_data', JSON.stringify(authData.user));
// Update app authentication state
this.setAuthenticatedUser(authData.user, authData.token);
}
}