Skip to content

Commit

Permalink
fix: Improve withSessionAuth middleware (#225)
Browse files Browse the repository at this point in the history
Co-authored-by: Seam Bot <[email protected]>
  • Loading branch information
andrii-balitskyi and seambot authored Dec 3, 2024
1 parent 2a7cf09 commit 8476d35
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 59 deletions.
4 changes: 2 additions & 2 deletions src/lib/database/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ export const seedDatabase = (db: Database): Seed => {
})

db.addUserWorkspace({
user_id: seed.john_user_id,
workspace_id: seed.seed_workspace_2,
user_workspace_id: seed.john_user_workspace_1,
user_id: seed.john_user_id,
workspace_id: seed.seed_workspace_1,
is_owner: true,
})

Expand Down
122 changes: 67 additions & 55 deletions src/lib/middleware/with-session-auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import jwt from "jsonwebtoken"
import {
HttpException,
BadRequestException,
InternalServerErrorException,
type Middleware,
NotFoundException,
UnauthorizedException,
} from "nextlove"
import type { AuthenticatedRequest } from "src/types/authenticated-request.ts"

import type { Database } from "lib/database/index.ts"

import type { UserWorkspace } from "lib/zod/user_workspace.ts"

import { withSimulatedOutage } from "./with-simulated-outage.ts"

export const withSessionAuth =
Expand All @@ -35,7 +37,13 @@ export const withSessionAuth =
async (req, res) => {
const token = req.headers.authorization?.split("Bearer ")?.[1]

if (token == null) return res.status(401).end("Unauthorized")
if (token == null) {
throw new UnauthorizedException({
type: "unauthorized",
message:
"No token found in header (did you mean to add Authorization?)",
})
}

const workspace_id_from_header = req.headers["seam-workspace"]
const workspace_id =
Expand All @@ -45,75 +53,79 @@ export const withSessionAuth =
: ""

if (workspace_id.length === 0 && is_workspace_id_required) {
throw new UnauthorizedException({
throw new BadRequestException({
type: "missing_workspace_id",
message: "Workspace ID is required",
message:
"When using user session authentication, you must provide the Seam-Workspace header",
})
}

const is_jwt = token.startsWith("ey")

let decodedJwt: any
if (is_jwt) {
try {
decodedJwt = jwt.verify(token, "secret")
} catch (error) {
throw new UnauthorizedException({
type: "invalid_jwt",
message: "Invalid JWT",
})
}
try {
decodedJwt = jwt.verify(token, "secret")
} catch (error: any) {
throw new InternalServerErrorException({
type: "internal server error",
message: error.message,
})
}

const user_session = req.db.user_sessions.find(
(us) => us.user_id === decodedJwt.user_id,
)
const { user_id, key } = decodedJwt

const user_workspace = req.db.user_workspaces.find(
(uw) => uw.user_id === decodedJwt.user_id,
const user_session = req.db.user_sessions.find(
(us) => us.user_id === user_id && us.key === key,
)

if (user_session == null) {
throw new UnauthorizedException({
type: "unauthorized",
message: "Session not found",
})
}

let user_workspace: UserWorkspace | undefined
if (workspace_id.length !== 0) {
user_workspace = req.db.user_workspaces.find(
(uw) => uw.user_id === user_id && uw.workspace_id === workspace_id,
)

if (user_session == null) {
throw new NotFoundException({
type: "user session_not_found",
message: "User Session not found",
if (user_workspace == null) {
throw new UnauthorizedException({
type: "unauthorized",
message: "User does not have access to this workspace",
})
}
}

if (is_workspace_id_required) {
;(req.auth as Extract<
AuthenticatedRequest["auth"],
{
type: "user_session"
}
>) = {
type: "user_session",
workspace_id: workspace_id ?? user_workspace?.workspace_id,
user_id: user_session.user_id,
}
} else {
;(req.auth as Extract<
AuthenticatedRequest["auth"],
{ type: "user_session_without_workspace" }
>) = {
type: "user_session_without_workspace",
user_id: user_session.user_id,
if (is_workspace_id_required) {
;(req.auth as Extract<
AuthenticatedRequest["auth"],
{
type: "user_session"
}
>) = {
type: "user_session",
user_session_id: user_session.user_session_id,
workspace_id,
user_id,
}
} else {
;(req.auth as Extract<
AuthenticatedRequest["auth"],
{ type: "user_session_without_workspace" }
>) = {
type: "user_session_without_workspace",
user_session_id: user_session.user_session_id,
user_id,
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return withSimulatedOutage(next as unknown as any)(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
req as unknown as any,
res,
)
}

// Cannot run middleware after auth middleware.
// UPSTREAM: https://github.com/seamapi/nextlove/issues/118
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument

throw new HttpException(500, {
type: "unknown_auth_mode",
message: "Unknown Auth Mode",
})
return withSimulatedOutage(next as unknown as any)(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
req as unknown as any,
res,
)
}
2 changes: 1 addition & 1 deletion src/pages/api/workspaces/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { workspace } from "lib/zod/index.ts"

export default withRouteSpec({
methods: ["GET", "POST"],
auth: ["client_session", "pat_with_workspace", "console_session", "api_key"],
auth: ["client_session", "pat_with_workspace", "api_key", "console_session"],
jsonResponse: z.object({
workspace,
}),
Expand Down
4 changes: 3 additions & 1 deletion src/types/authenticated-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export type AuthenticatedRequest = Request & {
}
| {
type: "user_session"
workspace_id: string
user_id: string
user_session_id: string
workspace_id: string
}
| {
type: "user_session_without_workspace"
user_id: string
user_session_id: string
}
}
67 changes: 67 additions & 0 deletions test/middleware/with-session-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import test from "ava"
import jwt from "jsonwebtoken"

import {
getTestServer,
type SimpleAxiosError,
} from "fixtures/get-test-server.ts"
import { seedDatabase } from "lib/database/seed.ts"

test("withSessionAuth middleware - successful auth", async (t) => {
const { axios, db } = await getTestServer(t, { seed: false })
const seed_result = seedDatabase(db)

const token = jwt.sign(
{
user_id: seed_result.john_user_id,
key: seed_result.john_user_key,
},
"secret",
)

// Test successful auth with workspace
const { status } = await axios.get("/workspaces/get", {
headers: {
Authorization: `Bearer ${token}`,
"Seam-Workspace": seed_result.seed_workspace_1,
},
})
t.is(status, 200)

// Test missing workspace header
const missingWorkspaceErr = await t.throwsAsync<SimpleAxiosError>(
axios.get("/workspaces/get", {
headers: {
Authorization: `Bearer ${token}`,
},
}),
)

t.is(missingWorkspaceErr?.status, 400)
t.is(missingWorkspaceErr?.response.error.type, "missing_workspace_id")

// Test invalid token
const invalidTokenErr = await t.throwsAsync<SimpleAxiosError>(
axios.get("/workspaces/get", {
headers: {
Authorization: "Bearer invalid_token",
"Seam-Workspace": seed_result.seed_workspace_1,
},
}),
)

t.is(invalidTokenErr?.status, 500)

// Test unauthorized workspace access
const unauthorizedErr = await t.throwsAsync<SimpleAxiosError>(
axios.get("/workspaces/get", {
headers: {
Authorization: `Bearer ${token}`,
"Seam-Workspace": "invalid_workspace_id",
},
}),
)

t.is(unauthorizedErr?.status, 401)
t.is(unauthorizedErr?.response.error.type, "unauthorized")
})

0 comments on commit 8476d35

Please sign in to comment.