From 8f4f1b618a45b26638e7ac1647e4e32569494de3 Mon Sep 17 00:00:00 2001 From: EvgenyWas Date: Sun, 5 May 2024 15:42:34 +0100 Subject: [PATCH] feat: implement google oauth athorization --- server/api/auth/login.post.ts | 8 ++++ server/api/auth/token/refresh.post.ts | 35 ++++++++++++---- server/middleware/auth.ts | 10 +++-- server/routes/google/oauth/callback.ts | 57 ++++++++++++++++++++++++++ server/routes/logout.ts | 16 ++++++-- server/types.ts | 11 +++++ 6 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 server/routes/google/oauth/callback.ts diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index 8659cb0..efc31d0 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -6,6 +6,10 @@ import type { Profile as IProfile } from '~/types/user'; import { stringToBase64 } from '~/utils/converters'; import { emailValidator, passwordValidator } from '~/utils/validators'; +const anotherAuthProviderMessage = + // eslint-disable-next-line max-len + 'You tried signing in with a different authentication method than the one you used during signup. Please try again using your original authentication method.'; + const loginPayloadSchema = z.object({ email: emailValidator, password: passwordValidator, @@ -28,6 +32,10 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, statusMessage: 'User with the provided email does not exist' }); } + if (user.auth_provider !== AUTH_PROVIDERS.Email_And_Password) { + throw createError({ statusCode: 404, statusMessage: anotherAuthProviderMessage }); + } + if (user.password !== payload.password) { throw createError({ statusCode: 403, statusMessage: 'Password is incorrect' }); } diff --git a/server/api/auth/token/refresh.post.ts b/server/api/auth/token/refresh.post.ts index a3974c1..91513bb 100644 --- a/server/api/auth/token/refresh.post.ts +++ b/server/api/auth/token/refresh.post.ts @@ -1,7 +1,6 @@ import { AUTH_PROVIDERS, COOKIE_NAMES } from '~/configs/properties'; import { userIdentitySchema } from '~/server/schemas'; -import { jwtGenerator } from '~/server/services'; -import octokitOAuthApp from '~/server/services/octokitOAuthApp'; +import { googleOAuthClient, jwtGenerator, octokitOAuthApp } from '~/server/services'; import type { UserIdentity } from '~/server/types'; import { base64ToString } from '~/utils/converters'; @@ -11,10 +10,9 @@ export default defineEventHandler(async (event) => { return sendError(event, createError({ statusCode: 400, statusMessage: 'Token is not provided' })); } - const identityCookie = getCookie(event, COOKIE_NAMES.userIdentity) ?? ''; let identity: UserIdentity; try { - identity = userIdentitySchema.parse(JSON.parse(base64ToString(identityCookie))); + identity = userIdentitySchema.parse(JSON.parse(base64ToString(getCookie(event, COOKIE_NAMES.userIdentity) ?? ''))); } catch (error) { return sendError( event, @@ -38,14 +36,33 @@ export default defineEventHandler(async (event) => { createError({ statusCode: 500, statusMessage: 'Internal server error during refreshing token' }), ); } - } else { + } + + if (identity.provider === AUTH_PROVIDERS.Google) { try { - const { accessToken, refreshToken, refreshExpiresIn: maxAge } = jwtGenerator.refresh(token); - setCookie(event, COOKIE_NAMES.refreshToken, refreshToken, { httpOnly: true, sameSite: true, maxAge }); + googleOAuthClient.setCredentials({ refresh_token: token }); + const { credentials } = await googleOAuthClient.refreshAccessToken(); + setCookie(event, COOKIE_NAMES.refreshToken, credentials.refresh_token ?? '', { + httpOnly: true, + sameSite: true, + maxAge: credentials.expiry_date as number, + }); - return { accessToken, type: 'bearer' }; + return { accessToken: credentials.access_token, type: credentials.token_type }; } catch (error) { - return sendError(event, createError({ statusCode: 401, data: error })); + return sendError( + event, + createError({ statusCode: 500, statusMessage: 'Internal server error during refreshing token' }), + ); } } + + try { + const { accessToken, refreshToken, refreshExpiresIn: maxAge } = jwtGenerator.refresh(token); + setCookie(event, COOKIE_NAMES.refreshToken, refreshToken, { httpOnly: true, sameSite: true, maxAge }); + + return { accessToken, type: 'bearer' }; + } catch (error) { + return sendError(event, createError({ statusCode: 401, data: error })); + } }); diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index a99f8b9..9bf5346 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -1,7 +1,6 @@ import { AUTH_PROVIDERS, COOKIE_NAMES } from '~/configs/properties'; import { userIdentitySchema } from '~/server/schemas'; -import { jwtGenerator } from '~/server/services'; -import octokitOAuthApp from '~/server/services/octokitOAuthApp'; +import { googleOAuthClient, jwtGenerator, octokitOAuthApp } from '~/server/services'; import type { UserIdentity } from '~/server/types'; import { base64ToString } from '~/utils/converters'; @@ -9,10 +8,11 @@ const AUTHORIZED_PATHES = ['/api/user']; export default defineEventHandler(async (event) => { if (AUTHORIZED_PATHES.some((path) => event.path.startsWith(path))) { - const identityCookie = getCookie(event, COOKIE_NAMES.userIdentity) ?? ''; let identity: UserIdentity; try { - identity = userIdentitySchema.parse(JSON.parse(base64ToString(identityCookie))); + identity = userIdentitySchema.parse( + JSON.parse(base64ToString(getCookie(event, COOKIE_NAMES.userIdentity) ?? '')), + ); } catch (error) { return sendError( event, @@ -26,6 +26,8 @@ export default defineEventHandler(async (event) => { const token = accessToken.replace('Bearer ', ''); if (identity.provider === AUTH_PROVIDERS.Github) { await octokitOAuthApp.checkToken({ token }); + } else if (identity.provider === AUTH_PROVIDERS.Google) { + await googleOAuthClient.getTokenInfo(token); } else { jwtGenerator.verifyAccessToken(token); } diff --git a/server/routes/google/oauth/callback.ts b/server/routes/google/oauth/callback.ts new file mode 100644 index 0000000..4ddaaed --- /dev/null +++ b/server/routes/google/oauth/callback.ts @@ -0,0 +1,57 @@ +import { AUTH_PROVIDERS, COOKIE_NAMES, USER_IDENTITY_MAX_AGE } from '~/configs/properties'; +import Profile from '~/server/models/user/profile.model'; +import { googleOAuthClient } from '~/server/services'; +import type { GoogleUserInfoResponse } from '~/server/types'; +import { stringToBase64 } from '~/utils/converters'; + +const anotherAuthProviderMessage = + // eslint-disable-next-line max-len + 'You tried signing in with a different authentication method than the one you used during signup. Please try again using your original authentication method.'; + +export default defineEventHandler(async (event) => { + const queries = getQuery(event); + try { + if (queries.error) { + return sendError(event, createError({ statusCode: 400, statusMessage: queries.error as string })); + } + + const { tokens } = await googleOAuthClient.getToken(queries.code as string); + const user = await $fetch('https://www.googleapis.com/userinfo/v2/me', { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + const existedProfile = await Profile.findOne({ email: user.email }); + + let profile = existedProfile; + if (existedProfile) { + if (existedProfile.auth_provider !== AUTH_PROVIDERS.Google) { + return sendError(event, createError({ statusCode: 403, statusMessage: anotherAuthProviderMessage })); + } + } else { + profile = await Profile.create({ + name: user.name, + email: user.email, + avatar: user.picture, + auth_provider: AUTH_PROVIDERS.Google, + }); + } + + googleOAuthClient.setCredentials(tokens); + + const indentity = stringToBase64(JSON.stringify({ id: profile?._id.toString(), provider: AUTH_PROVIDERS.Google })); + + setCookie(event, COOKIE_NAMES.refreshToken, tokens.refresh_token ?? '', { + httpOnly: true, + sameSite: true, + maxAge: tokens.expiry_date as number, + }); + setCookie(event, COOKIE_NAMES.userIdentity, indentity, { + httpOnly: true, + sameSite: true, + maxAge: USER_IDENTITY_MAX_AGE, + }); + + return await sendRedirect(event, '/', 302); + } catch (error) { + return sendError(event, createError({ statusCode: 500, statusMessage: JSON.stringify(error) })); + } +}); diff --git a/server/routes/logout.ts b/server/routes/logout.ts index 89bf7ab..0747014 100644 --- a/server/routes/logout.ts +++ b/server/routes/logout.ts @@ -2,7 +2,7 @@ import { H3Event } from 'h3'; import { userIdentitySchema } from '../schemas'; import { AUTH_PROVIDERS, COOKIE_NAMES } from '~/configs/properties'; -import octokitOAuthApp from '~/server/services/octokitOAuthApp'; +import { googleOAuthClient, octokitOAuthApp } from '~/server/services'; import type { UserIdentity } from '~/server/types'; import { base64ToString } from '~/utils/converters'; @@ -27,12 +27,20 @@ export default defineEventHandler(async (event) => { return await sendRedirect(event, location, 302); } + const token = getCookie(event, COOKIE_NAMES.refreshToken) ?? ''; if (identity.provider === AUTH_PROVIDERS.Github) { - console.log('ACCESS TOKEN:', getCookie(event, COOKIE_NAMES.refreshToken)); try { - await octokitOAuthApp.deleteToken({ token: getCookie(event, COOKIE_NAMES.refreshToken) ?? '' }); + await octokitOAuthApp.deleteToken({ token }); } catch (error) { - console.log('ERROR HELLO!', error); + console.log(error); + } + } + + if (identity.provider === AUTH_PROVIDERS.Google) { + try { + await googleOAuthClient.revokeToken(token); + } catch (error) { + console.log(error); } } diff --git a/server/types.ts b/server/types.ts index 864b757..6359158 100644 --- a/server/types.ts +++ b/server/types.ts @@ -4,3 +4,14 @@ export interface UserIdentity { id: string; provider: AUTH_PROVIDERS; } + +export interface GoogleUserInfoResponse { + id: string; + email: string; + verified_email: boolean; + name: string; + given_name: string; + family_name: string; + picture: string; + locale: string; +}