Skip to content

Commit 2d1bc17

Browse files
authored
Remix seamless auth (#5111)
* don't proxy authenticate * maybeGetUserFromSession, update tests * authenticate enriched proxy * redirect to projects if logged in * fix url * no cache
1 parent 4d46a8f commit 2d1bc17

17 files changed

+113
-26
lines changed

utopia-remix/app/handlers/listProjects.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('handleListProjects', () => {
3636
const fn = async () => handleListProjects(req)
3737

3838
await expect(fn).rejects.toThrow(ApiError)
39-
await expect(fn).rejects.toThrow('session not found')
39+
await expect(fn).rejects.toThrow('unauthorized')
4040
})
4141

4242
describe('with an authorized user', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { prisma } from '../db.server'
2+
import { createTestSession, createTestUser, truncateTables } from '../test-util'
3+
import { maybeGetUserFromSession } from './session.server'
4+
5+
describe('session', () => {
6+
afterEach(async () => {
7+
await truncateTables([prisma.persistentSession, prisma.userDetails])
8+
})
9+
describe('maybeGetUserFromSession', () => {
10+
beforeEach(async () => {
11+
await createTestUser(prisma, { id: 'bob' })
12+
await createTestUser(prisma, { id: 'alice' })
13+
await createTestSession(prisma, { userId: 'bob', key: 'bob-key' })
14+
})
15+
it('returns null if the session is not found', async () => {
16+
const got = await maybeGetUserFromSession({ key: '' })
17+
expect(got).toBe(null)
18+
})
19+
it('returns null if the session data is invalid', async () => {
20+
const got = await maybeGetUserFromSession({ key: '{{WRONG}}' })
21+
expect(got).toBe(null)
22+
})
23+
it('returns null if the user is not found', async () => {
24+
const got = await maybeGetUserFromSession({ key: 'unknown' })
25+
expect(got).toBe(null)
26+
})
27+
it('returns the user details if found', async () => {
28+
const got = await maybeGetUserFromSession({ key: 'bob-key' })
29+
expect(got).not.toBe(null)
30+
expect(got?.user_id).toBe('bob')
31+
})
32+
})
33+
})

utopia-remix/app/models/session.server.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { PersistentSession, UserDetails } from 'prisma-client'
22
import { prisma } from '../db.server'
3-
import { ensure } from '../util/api.server'
4-
import { Status } from '../util/statusCodes'
53

64
export async function getSession(params: { key: string }): Promise<PersistentSession | null> {
75
return await prisma.persistentSession.findFirst({
@@ -17,10 +15,13 @@ function isSessionJSONData(v: unknown): v is SessionJSONData {
1715
return typeof v === 'object' && (v as SessionJSONData).userID != null
1816
}
1917

20-
export async function getUserFromSession(params: { key: string }): Promise<UserDetails | null> {
18+
export async function maybeGetUserFromSession(params: {
19+
key: string
20+
}): Promise<UserDetails | null> {
2121
const session = await getSession({ key: params.key })
22-
ensure(session != null, 'session not found', Status.UNAUTHORIZED)
23-
ensure(isSessionJSONData(session.session_json), 'invalid session', Status.UNAUTHORIZED)
22+
if (session == null || !isSessionJSONData(session.session_json)) {
23+
return null
24+
}
2425

2526
return prisma.userDetails.findFirst({
2627
where: { user_id: session.session_json.userID },

utopia-remix/app/routes-test/internal.projects.$id.access.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ describe('handleChangeAccess', () => {
111111
})
112112
const error = await getActionResult('two', AccessLevel.PRIVATE, 'no-cookie')
113113
expect(error).toEqual({
114-
message: 'session not found',
115-
status: Status.UNAUTHORIZED,
114+
message: 'Project not found',
115+
status: Status.NOT_FOUND,
116116
error: 'Error',
117117
})
118118
})

utopia-remix/app/routes-test/internal.projects.$id.delete.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('handleDeleteProject', () => {
3131
const fn = async () =>
3232
handleDeleteProject(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
3333
await expect(fn).rejects.toThrow(ApiError)
34-
await expect(fn).rejects.toThrow('session not found')
34+
await expect(fn).rejects.toThrow('unauthorized')
3535
})
3636
it('requires a valid id', async () => {
3737
const fn = async () =>

utopia-remix/app/routes-test/internal.projects.$id.destroy.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('handleDestroyProject', () => {
4646
const fn = async () =>
4747
handleDestroyProject(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
4848
await expect(fn).rejects.toThrow(ApiError)
49-
await expect(fn).rejects.toThrow('session not found')
49+
await expect(fn).rejects.toThrow('unauthorized')
5050
})
5151
it('requires a valid id', async () => {
5252
const fn = async () =>

utopia-remix/app/routes-test/internal.projects.$id.github.repository.update.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('handleUpdateGithubRepository', () => {
3131
const fn = async () =>
3232
handleUpdateGithubRepository(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
3333
await expect(fn).rejects.toThrow(ApiError)
34-
await expect(fn).rejects.toThrow('session not found')
34+
await expect(fn).rejects.toThrow('unauthorized')
3535
})
3636
it('requires a valid id', async () => {
3737
const fn = async () =>

utopia-remix/app/routes-test/internal.projects.$id.rename.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('handleRenameProject', () => {
3232
const fn = async () =>
3333
handleRenameProject(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
3434
await expect(fn).rejects.toThrow(ApiError)
35-
await expect(fn).rejects.toThrow('session not found')
35+
await expect(fn).rejects.toThrow('unauthorized')
3636
})
3737
it('requires a valid id', async () => {
3838
const fn = async () =>

utopia-remix/app/routes-test/internal.projects.$id.restore.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('handleRestoreDeletedProject', () => {
3636
const fn = async () =>
3737
handleRestoreDeletedProject(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
3838
await expect(fn).rejects.toThrow(ApiError)
39-
await expect(fn).rejects.toThrow('session not found')
39+
await expect(fn).rejects.toThrow('unauthorized')
4040
})
4141
it('requires a valid id', async () => {
4242
const fn = async () =>

utopia-remix/app/routes-test/internal.projects.deleted.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('handleDeleteProject', () => {
4949
const fn = async () =>
5050
handleListDeletedProjects(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
5151
await expect(fn).rejects.toThrow(ApiError)
52-
await expect(fn).rejects.toThrow('session not found')
52+
await expect(fn).rejects.toThrow('unauthorized')
5353
})
5454
it('returns deleted projects', async () => {
5555
const fn = async () => {

utopia-remix/app/routes-test/internal.projects.destroy.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('handleDestroyAllProjects', () => {
3737
const fn = async () =>
3838
handleDestroyAllProjects(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
3939
await expect(fn).rejects.toThrow(ApiError)
40-
await expect(fn).rejects.toThrow('session not found')
40+
await expect(fn).rejects.toThrow('unauthorized')
4141
})
4242
it('hard-deletes all soft-deleted projects owned by the user', async () => {
4343
const fn = async () => {

utopia-remix/app/routes/_index.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { type LinksFunction, type MetaFunction } from '@remix-run/node'
1+
import type { LoaderFunctionArgs } from '@remix-run/node'
2+
import { redirect, type LinksFunction, type MetaFunction } from '@remix-run/node'
23
import {
34
ContactUs,
45
CookieConsentBar,
@@ -13,6 +14,7 @@ import stylesheet from '~/styles/next-tailwind.css'
1314
import urlJoin from 'url-join'
1415
import type { rootLoader } from '../root'
1516
import React from 'react'
17+
import { getUser } from '../util/api.server'
1618

1719
export const links: LinksFunction = () => [
1820
// css
@@ -56,7 +58,7 @@ export const meta: MetaFunction<typeof rootLoader> = ({ data }) => {
5658
{ name: 'msapplication-TileColor', content: '#da532c' },
5759
{ name: 'theme-color', content: '#ffffff' },
5860
{ property: 'og:title', content: 'Utopia: Design and Code on one platform' },
59-
{ property: 'og:image', content: urlJoin(data?.env.UTOPIA_CDN_URL ?? '', '/og-card.png') },
61+
{ property: 'og:image', content: urlJoin(data?.env?.UTOPIA_CDN_URL ?? '', '/og-card.png') },
6062
{ property: 'og:type', content: 'website' },
6163
{
6264
property: 'og:description',
@@ -66,6 +68,14 @@ export const meta: MetaFunction<typeof rootLoader> = ({ data }) => {
6668
]
6769
}
6870

71+
export async function loader(args: LoaderFunctionArgs) {
72+
const user = await getUser(args.request)
73+
if (user != null) {
74+
return redirect(`/projects`)
75+
}
76+
return {}
77+
}
78+
6979
const IndexPage = React.memo(() => {
7080
return (
7181
<div className='bg-white'>
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
2+
import { ALLOW } from '../handlers/validators'
3+
import { handle } from '../util/api.server'
4+
import { proxy } from '../util/proxy.server'
5+
6+
export async function loader(args: LoaderFunctionArgs) {
7+
return handle(args, {
8+
GET: {
9+
validator: ALLOW,
10+
handler: handleAuthenticate,
11+
},
12+
})
13+
}
14+
15+
async function handleAuthenticate(req: Request) {
16+
const url = new URL(req.url)
17+
const autoClose = url.searchParams.get('onto') === 'auto-close'
18+
const resp = await proxy(req, { rawOutput: true })
19+
20+
if (resp instanceof Response && resp.ok) {
21+
return autoClose
22+
? // auto-close the window when logins come from the editor
23+
authFromEditor(resp)
24+
: // redirect to projects
25+
authFromRemix(resp)
26+
}
27+
28+
return resp
29+
}
30+
31+
function authFromEditor(resp: Response) {
32+
return new Response(resp.body, {
33+
headers: {
34+
'content-type': 'text/html',
35+
'cache-control': 'no-cache',
36+
'set-cookie': resp.headers.get('set-cookie') ?? '',
37+
},
38+
status: resp.status,
39+
})
40+
}
41+
42+
function authFromRemix(resp: Response) {
43+
return redirect('/projects', { headers: resp.headers })
44+
}

utopia-remix/app/util/api.server.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ describe('requireUser', () => {
3232
const req = newTestRequest({ authCookie: 'wrong' })
3333
const fn = async () => requireUser(req)
3434
await expect(fn).rejects.toThrow(ApiError)
35-
await expect(fn).rejects.toThrow('session not found')
35+
await expect(fn).rejects.toThrow('unauthorized')
3636
})
3737

3838
it('needs a user for the session cookie', async () => {
3939
const req = newTestRequest({ authCookie: 'invalid-key' })
4040
const fn = async () => requireUser(req)
4141
await expect(fn).rejects.toThrow(ApiError)
42-
await expect(fn).rejects.toThrow('user not found')
42+
await expect(fn).rejects.toThrow('unauthorized')
4343
})
4444

4545
it('returns the user details', async () => {

utopia-remix/app/util/api.server.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import type { UserDetails } from 'prisma-client'
66
import { PrismaClientKnownRequestError } from 'prisma-client/runtime/library.js'
77
import invariant from 'tiny-invariant'
88
import { ALLOW } from '../handlers/validators'
9-
import { getUserFromSession } from '../models/session.server'
109
import { ApiError } from './errors'
1110
import type { Method } from './methods.server'
1211
import { Status } from './statusCodes'
1312
import { ServerEnvironment } from '../env.server'
13+
import { maybeGetUserFromSession } from '../models/session.server'
1414

1515
interface ErrorResponse {
1616
error: string
@@ -192,8 +192,8 @@ export async function requireUser(
192192
try {
193193
const sessionId = getSessionId(request)
194194
ensure(sessionId != null, 'missing session cookie', Status.UNAUTHORIZED)
195-
const user = await getUserFromSession({ key: sessionId })
196-
ensure(user != null, 'user not found', Status.UNAUTHORIZED)
195+
const user = await maybeGetUserFromSession({ key: sessionId })
196+
ensure(user != null, 'unauthorized', Status.UNAUTHORIZED)
197197
return user
198198
} catch (error) {
199199
if (error instanceof ApiError && error.status === Status.UNAUTHORIZED) {
@@ -210,7 +210,7 @@ export async function getUser(request: Request): Promise<UserDetails | null> {
210210
if (sessionId == null) {
211211
return null
212212
}
213-
return getUserFromSession({ key: sessionId })
213+
return maybeGetUserFromSession({ key: sessionId })
214214
}
215215

216216
export function getProjectIdFromParams(params: Params<string>, key: string): string | null {

utopia-remix/app/util/auth0.server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import urlJoin from 'url-join'
22
import { ServerEnvironment } from '../env.server'
33

44
export function auth0LoginURL(): string {
5-
const behaviour = 'auto-close'
5+
const behaviour: 'auto-close' | 'authd-redirect' = 'authd-redirect'
66

77
const useAuth0 =
88
ServerEnvironment.AUTH0_ENDPOINT !== '' &&
@@ -12,7 +12,7 @@ export function auth0LoginURL(): string {
1212
console.warn(
1313
'Auth0 is disabled, if you need it be sure to set the AUTH0_ENDPOINT, AUTH0_CLIENT_ID, AUTH0_REDIRECT_URI environment variables',
1414
)
15-
const url = new URL(urlJoin(ServerEnvironment.BACKEND_URL, 'authenticate'))
15+
const url = new URL(urlJoin(ServerEnvironment.CORS_ORIGIN, 'authenticate'))
1616
url.searchParams.set('code', 'logmein')
1717
url.searchParams.set('onto', behaviour)
1818
return url.href

utopia-remix/server.js

-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ const app = express()
176176
app.disable('x-powered-by')
177177

178178
// proxy middleware hooks
179-
app.use('/authenticate', proxy)
180179
app.use('/editor', proxy)
181180
app.use('/hashed-assets.json', proxy)
182181
app.use('/logout', proxy)

0 commit comments

Comments
 (0)