Background
I'm writing a web API and webhooks (GraphQL for non-webhooks, but that's unimportant as that's just the outer layer) for a webapp where the vast majority of requests are authenticated, and the authorisation logic is slightly above trivial.
I am using Firestore which gives me an API to the database akin to a 'persistence layer' as well as my 'controllers' (either GQL queries/mutations or serverless function webhooks), and a 'service layer' which does the logic and calls to the persistence layer. These services also call each other, never reading each others collections in Firestore.
The app is a team chat room service, so there are terms such as "room", "membership" and "user" in the code below.
For authentication, I am receiving JWTs which I validate. I am not using custom claims in those JWTs - really just the user ID from them.
Current state
My services are constructed like this:
function createRoomService(
userToken: {uid: string, email_verified: boolean},
deps: {
firestore: FirestoreInstance
services: {
user: UserService
}
}): RoomService {
return {
getRoom(id: string) {
const room = deps.firestore.collection('rooms').get(id)
if (!room.allowedUsers.includes(userToken.uid)) {
return null
}
return room
},
changeUserStatus(roomId: string, status: string) {
const room = await this.getRoom(roomId)
if (!room) {
throw new Error("No such room or not your room");
}
// yes could do these in a batch and I really do
await deps.firestore.collection('rooms').doc(id).update({
users: {[userToken.uid]: {status}}
})
await deps.services.user.updateLastSeen();
return true // succceeded
}
}
}
GQL resolvers look like this:
getRoom(parent, args, context) {
return context.services.roomService.getRoom(args.id)
}
The userToken has already been added to the constructed service at the beginning of the request.
The problem
This is all fine, and you'll notice that the roomService
does its own authorisation on operations, and as the service makes calls between its own methods, in a sense "shares" some of that authorisation.
Now, I have a webhook call which comes from a trusted source which should update the user's status:
app.post("/subscription-lapsed", (req, res) => {
verifyRequestIsTrusted(req.headers.appToken)
const userId = req.body.userId
const roomId = req.body.subscriptionId
// I can't do this, because roomService is user-scoped, whereas this is not a user request
await roomService.changeUserStatus(roomId, "deactivated")
})
My original roomService
is user-scoped and requires a user token to create it, but this is being accessed outside of user context, but trusted.
Options I can think of
- Pass a fake user token to construct the service in the second case
- Don't require user token when creating service, but pass into each method (just pushes the problem somewhere else)
- Put the authorisation logic into the outer controller (GQL resolver or webhook handler) and make the
roomService
entirely privileged and assume it is supposed to make those changes. In this case, the controller would have to fetch data from the database to do the authorisation. - Have two layers of services - one which is totally privileged that receives
userId
as params to its methods, and the other which is user-scoped as it is now that the GQL layer uses. The webhook uses the privileged layer directly. - I'm sure there are others.
Questions
- Is this problem described, named and solved already somewhere?
- If so, what is it?!
- If not, how do the above options sound?
Aucun commentaire:
Enregistrer un commentaire