diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index f576f38f9..0560d126a 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -198,7 +198,7 @@ export class AuthController { @Get('/oauth/:provider') async oauthLink(@Param('provider') provider: string, @Query() query: any) { - return this._authService.oauthLink(provider, query); + return await this._authService.oauthLink(provider, query); } @Post('/activate') @@ -235,10 +235,11 @@ export class AuthController { @Post('/oauth/:provider/exists') async oauthExists( @Body('code') code: string, + @Body('state') state: string, @Param('provider') provider: string, @Res({ passthrough: false }) response: Response ) { - const { jwt, token } = await this._authService.checkExists(provider, code); + const { jwt, token } = await this._authService.checkExists(provider, code, state); if (token) { return response.json({ token }); diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index 31e889e64..02fde9013 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -218,18 +218,18 @@ export class AuthService { return false; } - oauthLink(provider: string, query?: any) { + async oauthLink(provider: string, query?: any) { const providerInstance = ProvidersFactory.loadProvider( provider as Provider ); - return providerInstance.generateLink(query); + return await providerInstance.generateLink(query); } - async checkExists(provider: string, code: string) { + async checkExists(provider: string, code: string, state?: string) { const providerInstance = ProvidersFactory.loadProvider( provider as Provider ); - const token = await providerInstance.getToken(code); + const token = await providerInstance.getToken(code, state); const user = await providerInstance.getUser(token); if (!user) { throw new Error('Invalid user'); diff --git a/apps/backend/src/services/auth/providers.interface.ts b/apps/backend/src/services/auth/providers.interface.ts index 8000cc35c..2e42b7e0b 100644 --- a/apps/backend/src/services/auth/providers.interface.ts +++ b/apps/backend/src/services/auth/providers.interface.ts @@ -1,6 +1,6 @@ export interface ProvidersInterface { generateLink(query?: any): Promise | string; - getToken(code: string): Promise; + getToken(code: string, state?: string): Promise; getUser( providerToken: string ): Promise<{ email: string; id: string }> | false; diff --git a/apps/backend/src/services/auth/providers/farcaster.provider.ts b/apps/backend/src/services/auth/providers/farcaster.provider.ts index 91262c80b..25a05501e 100644 --- a/apps/backend/src/services/auth/providers/farcaster.provider.ts +++ b/apps/backend/src/services/auth/providers/farcaster.provider.ts @@ -10,7 +10,7 @@ export class FarcasterProvider implements ProvidersInterface { return ''; } - async getToken(code: string) { + async getToken(code: string, state?: string) { const data = JSON.parse(Buffer.from(code, 'base64').toString()); const status = await client.lookupSigner({ signerUuid: data.signer_uuid }); if (status.status === 'approved') { diff --git a/apps/backend/src/services/auth/providers/github.provider.ts b/apps/backend/src/services/auth/providers/github.provider.ts index 2d8b95ea4..56b7d8915 100644 --- a/apps/backend/src/services/auth/providers/github.provider.ts +++ b/apps/backend/src/services/auth/providers/github.provider.ts @@ -9,7 +9,7 @@ export class GithubProvider implements ProvidersInterface { )}`; } - async getToken(code: string): Promise { + async getToken(code: string, state?: string): Promise { const { access_token } = await ( await fetch('https://github.com/login/oauth/access_token', { method: 'POST', diff --git a/apps/backend/src/services/auth/providers/google.provider.ts b/apps/backend/src/services/auth/providers/google.provider.ts index 993eda7ed..88949bd3d 100644 --- a/apps/backend/src/services/auth/providers/google.provider.ts +++ b/apps/backend/src/services/auth/providers/google.provider.ts @@ -47,7 +47,7 @@ export class GoogleProvider implements ProvidersInterface { }); } - async getToken(code: string) { + async getToken(code: string, state?: string) { const { client, oauth2 } = clientAndYoutube(); const { tokens } = await client.getToken(code); return tokens.access_token; diff --git a/apps/backend/src/services/auth/providers/oauth.provider.ts b/apps/backend/src/services/auth/providers/oauth.provider.ts index 301752382..978fd9724 100644 --- a/apps/backend/src/services/auth/providers/oauth.provider.ts +++ b/apps/backend/src/services/auth/providers/oauth.provider.ts @@ -1,4 +1,6 @@ import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface'; +import { randomBytes } from 'crypto'; +import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; export class OauthProvider implements ProvidersInterface { private readonly authUrl: string; @@ -48,18 +50,42 @@ export class OauthProvider implements ProvidersInterface { this.userInfoUrl = POSTIZ_OAUTH_USERINFO_URL; } - generateLink(): string { + async generateLink(): Promise { + const state = randomBytes(32).toString('hex'); + + await ioRedis.set(`oauth_state:${state}`, '1', 'EX', 600); + const params = new URLSearchParams({ client_id: this.clientId, scope: 'openid profile email', response_type: 'code', redirect_uri: `${this.frontendUrl}/settings`, + state, }); return `${this.authUrl}/?${params.toString()}`; } - async getToken(code: string): Promise { + private async validateState(state: string): Promise { + if (!state) { + return false; + } + + const key = `oauth_state:${state}`; + const exists = await ioRedis.get(key); + + if (!exists) { + return false; + } + + await ioRedis.del(key); + return true; + } + + async getToken(code: string, state?: string): Promise { + if (state && !(await this.validateState(state))) { + throw new Error('Invalid or expired state parameter'); + } const response = await fetch(`${this.tokenUrl}`, { method: 'POST', headers: { diff --git a/apps/backend/src/services/auth/providers/wallet.provider.ts b/apps/backend/src/services/auth/providers/wallet.provider.ts index 3f606e703..9fdc4ab16 100644 --- a/apps/backend/src/services/auth/providers/wallet.provider.ts +++ b/apps/backend/src/services/auth/providers/wallet.provider.ts @@ -40,7 +40,7 @@ export class WalletProvider implements ProvidersInterface { return challenge; } - async getToken(code: string) { + async getToken(code: string, state?: string) { const { publicKey, challenge, signature } = JSON.parse( Buffer.from(code, 'base64').toString() ); diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index 13d925d28..d1fc844aa 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -41,18 +41,20 @@ export function Register() { const fetch = useFetch(); const [provider] = useState(getQuery?.get('provider')?.toUpperCase()); const [code, setCode] = useState(getQuery?.get('code') || ''); + const [state] = useState(getQuery?.get('state') || ''); const [show, setShow] = useState(false); useEffect(() => { if (provider && code) { load(); } - }, []); + }, [provider, code, state]); const load = useCallback(async () => { const { token } = await ( await fetch(`/auth/oauth/${provider?.toUpperCase() || 'LOCAL'}/exists`, { method: 'POST', body: JSON.stringify({ code, + state, }), }) ).json(); @@ -60,7 +62,7 @@ export function Register() { setCode(token); setShow(true); } - }, [provider, code]); + }, [provider, code, state]); if (!code && !provider) { return ; }