google / GoogleSignIn-iOS

Enables iOS and macOS apps to sign in with Google.

Home Page:https://developers.google.com/identity/sign-in/ios

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

If user doesn't accept additional additional (not necessary scopes), return access_denied

mikeyyyzhao opened this issue · comments

Describe the bug
If a user does not accept the additional scopes. E.g. Calendar access but not contacts, the login module returns access_denied instead of the auth token.
Screenshot 2024-02-27 at 4 15 57 PM

However, for new users accepting scope (first time logging into an app), if the user doesn't accept the additional scopes, auth continues fine and returns the token. This bug only happens on subsequent tries when the user is prompted with additional scopes and do not accept them.

The exact same flow is working fine on web on first and nth try.

To Reproduce
Steps to reproduce the behavior:

  1. Login with existing account (already accept the mandatory scopes)
  2. Do not accept the additional scopes
  3. get access_denied error

Expected behavior
A clear and concise description of what you expected to happen.
For additional scopes, it should still return the user object with serverAuthCode, similar to how it works on web.

Environment

  • Device: iPhone
  • OS: iOS 17

Hi, thanks for the issue! Unfortunately I'm unable to test/repro this so far. Is the screenshot you shared above from the screen that looks like this?:
image

Are you able to reproduce the issue in the sample apps (in this folder)?

If not, would you be able to answer the following:

  • Which specific GIDSignIn methods are you calling, and which methods work as expected and which result in the bug?
  • Are there more details about the error (is there additional error info beyond access_denied)?
  • Do you get the bug if you accept a subset of the additional scopes? What about if you accept all of the additional scopes?

I've added our code for auth for reference (you just need to plug in your clientID and serverClientID. It's based off the birthday example so it should be fairly straight forward to plug in.

Steps to reproduce:

  1. Login with an google account that is new to the app
  2. Accept every permission minus the two contacts permissions (contacts.readonly and contacts.other.readonly")
  3. Log out (this is to simulate logging in again - we have both a web app and an iOS app so following these steps makes it so you don't have to use a web app)
  4. just for extra safety -> quit the app (swipe up) so we remove local caches
  5. login again -> still do not accept the two contact permissions
  6. the response from the library is access_denied

Note: if you remove access to the app through accounts.google.com -> third party access -> remove access to the app, you can repeat the steps above using the same email so this way, you don't have to recreate emails to test.

Are there more details about the error (is there additional error info beyond access_denied)?

  • Not getting much else other than access_denied unfortunately. I saw another ticket about giving back more detailed errors but I don't think that's been implemented in the library yet.

Do you get the bug if you accept a subset of the additional scopes? What about if you accept all of the additional scopes?

  • if you accept all the scopes it works fine. However, if you follow the same steps above on web (without accepting all permissions), you still get the auth code back.
private let additionalScopes = [
        "email",
        "profile",
        "https://www.googleapis.com/auth/calendar",
        "https://www.googleapis.com/auth/contacts.readonly",
         "https://www.googleapis.com/auth/contacts.other.readonly",
         "https://www.googleapis.com/auth/admin.directory.resource.calendar.readonly",
         "https://www.googleapis.com/auth/admin.directory.user.readonly",
     ]

    private lazy var configuration: GIDConfiguration = {
        return GIDConfiguration(clientID: clientID, serverClientID: serverClientId)
    }()
    
    private let authViewModel: AuthenticationViewModel
    
    /// Creates an instance of this authenticator.
    init(authViewModel: AuthenticationViewModel) {
        self.authViewModel = authViewModel
    }
    
     func signIn() {
         guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let window = windowScene.windows.first,
               let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
             print("There is no root view controller!")
             return
         }
         GIDSignIn.sharedInstance.configuration = configuration
         GIDSignIn.sharedInstance.signIn(
             withPresenting: rootViewController,
             hint: nil,
             additionalScopes: additionalScopes
    ) { user, error in
        print("serverAuthCode: \(user?.serverAuthCode)")
        ....

Thanks for all the detail! I was able to repro the issue in the DaysUntilBirthday sample app. I'll investigate further, but my initial thought is that somewhere within the auth flow, not giving any additional access to scopes (i.e. in step 5, where the UI presents this dialog:
image)
gets interpreted as an access denied error, despite the user already having granted access to scopes previously.

Thank you @ywsang! Let me know if there's anything else that I can provide that could be helpful

Hi @mikeyyyzhao! I did some research and have learned that it's the expected behavior for the Google OAuth server to return an access_denied error if no new consent is granted (which is the error being propagated back to you via the GoogleSignIn-iOS library).

Is there a specific reason why you need to ask for non-consented scopes on the user signing-in again? It seems that the recommendation would be to separate sign-in from the request for additional scopes, so that if the user signs out, signing in again does not ask for additional access (and the result would be a successful re-sign in). There's some additional guidance in the documentation here: https://developers.google.com/identity/protocols/oauth2/policies#unbundled-consent.

I'm still working to understand what the expectation here would be for the GoogleSignIn-iOS library. Could you let me know more about the specific integration that you're using for Google Sign In on the web, i.e. which SDK you're using, version info, etc?

@ywsang since the user is logged out, there's no way for us to know what the user has accepted? Therefore, we always show the default set of permissions that we need. GoogleSignIn-iOS has tried to separate the two steps in the past but reverted because it provides a terrible user experience.

Also, following the same steps above on web produces a valid auth code. We're not using a sdk for web but just a redirect URL

const googleLoginURL =
    `https://accounts.google.com/o/oauth2/auth?` +
    `client_id=${GOOGLE_CLIENT_ID}` +
    `&redirect_uri=${REDIRECT_URL}` +
    `&scope=${GOOGLE_LOGIN_SCOPES}` + // same scopes as iOS
    `&prompt=consent select_account` +
    "&access_type=offline" +
    "&include_granted_scopes=true" +
    `&response_type=code`

If the user accepts the same scopes as before, it should keep granting the auth code. I don't fully understand how/why not granting it would be the expected behavior.

@ywsang just following up on this as this bug is critical for apps that have are on both mobile and web