mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix(auth): enforce MFA verification in OAuth browser login flow (#1470)
This commit is contained in:
@@ -155,7 +155,7 @@ Examples:
|
|||||||
async executeLogin(
|
async executeLogin(
|
||||||
token?: string,
|
token?: string,
|
||||||
yes?: boolean,
|
yes?: boolean,
|
||||||
showHeader: boolean = true
|
showHeader = true
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = token
|
const result = token
|
||||||
@@ -393,7 +393,7 @@ Examples:
|
|||||||
*/
|
*/
|
||||||
async performInteractiveAuth(
|
async performInteractiveAuth(
|
||||||
yes?: boolean,
|
yes?: boolean,
|
||||||
showHeader: boolean = true
|
showHeader = true
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
ui.displayBanner('Task Master Authentication');
|
ui.displayBanner('Task Master Authentication');
|
||||||
@@ -498,6 +498,7 @@ Examples:
|
|||||||
/**
|
/**
|
||||||
* Authenticate with browser using OAuth 2.0 with PKCE
|
* Authenticate with browser using OAuth 2.0 with PKCE
|
||||||
* Uses shared countdown timer from auth-ui.ts
|
* Uses shared countdown timer from auth-ui.ts
|
||||||
|
* Includes MFA handling if user has MFA enabled
|
||||||
*/
|
*/
|
||||||
private async authenticateWithBrowser(): Promise<AuthCredentials> {
|
private async authenticateWithBrowser(): Promise<AuthCredentials> {
|
||||||
const countdownTimer = new AuthCountdownTimer(AUTH_TIMEOUT_MS);
|
const countdownTimer = new AuthCountdownTimer(AUTH_TIMEOUT_MS);
|
||||||
@@ -535,6 +536,25 @@ Examples:
|
|||||||
|
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Check if MFA is required BEFORE showing failure message
|
||||||
|
if (
|
||||||
|
error instanceof AuthenticationError &&
|
||||||
|
error.code === 'MFA_REQUIRED'
|
||||||
|
) {
|
||||||
|
// Stop spinner without showing failure - MFA is required, not a failure
|
||||||
|
countdownTimer.stop('mfa');
|
||||||
|
|
||||||
|
if (!error.mfaChallenge?.factorId) {
|
||||||
|
throw new AuthenticationError(
|
||||||
|
'MFA challenge information missing',
|
||||||
|
'MFA_VERIFICATION_FAILED'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use shared MFA flow handler
|
||||||
|
return this.handleMFAVerification(error);
|
||||||
|
}
|
||||||
|
|
||||||
countdownTimer.stop('failure');
|
countdownTimer.stop('failure');
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -605,7 +625,7 @@ Examples:
|
|||||||
private async performTokenAuth(
|
private async performTokenAuth(
|
||||||
token: string,
|
token: string,
|
||||||
yes?: boolean,
|
yes?: boolean,
|
||||||
showHeader: boolean = true
|
showHeader = true
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
ui.displayBanner('Task Master Authentication');
|
ui.displayBanner('Task Master Authentication');
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import crypto from 'crypto';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { Session } from '@supabase/supabase-js';
|
import type { Session } from '@supabase/supabase-js';
|
||||||
import { TASKMASTER_VERSION } from '../../../common/constants/index.js';
|
import { TASKMASTER_VERSION } from '../../../common/constants/index.js';
|
||||||
import { getLogger } from '../../../common/logger/index.js';
|
import { getLogger } from '../../../common/logger/index.js';
|
||||||
import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
|
import type { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
|
||||||
import { getAuthConfig } from '../config.js';
|
import { getAuthConfig } from '../config.js';
|
||||||
import { ContextStore } from '../services/context-store.js';
|
import type { ContextStore } from '../services/context-store.js';
|
||||||
import {
|
import {
|
||||||
AuthConfig,
|
type AuthConfig,
|
||||||
AuthCredentials,
|
type AuthCredentials,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
CliData,
|
type CliData,
|
||||||
OAuthFlowOptions
|
type MFAChallenge,
|
||||||
|
type OAuthFlowOptions
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|
||||||
export class OAuthService {
|
export class OAuthService {
|
||||||
@@ -125,7 +126,7 @@ export class OAuthService {
|
|||||||
/**
|
/**
|
||||||
* Start the OAuth flow (internal implementation)
|
* Start the OAuth flow (internal implementation)
|
||||||
*/
|
*/
|
||||||
private async startFlow(timeout: number = 300000): Promise<AuthCredentials> {
|
private async startFlow(timeout = 300000): Promise<AuthCredentials> {
|
||||||
const state = this.generateState();
|
const state = this.generateState();
|
||||||
|
|
||||||
// Store the original state for verification
|
// Store the original state for verification
|
||||||
@@ -289,6 +290,10 @@ export class OAuthService {
|
|||||||
email: session.user.email
|
email: session.user.email
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if MFA is required for this user
|
||||||
|
// This will throw MFA_REQUIRED error if MFA verification is needed
|
||||||
|
await this.checkAndThrowIfMFARequired();
|
||||||
|
|
||||||
// Calculate expiration - can be overridden with TM_TOKEN_EXPIRY_MINUTES
|
// Calculate expiration - can be overridden with TM_TOKEN_EXPIRY_MINUTES
|
||||||
let expiresAt: string | undefined;
|
let expiresAt: string | undefined;
|
||||||
const tokenExpiryMinutes = process.env.TM_TOKEN_EXPIRY_MINUTES;
|
const tokenExpiryMinutes = process.env.TM_TOKEN_EXPIRY_MINUTES;
|
||||||
@@ -363,6 +368,10 @@ export class OAuthService {
|
|||||||
email: user?.email
|
email: user?.email
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if MFA is required for this user
|
||||||
|
// This will throw MFA_REQUIRED error if MFA verification is needed
|
||||||
|
await this.checkAndThrowIfMFARequired();
|
||||||
|
|
||||||
// Calculate expiration time - can be overridden with TM_TOKEN_EXPIRY_MINUTES
|
// Calculate expiration time - can be overridden with TM_TOKEN_EXPIRY_MINUTES
|
||||||
let expiresAt: string | undefined;
|
let expiresAt: string | undefined;
|
||||||
const tokenExpiryMinutes = process.env.TM_TOKEN_EXPIRY_MINUTES;
|
const tokenExpiryMinutes = process.env.TM_TOKEN_EXPIRY_MINUTES;
|
||||||
@@ -429,4 +438,42 @@ export class OAuthService {
|
|||||||
getAuthorizationUrl(): string | null {
|
getAuthorizationUrl(): string | null {
|
||||||
return this.authorizationUrl;
|
return this.authorizationUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if MFA is required and throw appropriate error if so
|
||||||
|
* This ensures OAuth flow enforces MFA when user has it enabled
|
||||||
|
*/
|
||||||
|
private async checkAndThrowIfMFARequired(): Promise<void> {
|
||||||
|
const mfaCheck = await this.supabaseClient.checkMFARequired();
|
||||||
|
|
||||||
|
if (mfaCheck.required) {
|
||||||
|
// MFA is required - check if we have complete factor information
|
||||||
|
if (!mfaCheck.factorId || !mfaCheck.factorType) {
|
||||||
|
this.logger.error('MFA required but factor information is incomplete', {
|
||||||
|
mfaCheck
|
||||||
|
});
|
||||||
|
throw new AuthenticationError(
|
||||||
|
'MFA is required but the server returned incomplete factor configuration. Please contact support or try re-enrolling MFA.',
|
||||||
|
'MFA_REQUIRED_INCOMPLETE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('MFA verification required after OAuth login', {
|
||||||
|
factorId: mfaCheck.factorId,
|
||||||
|
factorType: mfaCheck.factorType
|
||||||
|
});
|
||||||
|
|
||||||
|
const mfaChallenge: MFAChallenge = {
|
||||||
|
factorId: mfaCheck.factorId,
|
||||||
|
factorType: mfaCheck.factorType
|
||||||
|
};
|
||||||
|
|
||||||
|
throw new AuthenticationError(
|
||||||
|
'MFA verification required. Please provide your authentication code.',
|
||||||
|
'MFA_REQUIRED',
|
||||||
|
undefined,
|
||||||
|
mfaChallenge
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export type AuthErrorCode =
|
|||||||
| 'CODE_AUTH_FAILED'
|
| 'CODE_AUTH_FAILED'
|
||||||
| 'INVALID_CODE'
|
| 'INVALID_CODE'
|
||||||
| 'MFA_REQUIRED'
|
| 'MFA_REQUIRED'
|
||||||
|
| 'MFA_REQUIRED_INCOMPLETE'
|
||||||
| 'MFA_VERIFICATION_FAILED'
|
| 'MFA_VERIFICATION_FAILED'
|
||||||
| 'INVALID_MFA_CODE';
|
| 'INVALID_MFA_CODE';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user