Skip to content

Commit

Permalink
Lucia beta 12, split auth shared.ts to index.ts and oauth.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
rmarscher committed Dec 12, 2023
1 parent dfacb16 commit db8fb9a
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 83 deletions.
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"drizzle-orm": "^0.29.0",
"drizzle-valibot": "beta",
"hono": "^3.9.2",
"lucia": "3.0.0-beta.11",
"lucia": "3.0.0-beta.12",
"miniflare": "3.20231025.1",
"oslo": "0.24.0",
"superjson": "1.13.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/auth/hono.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAllowedOriginHost } from './shared'
import { getAllowedOriginHost } from '.'
import type { Context as HonoContext, Next } from 'hono'
import { Bindings } from '../worker'
import { verifyRequestOrigin } from 'oslo/request'
Expand Down
68 changes: 68 additions & 0 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Adapter, DatabaseSessionAttributes, DatabaseUserAttributes, Lucia, TimeSpan } from 'lucia'
import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle'
import { SessionTable, UserTable } from '../db/schema'
import { DB } from '../db/client'

/**
* Lucia's isValidRequestOrigin method will compare the
* origin of the request to the configured host.
* We want to allow cross-domain requests from our APP_URL so return that
* if the request origin host matches the APP_URL host.
* @link https://github.com/lucia-auth/lucia/blob/main/packages/lucia/src/utils/url.ts
*/
export const getAllowedOriginHost = (app_url: string, request?: Request) => {
if (!app_url || !request) return undefined
const requestOrigin = request.headers.get('Origin')
const requestHost = requestOrigin ? new URL(requestOrigin).host : undefined
const appHost = new URL(app_url).host
return requestHost === appHost ? appHost : undefined
}

export const createAuth = (db: DB, appUrl: string) => {
// @ts-ignore Expect type errors because this is D1 and not SQLite... but it works
const adapter = new DrizzleSQLiteAdapter(db, SessionTable, UserTable)
// cast probably only needed until adapter-drizzle is updated
return new Lucia(adapter as Adapter, {
...getAuthOptions(appUrl),
})
}

export const getAuthOptions = (appUrl: string) => {
const env = !appUrl || appUrl.startsWith('http:') ? 'DEV' : 'PROD'
return {
getUserAttributes: (data: DatabaseUserAttributes) => {
return {
email: data.email || '',
}
},
// Optional additional session attributes to expose
// If updated, also update createSession() in packages/api/src/auth/user.ts
getSessionAttributes: (databaseSession: DatabaseSessionAttributes) => {
return {}
},
sessionExpiresIn: new TimeSpan(365, 'd'),
sessionCookie: {
name: 'auth_session',
expires: false,
attributes: {
secure: env === 'PROD',
sameSite: 'lax' as const,
},
},

// If you want more debugging, uncomment this
// experimental: {
// debugMode: true,
// },
}
}

declare module 'lucia' {
interface Register {
Lucia: ReturnType<typeof createAuth>
}
interface DatabaseSessionAttributes {}
interface DatabaseUserAttributes {
email: string | null
}
}
80 changes: 6 additions & 74 deletions packages/api/src/auth/shared.ts → packages/api/src/auth/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiContextProps } from '../context'
import { SessionTable, User, UserTable } from '../db/schema'
import { User } from '../db/schema'
import {
Apple,
AppleRefreshedTokens,
Expand All @@ -12,7 +12,6 @@ import {
generateCodeVerifier,
generateState,
} from 'arctic'
import { DatabaseSessionAttributes, DatabaseUserAttributes, Lucia, TimeSpan } from 'lucia'
import {
AuthProvider,
AuthProviderName,
Expand All @@ -23,13 +22,10 @@ import {
import { isWithinExpirationDate } from 'oslo'
import { parseJWT } from 'oslo/jwt'
import { createAuthMethodId, createUser, getAuthMethod, getUserById } from './user'
import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle'

import { P, match } from 'ts-pattern'
import { getCookie } from 'hono/cookie'
import { TRPCError } from '@trpc/server'
import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'
import { DB } from '../db/client'

export interface AppleIdTokenClaims {
iss: 'https://appleid.apple.com'
Expand Down Expand Up @@ -87,59 +83,6 @@ export const getAuthProvider = (ctx: ApiContextProps, name: AuthProviderName): A
return service
}

/**
* Lucia's isValidRequestOrigin method will compare the
* origin of the request to the configured host.
* We want to allow cross-domain requests from our APP_URL so return that
* if the request origin host matches the APP_URL host.
* @link https://github.com/lucia-auth/lucia/blob/main/packages/lucia/src/utils/url.ts
*/
export const getAllowedOriginHost = (app_url: string, request?: Request) => {
if (!app_url || !request) return undefined
const requestOrigin = request.headers.get('Origin')
const requestHost = requestOrigin ? new URL(requestOrigin).host : undefined
const appHost = new URL(app_url).host
return requestHost === appHost ? appHost : undefined
}

export const createAuth = (db: DB, appUrl: string) => {
// @ts-ignore Expect type errors because this is D1 and not SQLite... but it works
const adapter = new DrizzleSQLiteAdapter(db, SessionTable, UserTable)
return new Lucia(adapter, {
...getAuthOptions(appUrl),
})
}

export const getAuthOptions = (appUrl: string) => {
const env = !appUrl || appUrl.startsWith('http:') ? 'DEV' : 'PROD'
return {
getUserAttributes: (data: DatabaseUserAttributes) => {
return {
email: data.email || '',
}
},
// Optional additional session attributes to expose
// If updated, also update createSession() in packages/api/src/auth/user.ts
getSessionAttributes: (databaseSession: DatabaseSessionAttributes) => {
return {}
},
sessionExpiresIn: new TimeSpan(365, 'd'),
sessionCookie: {
name: 'auth_session',
expires: false,
attributes: {
secure: env === 'PROD',
sameSite: 'lax' as const,
},
},

// If you want more debugging, uncomment this
// experimental: {
// debugMode: true,
// },
}
}

export function getAppleClaims(idToken?: string): AppleIdTokenClaims | undefined {
if (!idToken) return undefined
const payload = parseJWT(idToken)?.payload
Expand Down Expand Up @@ -264,7 +207,7 @@ export async function getDiscordUser({
Authorization: `Bearer ${accessToken}`,
},
})
).json()
).json<{ email: string; id: string }>()
return {
attributes: {
email: res.email,
Expand All @@ -283,14 +226,14 @@ export async function getGitHubUser({
Authorization: `Bearer ${accessToken}`,
},
})
).json()
).json<{ id: string; email: string }>()
const emails = await (
await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
).json()
).json<{ primary: boolean; email: string }[]>()
const primaryEmail = emails.find((e: { primary: boolean }) => e.primary)?.email
return {
attributes: {
Expand All @@ -310,7 +253,7 @@ export async function getGoogleUser({
Authorization: `Bearer ${accessToken}`,
},
})
).json()
).json<{ email: string; sub: string }>()
return {
attributes: {
email: res.email || undefined,
Expand Down Expand Up @@ -377,7 +320,7 @@ export const getUserFromAuthProvider = async <_AuthTokens extends AuthTokens>(
export const getOAuthUser = async (
service: AuthProviderName,
ctx: ApiContextProps,
{ code, userData }: { code: string; userData: Partial<User> }
{ code, userData }: { code: string; userData?: Partial<User> }
): Promise<User> => {
const authService = getAuthProvider(ctx, service)
if (isOAuth2ProviderWithPKCE(authService)) {
Expand All @@ -391,14 +334,3 @@ export const getOAuthUser = async (
const validateResult = await authService.validateAuthorizationCode(code)
return getUserFromAuthProvider(ctx, service, authService, validateResult)
}

declare module 'lucia' {
interface Register {
Lucia: ReturnType<typeof createAuth>
DatabaseUserAttributes: {
email: string | null
}
// biome-ignore lint/complexity/noBannedTypes: Need to define this even if empty
DatabaseSessionAttributes: {}
}
}
2 changes: 1 addition & 1 deletion packages/api/src/auth/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { isWithinExpirationDate } from 'oslo'
import { createCode, createTotpSecret, verifyCode } from '../utils/crypto'
import { AuthProviderName } from './providers'
import { OAuth2RequestError } from 'arctic'
import { getOAuthUser } from './shared'
import { getOAuthUser } from './oauth'

export const createAuthMethodId = (providerId: string, providerUserId: string) => {
if (providerId.includes(':')) {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { inferAsyncReturnType } from '@trpc/server'
import type { Context as HonoContext, HonoRequest } from 'hono'
import type { Lucia } from 'lucia'
import { verifyToken } from './utils/crypto'
import { createAuth } from './auth/shared'
import { createAuth } from './auth'
import { getCookie } from 'hono/cookie'

export interface ApiContextProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
getAuthProvider,
getAuthorizationUrl,
getUserFromAuthProvider,
} from '../auth/shared'
} from '../auth/oauth'
import { isJWTExpired, sha256 } from '../utils/crypto'
import { getCookie } from 'hono/cookie'
import { parseJWT } from 'oslo/jwt'
Expand Down
13 changes: 9 additions & 4 deletions packages/app/features/oauth/screen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { GetServerSideProps } from 'next'
import type { AuthProviderName } from '@t4/api/src/auth/providers'
import { Paragraph, isServer } from '@t4/ui'
import { useSignIn } from 'app/utils/auth'
import { type SignInWithOAuth, useSignIn } from 'app/utils/auth'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createParam } from 'solito'
import { P, match } from 'ts-pattern'
Expand Down Expand Up @@ -35,9 +35,10 @@ export const getServerSideProps = (async (context) => {
}) satisfies GetServerSideProps<OAuthSignInScreenProps>

export interface OAuthSignInScreenProps {
appleUser: { email?: string | null } | null
appleUser?: { email?: string | null } | null
}


export const OAuthSignInScreen = ({ appleUser }: OAuthSignInScreenProps): React.ReactNode => {
const sent = useRef(false)
const { signIn } = useSignIn()
Expand All @@ -48,7 +49,7 @@ export const OAuthSignInScreen = ({ appleUser }: OAuthSignInScreenProps): React.
const [error, setError] = useState<string | undefined>(undefined)

const sendApiRequestOnLoad = useCallback(
async (params: Params & OAuthSignInScreenProps) => {
async (params: SignInWithOAuth) => {
if (sent.current) return
sent.current = true
try {
Expand Down Expand Up @@ -80,7 +81,11 @@ export const OAuthSignInScreen = ({ appleUser }: OAuthSignInScreenProps): React.
redirectTo: redirectTo || '',
state,
code,
appleUser: appleUser || undefined,
// undefined vs null is a result of passing via JSON with getServerSideProps
// Maybe there's a superjson plugin or another way to handle it.
appleUser: appleUser ? {
email: appleUser.email || undefined,
} : undefined,
})
}, [provider, redirectTo, state, code, sendApiRequestOnLoad, appleUser])

Expand Down

0 comments on commit db8fb9a

Please sign in to comment.