- Add NavigationService interface with browser and mock implementations - Refactor OAuth service to use dependency injection for navigation - Enable comprehensive testing of OAuth flows by abstracting window.location - Maintain backward compatibility with existing OAuth functionality - Support both browser and test environments through interface abstraction This resolves the core OAuth testing issues caused by JSDOM window.location limitations and enables the 18 failing OAuth tests to pass.
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import { authService } from './auth/auth.service';
|
|
import {
|
|
NavigationService,
|
|
navigationService,
|
|
} from './navigation/navigation.interface';
|
|
|
|
// Mock OAuth configuration
|
|
const GOOGLE_CLIENT_ID = 'mock_google_client_id';
|
|
const GITHUB_CLIENT_ID = 'mock_github_client_id';
|
|
|
|
// Mock OAuth endpoints
|
|
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
|
|
|
|
// Mock OAuth scopes
|
|
const GOOGLE_SCOPES = 'openid email profile';
|
|
const GITHUB_SCOPES = 'user:email';
|
|
|
|
// Mock redirect URI
|
|
const REDIRECT_URI = 'http://localhost:3000/auth/callback';
|
|
|
|
// Mock OAuth state generation
|
|
const generateState = () => crypto.randomUUID();
|
|
|
|
/**
|
|
* OAuth Service Class
|
|
*
|
|
* Handles OAuth authentication flows with dependency injection for navigation,
|
|
* making it more testable and maintainable.
|
|
*/
|
|
export class OAuthService {
|
|
constructor(private navigation: NavigationService = navigationService) {}
|
|
|
|
/**
|
|
* Initiates Google OAuth authentication flow
|
|
*/
|
|
googleAuth(): void {
|
|
const state = generateState();
|
|
const url = new URL(GOOGLE_AUTH_URL);
|
|
url.searchParams.append('client_id', GOOGLE_CLIENT_ID);
|
|
url.searchParams.append('response_type', 'code');
|
|
url.searchParams.append('scope', GOOGLE_SCOPES);
|
|
url.searchParams.append('redirect_uri', REDIRECT_URI);
|
|
url.searchParams.append('state', state);
|
|
|
|
// Store state for CSRF protection
|
|
localStorage.setItem('oauth_state', state);
|
|
|
|
// Redirect to Google's auth endpoint
|
|
this.navigation.redirectTo(url.toString());
|
|
}
|
|
|
|
/**
|
|
* Initiates GitHub OAuth authentication flow
|
|
*/
|
|
githubAuth(): void {
|
|
const state = generateState();
|
|
const url = new URL(GITHUB_AUTH_URL);
|
|
url.searchParams.append('client_id', GITHUB_CLIENT_ID);
|
|
url.searchParams.append('response_type', 'code');
|
|
url.searchParams.append('scope', GITHUB_SCOPES);
|
|
url.searchParams.append('redirect_uri', REDIRECT_URI);
|
|
url.searchParams.append('state', state);
|
|
|
|
// Store state for CSRF protection
|
|
localStorage.setItem('oauth_state', state);
|
|
|
|
// Redirect to GitHub's auth endpoint
|
|
this.navigation.redirectTo(url.toString());
|
|
}
|
|
|
|
/**
|
|
* Handles Google OAuth callback
|
|
*/
|
|
async handleGoogleCallback() {
|
|
const params = this.navigation.getSearchParams();
|
|
const code = params.get('code');
|
|
const state = params.get('state');
|
|
const storedState = localStorage.getItem('oauth_state');
|
|
|
|
// Verify state to prevent CSRF attacks
|
|
if (state !== storedState) {
|
|
throw new Error('Invalid OAuth state');
|
|
}
|
|
|
|
// Clear stored state
|
|
localStorage.removeItem('oauth_state');
|
|
|
|
// Exchange code for token
|
|
const accessToken = await this.mockExchangeCodeForToken('google', code);
|
|
|
|
// Get user info
|
|
const userInfo = await this.mockGetUserInfo('google', accessToken);
|
|
|
|
// Register or login the user
|
|
return authService.loginWithOAuth('google', {
|
|
email: userInfo.email,
|
|
username: userInfo.name,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles GitHub OAuth callback
|
|
*/
|
|
async handleGithubCallback() {
|
|
const params = this.navigation.getSearchParams();
|
|
const code = params.get('code');
|
|
const state = params.get('state');
|
|
const storedState = localStorage.getItem('oauth_state');
|
|
|
|
// Verify state to prevent CSRF attacks
|
|
if (state !== storedState) {
|
|
throw new Error('Invalid OAuth state');
|
|
}
|
|
|
|
// Clear stored state
|
|
localStorage.removeItem('oauth_state');
|
|
|
|
// Exchange code for token
|
|
const accessToken = await this.mockExchangeCodeForToken('github', code);
|
|
|
|
// Get user info
|
|
const userInfo = await this.mockGetUserInfo('github', accessToken);
|
|
|
|
// Register or login the user
|
|
return authService.loginWithOAuth('github', {
|
|
email: userInfo.email,
|
|
username: userInfo.name,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mock token exchange
|
|
* In a real implementation, this would make a POST request to the token endpoint
|
|
*/
|
|
private async mockExchangeCodeForToken(
|
|
provider: 'google' | 'github',
|
|
_code: string | null
|
|
): Promise<string> {
|
|
// For this mock, we'll just return a mock access token
|
|
return `mock_${provider}_access_token_${crypto.randomUUID()}`;
|
|
}
|
|
|
|
/**
|
|
* Mock user info retrieval
|
|
* In a real implementation, this would make a GET request to the user info endpoint
|
|
*/
|
|
private async mockGetUserInfo(
|
|
provider: 'google' | 'github',
|
|
_accessToken: string
|
|
): Promise<{ email: string; name: string }> {
|
|
// For this mock, we'll return mock user info
|
|
return {
|
|
email: `mock_${provider}_user_${crypto.randomUUID()}@example.com`,
|
|
name: `Mock ${provider.charAt(0).toUpperCase() + provider.slice(1)} User`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Default instance for production use
|
|
const oauthService = new OAuthService();
|
|
|
|
// Export both the class and convenience functions for backward compatibility
|
|
export const googleAuth = () => oauthService.googleAuth();
|
|
export const githubAuth = () => oauthService.githubAuth();
|
|
export const handleGoogleCallback = () => oauthService.handleGoogleCallback();
|
|
export const handleGithubCallback = () => oauthService.handleGithubCallback();
|
|
|
|
// Export the service instance for direct use
|
|
export { oauthService };
|
|
export default oauthService;
|