hippware / wocky

Server side (Erlang/Elixir) software
13 stars 3 forks source link

Research token-based authentication systems #1055

Closed bengtan closed 6 years ago

bengtan commented 7 years ago

OAuth 2 uses a "two-token" system for added security. We may be able to use an OAuth 2 implementation directly, or use its token system as a model to improve our own.

From https://github.com/hippware/rn-chat/issues/1421#issuecomment-339441540

Please go ahead and:

  • Scope out the work involved (for the server side) to implement OAuth, and
  • Put together an implementation proposal for a 'two token' system.
toland commented 6 years ago

The main OAuth2 site is at https://oauth.net/2/

The RFCs are relevant to the work we are doing:

OAuth 2 Simplified is a nice overview of OAuth 2.

toland commented 6 years ago

After digging into the topic, I have some thoughts on our path forward.

First, the criticisms of the MIM two-token implementation generally also apply to OAuth 2. It is not a perfect scheme but makes some reasonable security/performance/usability tradeoffs. OAuth 2 is laser-focused on browser-based apps and 3rd-party delegation. This means that it has many mechanisms and features which aren't really relevant to our use case. The MIM token system is a reasonable implementation of the two-token system without the extra cruft that OAuth 2 brings along.

Second, Firebase is an OAuth 2 system and to the extent that we have implemented token-based authentication ourselves we are reinventing the wheel. There is no reason that we shouldn't just use the Firebase tokens as our primary authentication mechanism and not implement anything extra ourselves.

Third, there are some privacy and security considerations that we need to take into account when using SMS authentication via Firebase. From the Firebase documentation:

Phone numbers that end-users provide for authentication will be sent and stored by Google to improve our spam and abuse prevention across Google services, including but not limited to Firebase. Developers should ensure they have appropriate end-user consent prior to using the Firebase Authentication phone number sign-in service.

and

Authentication using only a phone number, while convenient, is less secure than the other available methods because possession of a phone number can be easily transferred between users. Also, on devices with multiple user profiles, any user that can receive SMS messages can sign in to an account using the device's phone number.

If you use phone number based sign-in in your app, you should offer it alongside more secure sign-in methods, and inform users of the security tradeoffs of using phone number sign-in.

I will write up a summary of the problem and a plan of action and post it here and in #development on Slack.

thescurry commented 6 years ago

@toland thanks for the blurb on numbers being stored on google infrastructure. I've passed that to our attorneys and they are updated our privacy policy.

bengtan commented 6 years ago

I will write up a summary of the problem and a plan of action and post it here and in #development on Slack.

Looking forward to it.

toland commented 6 years ago

So, here is the gist of it:

Firebase is an OAuth 2 service. The client is provided with both an access token and a refresh token when they authenticate the user via SMS. Any additional authentication that is done on the server side is unnecessary. Using this method, the client should only ever need to do SMS authentication on a fresh install or when the refresh token has been invalidated.

The general idea is that the client will authenticate with the Firebase external ID and access token. The client can keep the refresh token encrypted in local storage and get a new access token when the old one expires.

This has the benefit of allowing us to use any authentication mechanism supported by Firebase without changing the backend code. A downside is that we are now intimately tied to Firebase for authentication. However, we could use any OAuth 2 provider with minimal changes to the backend (including one that we operate).

I have been handwavey about the details of the XMPP authentication since I think that might be a little tricky. However, since everything else lines up nicely, I am sure we can find a way to make it work. To that end, I am reviewing the MIM and Wocky authentication code to come up with something more concrete. In the meantime, feedback is welcome.

bengtan commented 6 years ago

Alright, I think this discussion is starting in earnest now.

Firebase is an OAuth 2 service.

There are a number of things that this statement of yours might be implying. I'm going to try to bring these out.

Are you reasoning that 'If we want an OAuth 2 service, and if Firebase is an OAuth 2 service, then we might as well use Firebase instead of implementing our own OAuth 2 service'?

Not that I'm against using Firebase as our OAuth 2 service (It's too early for me to be decided), but that I'm probing our assumptions.

A downside is that we are now intimately tied to Firebase for authentication.

Which might be an important restriction for 'business rules' reasons rather than software/technical reasons.

However, we could use any OAuth 2 provider with minimal changes to the backend

One advantage of implementing our own is that we can choose token durations that suit us. If we use firebase, we'd be stuck with their token duration policy (whatever that is), no?

I guess we could try to find out what their durations are. Maybe they match close enough to our 'ideal' case that this is not an issue.

This has the benefit of allowing us to use any authentication mechanism supported by Firebase without changing the backend code.

I'm going to throw a potential spanner into this argument. Let's say we eventually decide to implement password-based authentication.

So, either:

Not ideal, but unknown whether this is significant or not.


From a broader view, @toland, I think you're going to be suggesting an OAuth 2 system of some form? But maybe the parameters (ie. token durations) are undecided yet?


The client is provided with both an access token and a refresh token when they authenticate the user via SMS.

I didn't realise the client gets two tokens.

@southerneer, @aksonov:

Can one of you verify that firebase authentication gives the client TWO tokens, an access token and a refresh token?

bengtan commented 6 years ago

@toland:

Two more subtopics I'm going to throw out for you to consider. Just to complicate matters :)

  1. Whatever OAuth2/token system we use, it would be nice if it's also suitable (ie. not insecure) for using for http traffic ie. use the token as a cookie or as an 'Authentication:' http header. This might complicate matters because http/web-browser traffic has to deal with a wider range of threats.

  2. Are you factoring 'How to pass the user agent (or else, a version number) to the server' in your thinking? This subtopic (ie. identifying the client version in order to decide whether to deny access) was included as part of this main topic because it might be convenient or related. However, if it turns out to be the opposite, then we can split this subtopic out as a separate thing.

southerneer commented 6 years ago

@aksonov brought up a good question...how do we implement "bypass" authentication on Staging if the password is restricted to a valid firebase auth token?

aksonov commented 6 years ago

And what if we will want to decide to switch to other auth provider (auth0 or something else)

toland commented 6 years ago

how do we implement "bypass" authentication on Staging if the password is restricted to a valid firebase auth token?

I don't see any reason why the current bypass mechanism would not work in a pure OAuth regime. We don't currently validate the Firebase token when the phone number starts with "+1555", and we could continue to do that. Keep in mind that "bypass" in staging is a potential security problem and we shouldn't accept the status quo as necessarily the best way to do things.

An alternative that comes to mind is to have a set of pre-defined reusable test accounts in Firebase. Since Firebase supports multiple authentication mechanisms, we could auth those accounts via a good old-fashioned password (or whatever). This probably wouldn't make sense for testing, but might make sense for staging.

Of course, we could also accept a predefined "bypass token" for testing accounts. I would probably insist on restricting these to a small set of matched ID/token pairs that would be accepted by the server as valid without going through Firebase validation.

toland commented 6 years ago

And what if we will want to decide to switch to other auth provider (auth0 or something else)

Personally, I am agnostic on which 3rd-party authentication provider we use. It simply doesn't matter to me whether we use Firebase, Auth0, or Admiral Bob's Discount Authentication and Car Washing Service. That we need to use a 3rd-party authentication service seems like a foregone conclusion at this point. I am not sure that it is productive to quibble too much about which one.

We can use another OAuth 2 provider with minimal changes on the backend and little to no changes in the protocol. For example, we could authenticate against Facebook or Google quite easily. That is one of the benefits of using a pure OAuth solution. Any service which implements OAuth 2 and provides an API that we can use to validate tokens is fair game. FWIW, it appears that Auth0 uses OAuth 2.

toland commented 6 years ago

Are you reasoning that 'If we want an OAuth 2 service, and if Firebase is an OAuth 2 service, then we might as well use Firebase instead of implementing our own OAuth 2 service'?

Yes. As I said before, I am agnostic on which 3rd-party OAuth 2 service we use. Since we already use Firebase and are dependent upon it for SMS authentication, then we might as well go ahead and use that service more fully. I don't think it makes sense to use Firebase for SMS authentication and then implement another OAuth 2 service for non-SMS authentication. That is duplicating effort without bringing any real value.

One advantage of implementing our own is that we can choose token durations that suit us. If we use firebase, we'd be stuck with their token duration policy (whatever that is), no?

I guess we could try to find out what their durations are. Maybe they match close enough to our 'ideal' case that this is not an issue.

This is from the Firebase documentation:

Firebase Authentication sessions are long lived. Every time a user signs in, the user credentials are sent to the Firebase Authentication backend and exchanged for a Firebase ID token (a JWT) and refresh token. Firebase ID tokens are short lived and last for an hour; the refresh token can be used to retrieve new ID tokens. Refresh tokens expire only when one of the following occurs:

  • The user is deleted
  • The user is disabled
  • A major account change is detected for the user. This includes events like password or email address updates.

The Firebase Admin SDK provides the ability to revoke refresh tokens for a specified user. In addition, an API to check for ID token revocation is also made available. With these capabilities, you have more control over user sessions. The SDK provides the ability to add restrictions to prevent sessions from being used in suspicious circumstances, as well as a mechanism for recovery from potential token theft.

So, to summarize:

I don't think that we can change these policies, but I also don't see a reason why we would want to.

I'm going to throw a potential spanner into this argument. Let's say we eventually decide to implement password-based authentication.

I don't see any reason why would implement our own password authentication.

Use firebase for password authentication. In which case, the passwords are kept at Google/firebase. I'm not sure whether this is a good thing or not.

I think that cat is already out of the bag. Using Firebase means that we are already storing user phone numbers and other account details at Google.

We lose control over whether the passwords are hashed/salted etc.

The passwords Google stores are almost certainly hashed and salted.

Implement password authentication ourselves. Which is duplication and possibly non-intuitive for the client ie. If the client gets a refresh token as a result of password authentication, it has to remember to use it on our system, not firebase.

And further, this is no security panacea. If we implement our own system then we are right back into the security morass of designing a secure protocol, ensuring storage is secured and encrypted at rest, and so on and so forth. I am fine outsourcing all of that drudgery to someone else.

From a broader view, @toland, I think you're going to be suggesting an OAuth 2 system of some form? But maybe the parameters (ie. token durations) are undecided yet?

Yes and no. I am suggesting that we use an OAuth 2 system, and specifically Firebase. I don't think any of the parameters are undecided. The only thing pending at this point is how to shoehorn this into XMPP.

toland commented 6 years ago

Whatever OAuth2/token system we use, it would be nice if it's also suitable (ie. not insecure) for using for http traffic ie. use the token as a cookie or as an 'Authentication:' http header. This might complicate matters because http/web-browser traffic has to deal with a wider range of threats.

This is the exact use case for OAuth 2. It was designed specifically to address that wider range of threats that are presented when an app runs in the browser. There is, of course, disagreement about whether OAuth 2 is the "right" or "best" way to do authentication, but it is widely used and well understood.

Also, be careful not to conflate using HTTP as a protocol and running an app in a browser. Those are separate issues with unique security considerations.

Are you factoring 'How to pass the user agent (or else, a version number) to the server' in your thinking? This subtopic (ie. identifying the client version in order to decide whether to deny access) was included as part of this main topic because it might be convenient or related. However, if it turns out to be the opposite, then we can split this subtopic out as a separate thing.

Yes, I am thinking about client validation as part of the issue of shoehorning OAuth 2 into XMPP. I don't really have anything more to say on the topic at this time, but I will update the ticket soon as I have more information.

toland commented 6 years ago

We lose control over whether the passwords are hashed/salted etc.

The passwords Google stores are almost certainly hashed and salted.

This page in the Firebase API documentation implies that Firebase hashes and salts passwords.

This blog post from 2016 states that Firebase uses bcrypt on passwords.

I think that it is safe to assume that passwords stored in Firebase are at least as secure as passwords that we would store ourselves.

bengtan commented 6 years ago

Yay! A discussion! :)


Responding to @southerneer and @aksonov:

brought up a good question...how do we implement "bypass" authentication on Staging if the password is restricted to a valid firebase auth token?

This should still work (although we might have to modify the client somewhat). The client would look at the prefix of the number, and if it is '+1555', do something different, or send something different to our server.

On the server side, the server would also look at the prefix, and then do something different (ie. don't really verify the candidate token) if it's '+1555'.

Keep in mind, however, that the '+1555' bypass mechanism is just a kludgey work-around, and it might be an opportunity to revisit and see if we can replace it with something better.


Responding to @southerneer and @aksonov and @toland:

And what if we will want to decide to switch to other auth provider (auth0 or something else)

Switching auth providers ... might be problematic.

For example, if we decide to switch from provider X to provider Y, all the auth data (ie. passwords, tokens (if stored in a backend)) is in X, and I'm not sure if there's an easy way to bulk move, or import-export, all our users to Y.

For example, we could authenticate against Facebook or Google quite easily.

Yes, we can authenticate against F or G's OAuth2 provider easily, but is it easy to move all our users from one to the other? I'm not sure if it is easy or not. I'm also not sure if it's important or not.


Responding to @toland:

That we need to use a 3rd-party authentication service seems like a foregone conclusion at this point.

Playing devil's advocate ... I'd like to test this statement.

I'm sure there are software libraries we could use to setup our own OAuth2 service if we wanted to. Agree or disagree?

toland commented 6 years ago

For example, if we decide to switch from provider X to provider Y, all the auth data (ie. passwords, tokens (if stored in a backend)) is in X, and I'm not sure if there's an easy way to bulk move, or import-export, all our users to Y.

Firebase includes bulk import and export functionality. Interestingly, this includes passwords provided that they use a known hashing algorithm. Tokens would not be import/exportable, but I don't think they need to be. Token expiration/revocation is a thing and the application will need to be ready to handle that.

Assuming the proposed use of Firebase (SMS authentication and OAuth 2 tokens), we could move to any other provider as easily as we moved from Digits to Firebase.

Yes, we can authenticate against F or G's OAuth2 provider easily, but is it easy to move all our users from one to the other?

I think you are conflating user authentication with user profile storage. Firebase supports both, but I am only proposing that we use the authentication. Google, Github, and Facebook only support authentication plus potential exposure of the user profile information that they already store.

"Moving users from [Facebook] to [Google]" thus doesn't make any sense.

Think of OAuth like this: We are saying that we trust Firebase, and if Firebase tells us that you are who you say you are, then we are good with that and don't need to verify the fact for ourselves. If we decide to also trust Google (or Github or Facebook), then that is additive. There is no requirement that we have a single source of truth for the identity of users. There are ways to connect a user authenticating via different OAuth providers to the same user profile in our database.

Think about one of the sites that you use which allows OAuth authentication. Many of the 3rd party services we use allow for us to authenticate via multiple OAuth providers. I may need to use the same one to authenticate every time to get into the same account (or not), but a site supporting Google does not preclude them also supporting Github or Facebook.

That we need to use a 3rd-party authentication service seems like a foregone conclusion at this point.

Playing devil's advocate ... I'd like to test this statement.

I'm sure there are software libraries we could use to setup our own OAuth2 service if we wanted to. Agree or disagree?

Certainly, there are. I ran across some solid-looking OAuth implementations in Elixir as I was doing the initial research on this issue. And, of course, anything that runs in a Docker container is fair game for us. But, that isn't really the issue. I haven't yet found a good implementation that does SMS authentication, and SMS authentication has been a hard requirement thus far. Additionally, the authentication mechanism doesn't necessarily differentiate our product and I think that we can more fruitfully spend our time on other things. If it comes down to a choice between implementing our own OAuth provider and using ML to provide more useful content to our users, I know where I would prefer to spend my time.

toland commented 6 years ago

I think I see how we can use the Firebase tokens with reasonably small changes to the front and back end.

On the first install, the client authenticates the user via SMS as before. Then, the client registers the user as described in the wiki page. The only difference is that the server will ignore the token field and will not return a token.

As before, the client will then attempt to login with the PLAIN mechanism. Instead of a server provided token, the client will pass the Firebase access token in the password field prefixed with $O$. The prefix is so that the server can distinguish between OAuth tokens, old tokens (which are prefixed with $T$), and plain passwords.

The client will be responsible for refreshing the access token with the refresh token as appropriate.

This reuses 90% of our existing protocol and infrastructure while simply replacing the old server-generated token with an OAuth token.

Note that this means the server will authenticate the user twice on the first login: once during the registration process and once when the user logs in. The access token is necessary for the registration process to help prevent exploitation, and the login process is still necessary to initiate the XMPP session. This is suboptimal, but not a significant problem in the grand scheme of things.

toland commented 6 years ago

If we do follow that scheme, we are still left with the issue of client identification during authentication. I can think of a few potential solutions off the top of my head:

  1. Send an encoded JSON packet in the password field for PLAIN. This could be a JWT or just a plain JSON object that has been Base64 encoded. The JSON packet would contain the user's token as well as the client identifier and version.

  2. Have the client send a version IQ stanza to the server after authentication has been accomplished. This is cleaner, but perhaps less ideal.

  3. Develop a custom SASL mechanism that takes the client identification and version. I think we decided that the client couldn't do custom mechanisms properly, and that is why we went with PLAIN for the token auth. However, if the client can do custom mechanisms, this would be the way to go.

thescurry commented 6 years ago

@bengtan @toland happy to chew on this during the all hands tomorrow if needed.

bengtan commented 6 years ago

Quoting @toland:

Firebase includes bulk import and export functionality.

Oh good. That's a good safety net to have.

I think you are conflating user authentication with user profile storage.

Actually, I was thinking about the storage of user-specific data that is needed for authentication.

Related to the next point.

"Moving users from [Facebook] to [Google]" thus doesn't make any sense.

The mental model I was using here is probably different from yours.

I was thinking of the case where we used a single OAuth provider internally for all users, and needed to switch.

Your mental model was where we used multiple providers concurrently (I think?).

Anyway, given that firebase has import/export, this is no longer as much of a concern.

I haven't yet found a good implementation that does SMS authentication, and SMS authentication has been a hard requirement thus far.

Fair enough. I was thinking 'OAuth'. You were thinking 'OAuth and also sms'.

bengtan commented 6 years ago

@toland:

I think I see how we can use the Firebase tokens with reasonably small changes to the front and back end.

I have some notes about this over at:

https://github.com/hippware/wocky/issues/487#issuecomment-309357349

but they might not be up to date anymore.

bengtan commented 6 years ago

@toland:

So, to summarize:

access tokens expire in an hour refresh tokens are revoked when the user account changes or via the revocation API

Are you able to confirm (or deny) that, if a refresh token is used to acquire a new access token, that the refresh token itself is changed? [1]

Since the access tokens expire in an hour, it's possible that a user, over the course of a day, may acquire several access tokens. If the refresh token is used multiple times during a day, but if it doesn't get rotated ... that sounds ... odd.

Or should we get @aksonov to test that? Maybe he's in a better position to test.

toland commented 6 years ago

Are you able to confirm (or deny) that, if a refresh token is used to acquire a new access token, that the refresh token itself is changed?

According to the Firebase ReST API docs, the call to exchange a refresh token for a new access token also returns "the Firebase Auth refresh token provided in the request or a new refresh token." It sounds like the refresh token might be changed, but this is not definitive.

bengtan commented 6 years ago

The momentum in this discussion is pointing towards using Firebase as a OAuth2 provider. I'm going to ponder this for a day or so more, and then I'm going to ping @thescurry about it.


I think we're about 'halfway there' (conceptually) for deciding on the necessary protocol changes.

I have some random comments:

I think I see how we can use the Firebase tokens with reasonably small changes to the front and back end.

On the first install, the client authenticates the user via SMS as before. Then, the client registers the user as described in the wiki page. The only difference is that the server will ignore the token field and will not return a token.

The server can return, in the token field, the token that the client just used (obtained from firebase). This will go a long way towards preserving backward compatiblity with existing/old clients.

It would be desirable to have (some level of) backward compatibility.


@toland:

I know you have thought about how to communicate the 'user agent string' from the client to the server.

How about 'Cryptographic verification of client'? Are you ready to start thinking on it?

toland commented 6 years ago

The server can return, in the token field, the token that the client just used (obtained from firebase). This will go a long way towards preserving backward compatiblity with existing/old clients.

It would be desirable to have (some level of) backward compatibility.

We can do that, but it is still the client's responsibility to ensure that the token is valid and refresh when necessary. Depending on timing, that token may no longer be valid by the time it is returned in the response payload. This probably won't be common, but it is certainly possible.

How about 'Cryptographic verification of client'? Are you ready to start thinking on it?

I have been thinking about that. In principle, it works the same way as the client ID. Since the client also has to authenticate with Firebase to get a token, I am less concerned about the need for the client cert, but it is relatively easy to do.

bengtan commented 6 years ago

The server can return, in the token field, the token that the client just used (obtained from firebase). This will go a long way towards preserving backward compatiblity with existing/old clients.

It would be desirable to have (some level of) backward compatibility.

We can do that, but it is still the client's responsibility to ensure that the token is valid and refresh when necessary. Depending on timing, that token may no longer be valid by the time it is returned in the response payload. This probably won't be common, but it is certainly possible.

Ack.

Note to self: Have to think more about backward compatibility later.

How about 'Cryptographic verification of client'? Are you ready to start thinking on it?

I have been thinking about that. In principle, it works the same way as the client ID. Since the client also has to authenticate with Firebase to get a token, I am less concerned about the need for the client cert, but it is relatively easy to do.

I'm a bit ambivalent about cryptographic verification. I'll let Bernard weigh in on it when he gets back.

bernardd commented 6 years ago

My 2c having just read the thread: Basically +1 for all of Phil's stuff. I don't think there's anything there I disagree with. In terms of client identification, I'm inclined to go with option 1 (sticking it into the password field as JSON) as the simplest way with the least friction.

I'm not overly concerned about cryptographic client verification - it's in my mind as "nice to have" but I think at the moment we have bigger fish to fry. Plus, if we used JSON as mentioned above we could easily cram the verification data in there later when we got around to it.

bengtan commented 6 years ago

I'll talk to @thescurry about this in my meeting with him and Becky (my) tomorrow. I imagine he'll want a few days to ponder switching to Firebase.

Depending on his response (which might not arrive immediately), the 'research' phase may be considered to be complete and we can move to software/protocol design and implementation.

bengtan commented 6 years ago

I spoke to @thescurry.

He doesn't have any objections to moving to firebase-as-an-oauth2 provider.

So, once this has been communicated to the wider team (should be soon) and there are no other objections, the conceptual/research phase is done and we can move to design and implementation.

toland commented 6 years ago

It sounds like we have reached consensus. Closing this ticket in favor of #1261 and #1262.