micahlt / modchat-server

💽 Server component for the Modchat rebuild
GNU General Public License v3.0
4 stars 2 forks source link

OAuth Security Feature Request: Session Token Authentication #4

Closed AmazingMech2418 closed 2 years ago

AmazingMech2418 commented 3 years ago

A more secure way to authenticate requests than storing passwords and tokens in localStorage or cookies is to use session tokens.

Session token cookies can be made HTTP-Only, meaning that the user cannot access the cookie with JavaScript and it can only be modified through web requests. JWT tokens are good for this since they use RSA-based verification as well so that someone can't forge it as easily. However, since this uses two domains, I am not exactly sure if this will work with HTTP-Only cookies, but there might still be a way.

These tokens can store basic authentication data that can then be used to generate tokens that can be used to authenticate requests, such as through sockets, as well. These tokens can also be generated through a request with closures to ensure that they are not readily accessible through XSS attacks. While recreating this request is of course possible, this still provides yet another layer of security. Additionally, I am working on a system to be implemented into Scratch to prevent unwanted API requests, and this could be used as well once finished.

iamperry294 commented 2 years ago

Session token authentication is currently being worked on.

This is the flow:

The user logs in with correct credentials. This is checked against the DB with a hashed version of the password. Two session tokens are generated. One is an access token and one is a refresh token. The access token can either be a JWT or an Opaque token. The benefit of Opaque for ModChat would be that banning users is instant and we hit the database on most requests anyway. The access and refresh token are both session ID tokens.

The refresh token is 100 days long and the access token is anywhere from 1 to 24 hours long. These values are stored in the database. HTTP-Only cookies with CSRF protections will be sent to the client. Since the refresh token is used to get a new access token once it has expired, the cookie will be scoped to the refresh token endpoint. Every Socket.IO request, the access token is sent. It is verified against the DB by checking if it hasn't expired and matches the access token in the DB.

If the access token expires, Socket.IO will let the client know. The client will then send a request to an endpoint like "/api/refresh" with the refresh token. If the token has expired, the user is logged out. If the token hasn't expired and is valid, the user gets a new access token and refresh token.

Getting a new refresh token allows us to detect refresh token theft and makes for much better UX. Since the refresh token expires after 100 days, if the user login on the 100th day, the session will be extended another day. We can detect refresh token theft if the attacker and victim are online at the same time.

The user has Refresh Token 0 and Access Token 0. We'll call these RT0 and AT0. The attacker steals RT0. The victim or attacker makes a request to the refresh API. If the attacker makes a request first, the attacker gets AT1 and RT1. The the victim uses RT0, which the server knows is the old token. The session is then revoked since token theft has happened. A similar scenario would happen if the victim made a request first.

Refresh Token Theft Detection does lead to race conditions on the browser. This is prevented by only making sure the refresh API is called when making a socket request. This is optional but would be nice to have. If the attacker steals an access token, then theft cannot be detected with algorithms.

To prevent more race conditions such as the browser not getting the cookie, make sure to only invalidate the old tokens after the cookies have been received. This confirms that the client has received the cookies. When banning or logging out users, revoke the session tokens on the backend. If using a JWT access token, revoke the refresh token and clear the cookies from the frontend.

Refresh Token theft doesn't need to be detected by storing all of the old tokens, but this needs discussion. Currently, the prototype of this uses Opaque access and refresh tokens because they allow us to revoke sessions and we already hit the DB on most requests. This also needs discussion. The flow is much more secure than storing tokens and passwords in localStorage. This is a very similar flow to RFC 6819 and is recommended in OAuth 2.1.

illogicalapple commented 2 years ago

@iamperry294 wow detailed the problem is i dont understand it a bit

AmazingMech2418 commented 2 years ago

@iamperry294 I like the idea of using a refresh token to refresh the access token. Though, is the access token sent through the socket and refresh as an HTTP-Only cookie? If another model were used, it would make it more likely that both get stolen simultaneously which would harm theft detection efforts. Also, I am not familiar with Opaque session tokens, but I'm not sure how a specific token format would make it easier to enforce bans. It wouldn't be that difficult to block the current tokens for that user and prevent a new token from being generated. Also multi-level token systems might be useful to secure more privileged requests, for example those from admins. It might be possible to in those cases send a session request to generate a single-use admin token to be used along with the other tokens to prevent admin token theft.

AmazingMech2418 commented 2 years ago

Wait, quick question regarding the token system, what are you going to do with server persistence? I remember Heroku didn't have any filesystem persistence so the only way to do it would be a ReplDB backup or external DB, but I also remember some DoS issues with that. But storing the active tokens would require some sort of data saving mechanism or otherwise, it would be extremely easy to forge a token.

micahlt commented 2 years ago

Modchat now uses an external MongoDB instance to store room and user data.

iamperry294 commented 2 years ago

@AmazingMech2418 The access token is sent through sockets and the refresh token will be sent on API requests such as /api/refresh. There seems to be a bug with Express right now where you can't scope a cookie to a path without the cookie going away, so that may be a risk. Opaque session tokens are just storing the tokens in MongoDB and checking against them every request. It's easier to enforce bans because the tokens are revocable unlike a JWT. With a JWT, you can use the token until it expires. True, but for authentication, we're hitting the database anyway with Opaque. Since everything is stored in the user's data, it's quite easy to authorize a user that way. And correct, we are using MongoDB now. JWT isn't supposed to have a DB call. Something else to note is that since we are using WebSockets, cookies may not work.

AmazingMech2418 commented 2 years ago

@iamperry294 A JWT token is literally just a token format though. LOL! It can be revoked either way when the server realizes the token isn't valid. Honestly, if you really wanted, you could just generate a really long string and make that the token. LOL! Also, cookies aren't the only method of transmitting access tokens... And anyways, limiting websocket use would be the best either way to increase security and decrease stress on the server through polling. Also, what do you mean the cookie is "going away"?

iamperry294 commented 2 years ago

@AmazingMech2418 Generally speaking, a JWT token is used to limit database requests. The really long string thing IS an Opaque token. A cookie isn't an option and localStorage isn't an option. Storing the token in Javascript memory with closures could work. That is correct, but I'm not sure how @micahlt thinks about this. WebSockets have benefits. The cookie doesn't create on the client. Even if you edit a cookie after you get one to change the path, the cookie doesn't exist anymore. It's an odd problem related to Express, apparently.

AmazingMech2418 commented 2 years ago

@iamperry294 Well, it depends on the type of JWT token. There are some specifically designed to ping the server with an RSA private key that is supposed to match with a public key to verify the authentication. Also, the benefits of WebSockets are primarily the ability of the server to communicate with the user instead of requiring client-side polling, but any time you can avoid that, it is ideal to do so because of the security issues with WebSockets. And what do you mean "change the path"? Look here for information on HTTP-Only cookies. https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies

iamperry294 commented 2 years ago

@AmazingMech2418 Any stateless JWT does that. That doesn't really let you revoke the token and you're bottlenecked against one key. WebSockets do have security issues, but switching to HTTP long polling would not cover the benefits of WebSockets. "You can also set additional restrictions to a specific domain and path to limit where the cookie is sent." That's what I mean by changing the path.

AmazingMech2418 commented 2 years ago

@iamperry294 If the JWT token matches and you just find it to be in the databased of blocked tokens, you can still revoke it. And I'm not suggesting HTTP long polling, just not relying quite so much on websockets for everything. And yes, but there's no need to restrict the domain or path, especially if you are using two separate servers for the client and server ends.

AmazingMech2418 commented 2 years ago

Honestly, using two servers in the first place is a bad idea security-wise.

iamperry294 commented 2 years ago

@AmazingMech2418 That removes the purpose of a JWT. Sure, but what would be replaced with HTTP long polling? And ideally, you don't want the refresh token floating around every request that isn't a WebSocket one. It's one server, not two servers. If you mean one client and server, then sure.

AmazingMech2418 commented 2 years ago

@iamperry294 I'm not suggesting long-polling, just not using websockets in ways where fetch would be better. And true, but that's not what setting the path does. What it does is set the paths that can receive that cookie, not the request endpoints that take it in. And well, you need a webserver to host the client. LOL! But it's better to just use one server for both.

iamperry294 commented 2 years ago

@AmazingMech2418 Where though? I'm not 100% familiar with the codebase yet. The path would be ideally used to scope the refresh token to /api/refresh though. That's true, but I don't see the security downsides with that.

AmazingMech2418 commented 2 years ago

@iamperry294 I'm not familiar with the new codebase either, but if it's anything like v1, almost everything uses sockets. LOL! But anything other than chat messages and possibly room joining doesn't need that... And that's not what path restriction does. LOL! If you scope to /api/refresh, the cookie will only be stored if you are on /api/refresh. And there are a lot of security downsides... First off, two domains means you can't use domain scoping or samesite with cookies. You also would need to treat all API endpoints as CORS for fetch which has security downsides itself. MitM attacks are common in this scenario, etc.

iamperry294 commented 2 years ago

@AmazingMech2418 It's been a while since I have worked with MCv1, is it like there's zero endpoints and it's 100% sockets? Oh, but that's not exactly what I meant. And I believe you can, I've tried it and it works. I didn't expect it to work though. And MitM for what?

AmazingMech2418 commented 2 years ago

@iamperry294 Yeah, pretty much. And I don't believe you can. And MitM attacks are just overall easier with two servers instead of one. First off, someone could intercept requests midway. Second, someone could hack the client and steal headers and other information that way. Third, someone could hack the server and steal information that way. And it's harder to detect these sorts of attacks when there are more entrypoints, such as when you have two servers instead of one.

iamperry294 commented 2 years ago

@AmazingMech2418 Yeah, sockets are only used for chat messages and joining a room. I'm not sure if it does anything, but for sure, it does seem to work. And of course, but that's something TLS or SSL help with. ModChat automatically enforces HTTPS.

iamperry294 commented 2 years ago

@AmazingMech2418 The biggest hurdle right now is where to store an access token in the browser. Everything else is fine.

AmazingMech2418 commented 2 years ago

@iamperry294 I think the best method of storage would just be to have the refresh token as an HTTP-Only cookie which has a session request sent to the server to get the access token which is then stored in a closure.

iamperry294 commented 2 years ago

@AmazingMech2418 So you're saying the Access Token should be stored in a JS variable in a closure?

iamperry294 commented 2 years ago

@AmazingMech2418 The closure will work, but there are still a couple complications. One is how should refresh token theft detection work? Another one is if multiple devices at the same time. I don't know if that's a supported scenario however.

iamperry294 commented 2 years ago

Fixed in f6057d7.