diff --git a/.env.example b/.env.example index 527dae2..a539bcd 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,8 @@ TWO_FACTOR_TIMEOUT=300000 # OAuth GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" # Public diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2af8905 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License Copyright (c) 2024 Bradley Shellnut + +Permission is hereby granted, +free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice +(including the next paragraph) shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/package.json b/package.json index 9423b10..8caf66b 100644 --- a/package.json +++ b/package.json @@ -129,5 +129,6 @@ "tailwindcss-animate": "^1.0.7", "tsyringe": "^4.8.0", "zod-to-json-schema": "^3.23.3" - } -} + }, + "license": "MIT" +} \ No newline at end of file diff --git a/src/lib/server/api/common/env.ts b/src/lib/server/api/common/env.ts index 95b35c4..5dd1157 100644 --- a/src/lib/server/api/common/env.ts +++ b/src/lib/server/api/common/env.ts @@ -19,6 +19,8 @@ const EnvSchema = z.object({ DB_SEEDING: stringBoolean, GITHUB_CLIENT_ID: z.string(), GITHUB_CLIENT_SECRET: z.string(), + GOOGLE_CLIENT_ID: z.string(), + GOOGLE_CLIENT_SECRET: z.string(), NODE_ENV: z.string().default('development'), ORIGIN: z.string(), PUBLIC_SITE_NAME: z.string(), diff --git a/src/lib/server/api/controllers/oauth.controller.ts b/src/lib/server/api/controllers/oauth.controller.ts index 570f42b..4a7b44e 100644 --- a/src/lib/server/api/controllers/oauth.controller.ts +++ b/src/lib/server/api/controllers/oauth.controller.ts @@ -2,7 +2,7 @@ import 'reflect-metadata' import { Controller } from '$lib/server/api/common/types/controller' import { LuciaService } from '$lib/server/api/services/lucia.service' import { OAuthService } from '$lib/server/api/services/oauth.service' -import { github } from '$lib/server/auth' +import { github, google } from '$lib/server/auth' import { OAuth2RequestError } from 'arctic' import { getCookie, setCookie } from 'hono/cookie' import { TimeSpan } from 'oslo' @@ -18,55 +18,108 @@ export class OAuthController extends Controller { } routes() { - return this.controller.get('/github', async (c) => { - try { - const code = c.req.query('code')?.toString() ?? null - const state = c.req.query('state')?.toString() ?? null - const storedState = getCookie(c).github_oauth_state ?? null + return this.controller + .get('/github', async (c) => { + try { + const code = c.req.query('code')?.toString() ?? null + const state = c.req.query('state')?.toString() ?? null + const storedState = getCookie(c).github_oauth_state ?? null - console.log('code', code, 'state', state, 'storedState', storedState) + console.log('code', code, 'state', state, 'storedState', storedState) - if (!code || !state || !storedState || state !== storedState) { - return c.body(null, 400) + if (!code || !state || !storedState || state !== storedState) { + return c.body(null, 400) + } + + const tokens = await github.validateAuthorizationCode(code) + const githubUserResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + const githubUser: GitHubUser = await githubUserResponse.json() + + const userId = await this.oauthService.handleOAuthUser(githubUser.id, githubUser.login, 'github') + + const session = await this.luciaService.lucia.createSession(userId, {}) + const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) + + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: + sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() + ? sessionCookie.attributes.maxAge + : new TimeSpan(2, 'w').seconds(), + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as any, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires, + }) + + return c.json({ message: 'ok' }) + } catch (error) { + console.error(error) + // the specific error message depends on the provider + if (error instanceof OAuth2RequestError) { + // invalid code + return c.body(null, 400) + } + return c.body(null, 500) } + }) + .get('/google', async (c) => { + try { + const code = c.req.query('code')?.toString() ?? null + const state = c.req.query('state')?.toString() ?? null + const storedState = getCookie(c).google_oauth_state ?? null + const storedCodeVerifier = getCookie(c).google_oauth_code_verifier ?? null - const tokens = await github.validateAuthorizationCode(code) - const githubUserResponse = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${tokens.accessToken}`, - }, - }) - const githubUser: GitHubUser = await githubUserResponse.json() + console.log('code', code, 'state', state, 'storedState', storedState) - const userId = await this.oauthService.handleOAuthUser(githubUser.id, githubUser.login, 'github') + if (!code || !storedState || !storedCodeVerifier || state !== storedState) { + return c.body(null, 400) + } - const session = await this.luciaService.lucia.createSession(userId, {}) - const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) + const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier) + console.log('tokens', tokens) + const googleUserResponse = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + console.log('googleUserResponse', googleUserResponse) + const googleUser: GoogleUser = await googleUserResponse.json() - setCookie(c, sessionCookie.name, sessionCookie.value, { - path: sessionCookie.attributes.path, - maxAge: - sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() - ? sessionCookie.attributes.maxAge - : new TimeSpan(2, 'w').seconds(), - domain: sessionCookie.attributes.domain, - sameSite: sessionCookie.attributes.sameSite as any, - secure: sessionCookie.attributes.secure, - httpOnly: sessionCookie.attributes.httpOnly, - expires: sessionCookie.attributes.expires, - }) + const userId = await this.oauthService.handleOAuthUser(googleUser.id, googleUser.login, 'github') - return c.json({ message: 'ok' }) - } catch (error) { - console.error(error) - // the specific error message depends on the provider - if (error instanceof OAuth2RequestError) { - // invalid code - return c.body(null, 400) + const session = await this.luciaService.lucia.createSession(userId, {}) + const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) + + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: + sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() + ? sessionCookie.attributes.maxAge + : new TimeSpan(2, 'w').seconds(), + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as any, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires, + }) + + return c.json({ message: 'ok' }) + } catch (error) { + console.error(error) + // the specific error message depends on the provider + if (error instanceof OAuth2RequestError) { + // invalid code + return c.body(null, 400) + } + return c.body(null, 500) } - return c.body(null, 500) - } - }) + }) } } @@ -74,3 +127,8 @@ interface GitHubUser { id: number login: string } + +interface GoogleUser { + id: number + login: string +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index bf0d9aa..e06aa3e 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,4 +1,6 @@ import env from "$lib/server/api/common/env"; -import { GitHub } from "arctic"; +import { GitHub, Google } from "arctic"; export const github = new GitHub(env.GITHUB_CLIENT_ID, env.GITHUB_CLIENT_SECRET); + +export const google = new Google(env.GOOGLE_CLIENT_ID, env.GOOGLE_CLIENT_SECRET, `${env.ORIGIN}/auth/callback/google`); \ No newline at end of file diff --git a/src/routes/(app)/privacy/+page.svelte b/src/routes/(app)/privacy/+page.svelte index 97bcf66..4711f47 100644 --- a/src/routes/(app)/privacy/+page.svelte +++ b/src/routes/(app)/privacy/+page.svelte @@ -1,5 +1,5 @@

Privacy Policy

-

Last Updated: September 13th, 2023

+

Last Updated: September 19th, 2024

At Bored Game, we respect your privacy and are committed to protecting your personal information. We collect only the personal information that is necessary for us to provide our services to you. diff --git a/src/routes/(auth)/auth/callback/github/+server.ts b/src/routes/(auth)/auth/callback/github/+server.ts index f4c0f73..5453600 100644 --- a/src/routes/(auth)/auth/callback/github/+server.ts +++ b/src/routes/(auth)/auth/callback/github/+server.ts @@ -24,8 +24,3 @@ export async function GET(event: RequestEvent): Promise { redirect(StatusCodes.TEMPORARY_REDIRECT, '/') } - -interface GitHubUser { - id: number - login: string -} diff --git a/src/routes/(auth)/auth/callback/google/+server.ts b/src/routes/(auth)/auth/callback/google/+server.ts new file mode 100644 index 0000000..5bc0ad4 --- /dev/null +++ b/src/routes/(auth)/auth/callback/google/+server.ts @@ -0,0 +1,26 @@ +import { StatusCodes } from '$lib/constants/status-codes' +import type { RequestEvent } from '@sveltejs/kit' +import { redirect } from 'sveltekit-flash-message/server' + +export async function GET(event: RequestEvent): Promise { + const { locals, url } = event + const code = url.searchParams.get('code') + const state = url.searchParams.get('state') + console.log('code', code, 'state', state) + + const { data, error } = await locals.api.oauth.google.$get({ query: { code, state } }).then(locals.parseApiResponse) + + if (error) { + return new Response(JSON.stringify(error), { + status: 400, + }) + } + + if (!data) { + return new Response(JSON.stringify({ message: 'Invalid request' }), { + status: 400, + }) + } + + redirect(StatusCodes.TEMPORARY_REDIRECT, '/') +} diff --git a/src/routes/(auth)/login/google/+server.ts b/src/routes/(auth)/login/google/+server.ts index d9e5c33..3c2ce04 100644 --- a/src/routes/(auth)/login/google/+server.ts +++ b/src/routes/(auth)/login/google/+server.ts @@ -1,14 +1,25 @@ -import { github } from '$lib/server/auth' +import { google } from '$lib/server/auth' import { redirect } from '@sveltejs/kit' -import { generateState } from 'arctic' +import { generateCodeVerifier, generateState } from 'arctic' import type { RequestEvent } from '@sveltejs/kit' +// Google Login export async function GET(event: RequestEvent): Promise { const state = generateState() - const url = await github.createAuthorizationURL(state) + const codeVerifier = generateCodeVerifier(); - event.cookies.set('github_oauth_state', state, { + const url = await google.createAuthorizationURL(state, codeVerifier) + + event.cookies.set('google_oauth_state', state, { + path: '/', + secure: import.meta.env.PROD, + httpOnly: true, + maxAge: 60 * 10, + sameSite: 'lax', + }) + + event.cookies.set('google_oauth_code_verifier', codeVerifier, { path: '/', secure: import.meta.env.PROD, httpOnly: true,