Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending refresh token from phone to watch #801

Merged
merged 24 commits into from
Apr 26, 2023

Conversation

mchowning
Copy link
Contributor

@mchowning mchowning commented Feb 25, 2023

Description

This PR automatically authenticates a user on a paired watch when they are logged into Pocket Casts on their phone by:

  1. Retrieving a refresh token on the phone.
  2. Sending that refresh token to the watch using the Wearable Data Layer API.
  3. When the watch receives a refresh token, the watch will use it to log in with that token.

One issue with the current implementation is that it's pretty pushy about logging you in on the watch if you're logged in on a connected phone. For example, if you're logged in on both devices, and you log out on the watch, then the next time you reopen the app you'll be logged back in on the watch. I'm not sure how I feel about this, it doesn't seem ideal, but it feels like an edge case for a user to want to be logged in on their phone but not their watch. If a user did want to keep them separate, they would just need to log into a different account on the watch. To me the current implementation feels good enough for now, but let me know if you have any thoughts.

Screen.Recording.2023-04-23.at.5.49.01.AM.mov

Testing Instructions

When doing these tests, make sure you're using the same build variant on both the watch and the phone. If you don't, the syncing won't work. You may also want to comment out this line while you are testing so the notification doesn't get automatically dismissed too quickly. Note that I do have it set up so that the notification should show for a minimum of 5 seconds to ensure the user has time to see it and realize what is happening since this isn't a user initiated action but happens automatically when a refresh token is found.

  1. Use a phone and a watch that are paired
  2. Remove Pocket Casts from both the phone and the watch
  3. Install the Pocket Casts app module from this branch onto the phone
  4. Log into Pocket Casts on the phone
  5. Install the Pocket Casts wear module from this branch onto the watch
  6. ✅ Observe that the watch app is logged in pretty much immediately with a "logging in" screen shown while the refresh is in progress. This screen will show a spinning placeholder image if a gravatar image is not available for this account or if the gravatar image is still loading. I have this screen set to show for at least 5 seconds even if the refresh is finished earlier in order to make sure that there is enough time for the user to see this screen and realize that they are being logged in automatically.
  7. ✅ After the refresh is complete, the "logging in" screen should be dismissed and the Now Playing and Up Next chips should be updated appropriately (assuming the account has a currently playing episode and it's Up Next queue is not empty)
  8. Log out of the app on the phone
  9. ✅ Observe that the watch app is still logged in
  10. Log out of the watch app
  11. Log into Pocket Casts on the phone
  12. ✅ Observe that the watch app is logged in pretty much immediately

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews

@mchowning mchowning added the [Area] Wear OS watch app label Feb 25, 2023
@mchowning
Copy link
Contributor Author

mchowning commented Feb 25, 2023

  1. Sending that refresh token to the watch by using the Wearable Data Layer API.

This is Google's recommended approach, and has the benefit that the token gets stored so that once the phone sends the token to the data layer, the phone can disconnect from both the network and the watch (i.e., go into airplane mode), and the watch will still be able to retrieve the token if it has network access.

The tradeoff here is that the token is synced to Google's servers (h/t @NoahAndrews comment on the Pocket Casts Wear OS issue: #468 (comment)).

I decided to use the Data Layer API approach for this initial POC because it's the Google-recommended approach and seems to provide a better user experience. The other option would be to send messages between the phone and watch.

@mchowning
Copy link
Contributor Author

I have this PR labeled as a work in progress because there are still issues that need to be addressed in the code, but I believe the functionality described in the test steps is all working, so feel free to go ahead and test this out and let me know any issues you see.

@mchowning mchowning mentioned this pull request Feb 25, 2023
1 task
Comment on lines 63 to 74
val listener = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> dataClient.addListener(viewModel.phoneSyncDataListener)
Lifecycle.Event.ON_STOP -> dataClient.removeListener(viewModel.phoneSyncDataListener)
else -> { /* do nothing */
}
}
}
lifecycle.addObserver(listener)
return onDispose {
lifecycle.removeObserver(listener)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a listener, we could use a service to always listen for changes even when the Pocket Casts wear app is in the background. I don't think we need to listen and respond to these events until the app comes to the foreground, so I went with the listener approach. I do think either approach could work though, so let me know if you have other thoughts.

Base automatically changed from update/wear-horologist-to-0.3.4 to add/wear February 27, 2023 09:43
@NoahAndrews
Copy link

As a part of a response to google/horologist#1036, I compared Google's recommendations for syncing credentials in phone apps to the ones for Wear OS, and found that the phone app guidelines clearly discourage syncing credentials to the cloud without end-to-end encryption. This research made me more convinced that the better path here is to keep tokens out of DataClient, unless Google documents that that data is end-to-end encrypted somehow (which seems unlikely).

@NoahAndrews
Copy link

NoahAndrews commented Mar 1, 2023

Also, IANAL, but storing anything on Google's servers without explicit consent looks to me like it would violate the current Pocket Casts privacy policy.

Will you share my information?

No.

We will only disclose your information if we have your consent to do so, we are compelled to do so by law, with our service providers as permitted by law, or we do so in connection with the sale or reorganisation of the Service.

How is my data stored?

The data collected pursuant to this Privacy Policy will be stored on our servers. We use a variety of technologies designed to encrypt and protect your information to help prevent access by unauthorised parties.

Google's servers don't seem like they would count as "[Pocket Cast's] servers".

Comment on lines 52 to 53
// FIXME nonononono this makes an api call _every_ time
accountAuth.getTokensWithEmailAndPassword(email, password)
Copy link

@NoahAndrews NoahAndrews Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention to stop storing the password in AccountManager altogether, and store the refresh token instead? It seems like that would be the most straightforward way to keep track of everything. Only if the user does not have a refresh token stored would you check if there's a password stored. The less state to keep track of, the better (plus it's more secure to just store a token in all cases, instead of sometimes storing a password).

Let me know if I'm overstepping my bounds at all with this sort of feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention to stop storing the password in AccountManager altogether, and store the refresh token instead?

Yes, I believe that will be a part of @geekygecko 's work on Google Single-On work. The api call on this line to retrieve the refresh token is just a placeholder until that work is done and we can retrieve the token locally.

@mchowning
Copy link
Contributor Author

mchowning commented Mar 2, 2023

Thanks for the great feedback @NoahAndrews

As a part of google/horologist#1036 (comment) to google/horologist#1036, I compared Google's recommendations for syncing credentials in phone apps to the ones for Wear OS, and found that the phone app guidelines clearly discourage syncing credentials to the cloud without end-to-end encryption. This research made me more convinced that the better path here is to keep tokens out of DataClient, unless Google documents that that data is end-to-end encrypted somehow (which seems unlikely).

I was a bit surprised that the response to your issue wasn't a simple "Yes, we should clarify that in the documentation, but of course our recommended approach for sharing authentication tokens is fully encrypted and secure." Based on their response, it sounds like it very well may just be a documentation issue, but we don't know that yet. 😞

IANAL, but storing anything on Google's servers without explicit consent looks to me like it would violate the current Pocket Casts privacy policy.

Thanks for bringing this up. If we decide we want to release using the Data Layer approach, we'll make sure we run this by the legal team.

Let me know if I'm overstepping my bounds at all with this sort of feedback.

Not at all. Definitely appreciate your input. Keep it coming! 😄

I'm not quite as convinced as you are that we have to use the messages approach. You are raising good points though, and I'm also not convinced that we should use the Data Layer either. My inclination is to stick with the Data Layer approach for now, and to see what more information we get from Google on the horologist issue you opened (thanks for opening that btw 🙇 ). Then we can revisit this before we release our wear app to see if we want to switch away from the Data Layer approach.

With that said, if you're interested in prototyping what this would look like using the messages approach, that would be great! Maybe we'll see that it works so well that we want to go that way regardless of Google's answer to the encryption question. If Google does confirm that the Data Layer is end-to-end encrypted though, we might end up sticking with the Data Layer (on the other hand, if Google can't confirm that it is encrypted, using messages looks like a better option).

Just for the sake of completeness, I'll note that another authentication method on my mind is custom code authentication. That feels a bit more complicated than I'd like to use for the initial release, but I think it may be something we want to consider later since that would facilitate authentication of watches paired to iOS devices and watches that are not paired to a phone.

@yschimke
Copy link

yschimke commented Mar 3, 2023

Google Wear Developer here. We hope to be able to unblock you with some clear guidance early next week. We aren't ignoring you. cc @garanj

@yschimke
Copy link

yschimke commented Mar 8, 2023

Some info on data layer security

  • All data stored in the data layer is likely to go to the cloud
  • All data that goes to the cloud is end to end encrypted
  • Outdated devices that do not support E2EE no longer support Cloud Sync at all
  • Encryption keys are generated by the devices and exchanged exclusively over local Bluetooth ASAP (generally during device setup). Google never receives keys, or even encrypted messages that contain keys.
  • There is no key rotation.
  • Messages sent with MessageClient or ChannelClient will also be automatically routed through the cloud when Bluetooth is unavailable (but won’t be if it is)
  • Messages sent via either of those are also subject to the same E2EE setup as the DataClient, and cannot be read by Google either.

@mchowning
Copy link
Contributor Author

Thanks so much for confirming the security around the Data Layer @yschimke ! In light of that, I don't think there's any reason for us to avoid using the Data Layer from a security perspective. Do you agree @NoahAndrews ?

@mchowning mchowning changed the base branch from add/wear to main April 17, 2023 15:13
@mchowning mchowning marked this pull request as ready for review April 17, 2023 15:24
@mchowning mchowning requested a review from a team as a code owner April 17, 2023 15:24
@mchowning
Copy link
Contributor Author

I've updated this PR (with help from @luizgrp 🙇 ), and marked it ready for review.

@CLAassistant
Copy link

CLAassistant commented Apr 22, 2023

CLA assistant check
All committers have signed the CLA.

@mchowning
Copy link
Contributor Author

Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.

👋 @luizgrp ! I included your commit in this PR. Can you sign our CLA?

@luizgrp
Copy link
Contributor

luizgrp commented Apr 22, 2023

@mchowning done, apologies for the delay!

Copy link
Contributor

@ashiagr ashiagr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have gone through the changes, and all of them seem good to me. Auto-login works as described in the testing instructions. 👍

Nice job on showing the gravatar while logging in!

@ashiagr ashiagr merged commit 32171b7 into main Apr 26, 2023
@ashiagr ashiagr deleted the update/authenticate-watch-from-phone branch April 26, 2023 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants