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

Subscription oauth v2 #1033

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

federicocappelli
Copy link
Member

@federicocappelli federicocappelli commented Oct 25, 2024

Task/Issue URL: https://app.asana.com/0/1205842942115003/1207991044706235/f
iOS PR: duckduckgo/iOS#3480
macOS PR: duckduckgo/macos-browser#3580
What kind of version bump will this require?: Major
CC: @miasma13

iOS PR: duckduckgo/iOS#3480
macOS PR: duckduckgo/macos-browser#3580

Description:

This PR introduces the use of OAuth V2 authentication in Privacy Pro Subscription.
The code changes are comprehensive due to the paradigm changes between the old access token lifecycle and the new JWT lifecycle.
The Subscription UI and UX should be unchanged.

Steps to test this PR:
Test all Privacy Pro Subscription features and UX, more details here


Internal references:

Software Engineering Expectations
Technical Design Template

@federicocappelli federicocappelli marked this pull request as ready for review December 12, 2024 17:42
@THISISDINOSAUR
Copy link
Contributor

There's basically no PIR stuff in BSK at the moment

Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

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

Adding some comments. Still going through the code.

Sources/Common/KeychainType.swift Outdated Show resolved Hide resolved
Sources/Common/UserDefaultsCache.swift Show resolved Hide resolved
Sources/NetworkProtection/PacketTunnelProvider.swift Outdated Show resolved Hide resolved
Comment on lines -680 to +663
try load(options: startupOptions)

if (try? tokenStore.fetchToken()) == nil {
throw TunnelError.startingTunnelWithoutAuthToken
}
try await load(options: startupOptions)
Logger.networkProtection.log("Startup options loaded correctly")
Copy link
Contributor

Choose a reason for hiding this comment

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

The error handling needs to be improved here. Previously we could only have startingTunnelWithoutAuthToken, but now we can get a number of different possible errors from tokenProvider.adopt and tokenProvider.getTokenContainer.

These errors should ideally implement CustomNSError and include underlying error information. See PacketTunnelProvider.TunnelError for reference.

The reason this is important is because it's what'll allow us to debug tunnel start issues coming from these changes, and understand what's going on.

Copy link
Member Author

Choose a reason for hiding this comment

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

Understood, atm getTokenContainer can fail for a variety of reasons, from storage to API, decoding and decryption errors, we don't have a single set Error so implementing this is quite difficult, should I wrap any possible error into a higher level TunnelError?

Copy link
Contributor

@diegoreymendez diegoreymendez Jan 14, 2025

Choose a reason for hiding this comment

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

Yeah I'd do that.

We could wrap this in a (possibly new) TunnelError that contains an underlying error (by implementing CustomNSError) so we know where this is coming from at a high level, and what's the underlying problem.

That said, I'd encourage you to also consider implementing CustomNSError in the subscription errors we can get here too as that'll give us detailed error information in our pixels if we do. I know this can sound a bit annoying but the more detail we can get, the easier it will be to understand what's failing if something does fail, as we'll get detailed info in the pixels.

Copy link

github-actions bot commented Jan 1, 2025

This PR has been inactive for more than 7 days and will be automatically closed 7 days from now.

@diegoreymendez
Copy link
Contributor

Note: I've edited the description to add links to the related PRs for easy of navigation.

@diegoreymendez diegoreymendez self-requested a review January 14, 2025 15:55
Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

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

Added some additional comments, still going through all this.

Copy link
Contributor

@THISISDINOSAUR THISISDINOSAUR left a comment

Choose a reason for hiding this comment

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

There's almost nothing PIR related in BSK so I have no comments here

Copy link
Contributor

@miasma13 miasma13 left a comment

Choose a reason for hiding this comment

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

First batch of comments

Tests/NetworkingTests/OAuth/.swift Outdated Show resolved Hide resolved
Comment on lines +29 to +41
assertionFailure("Failed to retrieve auth token: \(error)")
}
return nil
}
set(newValue) {
do {
guard let newValue else {
try removeAccessToken()
return
}
try store(accessToken: newValue)
} catch {
assertionFailure("Failed set token: \(error)")
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we also need error handling for the legacy token store. Having pixel for failures to read/write at point where we for example attempt to migrate the token would be an important signal.

Comment on lines 116 to 114
do {
let transactionJWS = try await recoverSubscriptionFromDeadToken()
return .success(transactionJWS)
} catch {
return .failure(.purchaseFailed(OAuthClientError.deadToken))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This handling seems to be incorrect:

  • in L:117 you attempt to recoverSubscriptionFromDeadToken()
  • inside this function you call appStoreRestoreFlow.restoreAccountFromPastPurchase()
  • this does not makes sense as this call already failed in L:107 (scenario for no subscription to be recovered via past App Store purchases)

Copy link
Member Author

@federicocappelli federicocappelli Jan 21, 2025

Choose a reason for hiding this comment

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

restoreAccountFromPastPurchase fails because the token is dead, the first action of recoverSubscriptionFromDeadToken is to sign out the user (aka deleting everything, including the dead token) and re-run restoreAccountFromPastPurchase. The assumption is that the user had a token (dead) so a subscription was present, so the same subscription is recoverable with a new token.


do {
let subscription = try await subscriptionManager.confirmPurchase(signature: transactionJWS)
if subscription.isActive {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure about this check. If the subscription after completing the purchase ends up in wrong state, it is a BE issue and there is little we can do about it, cannot recover from it nor we don't special handle it.

I would only keep the refetch and check if entitlements were granted to the account.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure how to handle this but:

  • The request has a 3 max retries with a 4s delay between retries APIRequestV2.RetryPolicy(maxRetries: 3, delay: 4.0) but the call itself doesn't fail if the subscription is inactive
  • If the subscription is inactive we log it and return a failure to the script page with the correct error, so I would like to detect it still. I have never seen a failure here, I'm just trying to fail gracefully in case of BE errors.
  • I moved the token refresh out of the if so it is done no matter what.

subscription.platform != .apple {
return externalID
@discardableResult
private func recoverSubscriptionFromDeadToken() async throws -> String {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this is being called 3x from different scenarios but none of them seems for me to be likely to encounter dead token as the account was either just restored or created. In my opinion that while dead token is possible option during the purchase flow it should be treated as other "default" errors but is not recoverable here. We should just fail at given step.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, this is confusing, so I did the following:

  • removed the recovery function call from every AppStorePurchaseFlow call and returned the dead token error normally
  • made recoverSubscriptionFromDeadToken() public and generic, so can be used from averyone receiving a dead token error, I'll update the clients accordingly

Copy link
Contributor

@miasma13 miasma13 left a comment

Choose a reason for hiding this comment

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

Second part of the review

Sources/Networking/OAuth/OAuthClient.swift Outdated Show resolved Hide resolved
Sources/Networking/OAuth/SessionDelegate.swift Outdated Show resolved Hide resolved
Sources/Subscription/Managers/SubscriptionManager.swift Outdated Show resolved Hide resolved
Sources/Subscription/Managers/SubscriptionManager.swift Outdated Show resolved Hide resolved
Sources/Subscription/API/SubscriptionEndpointService.swift Outdated Show resolved Hide resolved
@samsymons samsymons removed their request for review January 23, 2025 23:43
@federicocappelli federicocappelli force-pushed the fcappelli/subscription_oauth_api_v2 branch from 1349661 to 402a26b Compare January 30, 2025 13:53
@federicocappelli federicocappelli force-pushed the fcappelli/subscription_oauth_api_v2 branch from 402a26b to cad6354 Compare January 30, 2025 14:08
Copy link

github-actions bot commented Feb 7, 2025

This PR has been inactive for more than 7 days and will be automatically closed 7 days from now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants