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

  1. PKCE Always Required: ChatMinder automatically generates PKCE parameters for enhanced security
  2. Secure Redirect URIs: Use custom URL schemes for mobile apps (myapp://oauth-callback)
  3. State Parameter Validation: Always verify the state parameter to prevent CSRF attacks
  4. Code Verifier Storage: Securely store code verifiers during the OAuth flow

Token Management

  1. 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
  2. Token Expiration: Implement proper token lifecycle management
  3. Token Rotation: Plan for refresh token implementation (future feature)

Transport Security

  1. HTTPS Only: All authentication endpoints require HTTPS in production
  2. Certificate Pinning: Implement certificate pinning for mobile apps
  3. 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 parameter
  • TOKEN_EXCHANGE_FAILED: Invalid or expired authorization code
  • USER_INFO_FAILED: Provider API error during user info retrieval
  • OAUTH_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

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);
    }
}