Skip to content

Commit

Permalink
Fix gateway user verification (#186)
Browse files Browse the repository at this point in the history
Gateway does not detect the user id param in the path, causing
verification to fail and redirect to the frontend address.

Let's add a new header to store the current user id, which should be
the same as that of the param uid.
  • Loading branch information
yhtMinceraft1010X authored Oct 26, 2023
1 parent ae0c485 commit 0f81576
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 38 deletions.
1 change: 1 addition & 0 deletions frontend/src/firebase-client/useDeleteOwnAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const useDeleteOwnAccount = () => {
method: "DELETE",
headers: {
"User-Id-Token": idToken,
"User-Id": currentUser.uid
},
});
// This will delete the user from the Firebase Authentication database
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/api/userHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EditableUser } from "@/types/UserTypes";

export const updateUserByUid = async (user: EditableUser, currentUser: any) => {
try {
const url = `${userApiPathAddress}${currentUser.uid}}`;
const url = `${userApiPathAddress}${currentUser.uid}`;
const idToken = await currentUser.getIdToken(true);

console.log("user", user);
Expand All @@ -14,6 +14,7 @@ export const updateUserByUid = async (user: EditableUser, currentUser: any) => {
headers: {
"Content-Type": "application/json",
"User-Id-Token": idToken,
"User-Id": currentUser.uid
},
body: JSON.stringify(user),
});
Expand Down
4 changes: 3 additions & 1 deletion services/gateway/src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {frontendAddress} from "../proxied_routes/service_names";

const redirectLink = frontendAddress;
const userIdTokenHeader = "User-Id-Token";
const userIdHeader = "User-Id";

export const setupIsLoggedIn = (app : Express, routes : any[]) => {
routes.forEach(r => {
Expand Down Expand Up @@ -32,8 +33,9 @@ export const setupUserIdMatch = (app : Express, routes : any[]) => {
routes.forEach(r => {
app.use(r.url, function(req : express.Request, res : express.Response, next : express.NextFunction) {
if (r.user_match_required_methods.includes(req.method)) {
console.log(req.params)
const idToken = req.get(userIdTokenHeader);
const paramUid = req.params.uid;
const paramUid = req.get(userIdHeader);
if (!idToken || !paramUid) {
res.redirect(redirectLink)
} else {
Expand Down
6 changes: 6 additions & 0 deletions services/user-service/openapiDoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
},
Expand All @@ -92,6 +95,9 @@
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
},
Expand Down
86 changes: 60 additions & 26 deletions services/user-service/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,72 @@ indexRouter.get(
indexRouter.put(
"/:uid",
function (req: express.Request, res: express.Response) {
userDatabaseFunctions
.updateUserByUid(req.params.uid, req.body)
.then((result) => {
res.status(200).json(result);
})
.catch((error) => {
if (error.code === "P2025") {
res.status(404).end();
} else {
// Server side error such as database not being available
res.status(500).end();
}
});
/**
* Need to check that header UID was not tampered with.
*
* Attack Scenario:
* 1) User 2 wants to edit profile of user 1.
* 2) This should be blocked by the gateway since path param uid = 1 and header uid = 1
* but user 2 only has authentication token for user 2
* 3) User 2 could change header uid = 2 to pass authentication, but retain path param uid = 1
* to attempt to change user 1's data
* 4) Hence, need to check that param uid = header uid
*/
const pathUid = req.params.uid;
const headerUid = req.get("User-Id");
if (pathUid !== headerUid) {
res.status(400).end();
} else {
userDatabaseFunctions
.updateUserByUid(req.params.uid, req.body)
.then((result) => {
res.status(200).json(result);
})
.catch((error) => {
if (error.code === "P2025") {
res.status(404).end();
} else {
// Server side error such as database not being available
res.status(500).end();
}
});
}
}
);

indexRouter.delete(
"/:uid",
function (req: express.Request, res: express.Response) {
userDatabaseFunctions
.deleteUserByUid(req.params.uid)
.then(() => {
res.status(204).end();
})
.catch((error) => {
if (error.code === "P2025") {
res.status(404).end();
} else {
// Server side error such as database not being available
res.status(500).end();
}
});
/**
* Need to check that header UID was not tampered with.
*
* Attack Scenario:
* 1) User 2 wants to edit profile of user 1.
* 2) This should be blocked by the gateway since path param uid = 1 and header uid = 1
* but user 2 only has authentication token for user 2
* 3) User 2 could change header uid = 2 to pass authentication, but retain path param uid = 1
* to attempt to change user 1's data
* 4) Hence, need to check that param uid = header uid
*/
const pathUid = req.params.uid;
const headerUid = req.get("User-Id");
if (pathUid !== headerUid) {
res.status(400).end();
} else {
userDatabaseFunctions
.deleteUserByUid(req.params.uid)
.then(() => {
res.status(204).end();
})
.catch((error) => {
if (error.code === "P2025") {
res.status(404).end();
} else {
// Server side error such as database not being available
res.status(500).end();
}
})
}
}
);

Expand Down
39 changes: 35 additions & 4 deletions services/user-service/systemtest/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const updatedNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl"

const updatePayload = { matchDifficulty: 1 };

const userIdHeader = "User-Id";

describe('/index', () => {
describe('Sample App Workflow', () => {
it('Step 1: Add user 1 to database should pass with status 201', async () => {
Expand All @@ -28,9 +30,21 @@ describe('/index', () => {
expect(response.body).toStrictEqual(fullNewUser);
})

it('Step 3a: Update details of user 1 from database by user 2 should fail with error 400', async () => {
// The function being tested
const response = await request(app)
.put('/api/user-service/1')
.set(userIdHeader, "2")
.send(updatePayload);
expect(response.status).toStrictEqual(400);
})

it('Step 3: Update details of user 1 from database should pass', async () => {
// The function being tested
const response = await request(app).put('/api/user-service/1').send(updatePayload);
const response = await request(app)
.put('/api/user-service/1')
.set(userIdHeader, "1")
.send(updatePayload);
expect(response.status).toStrictEqual(200);
expect(response.body).toStrictEqual(updatedNewUser);
})
Expand All @@ -47,8 +61,19 @@ describe('/index', () => {
expect(response.status).toStrictEqual(200);
})

it('Step 6a: Delete user 1 from database by user 2 should fail with status 400', async () => {
const response = await request(app)
.delete('/api/user-service/1')
.set(userIdHeader, "2")
.send();
expect(response.status).toStrictEqual(400);
})

it('Step 6: Delete user 1 from database', async () => {
const response = await request(app).delete('/api/user-service/1').send();
const response = await request(app)
.delete('/api/user-service/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(204);
})

Expand All @@ -60,13 +85,19 @@ describe('/index', () => {

it('Step 8: Update details of now deleted user 1 should fail', async () => {
// The function being tested
const response = await request(app).put('/api/user-service/1').send(updatePayload);
const response = await request(app)
.put('/api/user-service/1')
.set(userIdHeader, "1")
.send(updatePayload);
expect(response.status).toStrictEqual(404);
})

it('Step 9: Deleting the now deleted user 1 should fail', async () => {
// The function being tested
const response = await request(app).delete('/api/user-service/1').send();
const response = await request(app)
.delete('/api/user-service/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(404);
})
})
Expand Down
57 changes: 51 additions & 6 deletions services/user-service/test/routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import request from 'supertest';
vi.mock('../../src/db/functions')

const app = express();
const userIdHeader = "User-Id";
app.use(indexRouter);

const fullNewUser = { uid: '1', displayName: 'Test User', photoUrl: "fakeUrl", matchDifficulty: 0,
Expand Down Expand Up @@ -93,7 +94,10 @@ describe('/index', () => {
userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser);

// The function being tested
const response = await request(app).put('/1').send();
const response = await request(app)
.put('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(200);
expect(response.body).toStrictEqual(fullNewUser);
})
Expand All @@ -106,7 +110,10 @@ describe('/index', () => {
}));

// The function being tested
const response = await request(app).put('/1').send();
const response = await request(app)
.put('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(404);
})

Expand All @@ -115,9 +122,25 @@ describe('/index', () => {
userDatabaseFunctionsMock.updateUserByUid.mockRejectedValueOnce(new Error());

// The function being tested
const response = await request(app).put('/1').send();
const response = await request(app)
.put('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(500);
})

it('[PUT] /1 but the header UID does not match path param UID', async () => {

// Used to get back the user
userDatabaseFunctionsMock.updateUserByUid.mockResolvedValueOnce(fullNewUser);

// The function being tested
const response = await request(app)
.put('/1')
.set(userIdHeader, "2")
.send();
expect(response.status).toStrictEqual(400);
})
})

describe('deleteUserByUid', () => {
Expand All @@ -127,7 +150,10 @@ describe('/index', () => {
userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser);

// The function being tested
const response = await request(app).delete('/1').send();
const response = await request(app)
.delete('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(204);
})

Expand All @@ -139,7 +165,10 @@ describe('/index', () => {
}));

// The function being tested
const response = await request(app).delete('/1').send();
const response = await request(app)
.delete('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(404);
})

Expand All @@ -148,8 +177,24 @@ describe('/index', () => {
userDatabaseFunctionsMock.deleteUserByUid.mockRejectedValueOnce(new Error());

// The function being tested
const response = await request(app).delete('/1').send();
const response = await request(app)
.delete('/1')
.set(userIdHeader, "1")
.send();
expect(response.status).toStrictEqual(500);
})

it('[DELETE] /1 but the header UID does not match path param UID', async () => {

// Used to get back the user
userDatabaseFunctionsMock.deleteUserByUid.mockResolvedValueOnce(fullNewUser);

// The function being tested
const response = await request(app)
.delete('/1')
.set(userIdHeader, "2")
.send();
expect(response.status).toStrictEqual(400);
})
})
})

0 comments on commit 0f81576

Please sign in to comment.