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 @@