Is it possible to access `grantedScopes` outside of the `getAuthToken` callback, eg. chrome.identity.grantedScopes?

183 views
Skip to first unread message

lettuceleaf

unread,
Apr 17, 2024, 5:59:26 PMApr 17
to Chromium Extensions

So in light of the roll out of a granular OAuth consent screen in June, I decided to try enableGranularPermissions and ran into a problem. I need grantedScopes so that future calls to getAuthToken with interactive: false would work:

chrome.identity.getAuthToken({ interactive: false, scopes: grantedScopes })

I can save them in chrome.storage.sync, but, if for whatever reason it’s lost (extension was reinstalled, removed on accident, server error), I’d be stuck because without the correct scopes, getAuthToken above returns undefined, and the next time it's called with interactive: true, there will be no checkboxes to pick whatever necessary, because it will be counted as a second call.

It seem to be possible on iOS:

let grantedScopes = user.grantedScopes 
if grantedScopes == nil || !grantedScopes!.contains(driveScope) { 
  // Request additional Drive scope. 
}
Should I open a feature request if it hasn't been yet implemented in chrome extension api?

Patrick Kettner

unread,
Apr 19, 2024, 9:17:13 AMApr 19
to lettuceleaf, Chromium Extensions
Hello!

You can only get the scopes that are granted in getAuthToken. If you want to get them, you can just call it again. If you are trying to avoid showing the login prompt again, you can call it without setting `interactive: true`. 

const {grantedScopes} = await chrome.identity.getAuthToken({enableGranularPermissions: true, interactive: false})

best
patrick



--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/bb6e07f0-b366-4c40-bf3d-d683a1be0ba9n%40chromium.org.

Patrick Kettner

unread,
Apr 19, 2024, 10:07:57 AMApr 19
to lettuceleaf, Chromium Extensions
Hello again!
That will only return the value if the scopes have been granted to the token that is returned in the first argument. If no scopes are granted, or the user hasnt logged in, then there isn't anything to retrieve.

On Fri, Apr 19, 2024 at 9:59 AM lettuceleaf <datta...@gmail.com> wrote:
Thanks so much for the answer. I tried that, but unfortunately it throws an error: "OAuth2 not granted or revoked." the only thing that helps is specifying a `scopes` array

lettuceleaf

unread,
Apr 19, 2024, 10:08:09 AMApr 19
to Chromium Extensions, Patrick Kettner, Chromium Extensions, lettuceleaf
Thanks so much for the answer. I tried that, but unfortunately it throws an error: "OAuth2 not granted or revoked." the only thing that helps is specifying a `scopes` array

On Friday, April 19, 2024 at 4:17:13 PM UTC+3 Patrick Kettner wrote:

lettuceleaf

unread,
Apr 19, 2024, 10:44:55 AMApr 19
to Chromium Extensions, lettuceleaf, Patrick Kettner, Chromium Extensions
Let me explain what I’m doing and trying to achieve, so we’re on the same page, because I can’t quite understand the answer. Initially interactive is set to true like this: chrome.identity.getAuthToken({ interactive: true, enableGranularPermissions: true }). In a popup I pick whatever permissions I want, but not all of them, because we’re testing enableGranularPermissions. Then somewhere in the app it’s trying to fetch a fresh access token (I don’t store them anywhere and fully rely on getAuthToken, because I think it could be a security problem) the following way: const { token } = await chrome.identity.getAuthToken({ enableGranularPermissions: true, interactive: false }), but it throws an error. It works well only if all permissions were granted or scopes array is provided (with permissions that were previously chosen during the authentication).

Patrick Kettner

unread,
Apr 19, 2024, 10:51:47 AMApr 19
to lettuceleaf, Chromium Extensions
>  It works well only if all permissions were granted or scopes array is provided (with permissions that were previously chosen during the authentication).

That is expected. If a user has rejected the permissions, then there is no token to receive and no scopes to give. So you get an error. You can put a try/catch around the second getAuthtoken call (or use .catch()) to handle that situation

Patrick

lettuceleaf

unread,
Apr 19, 2024, 11:10:02 AMApr 19
to Chromium Extensions, Patrick Kettner, Chromium Extensions, lettuceleaf
got it, thanks, so that means that I'm supposed to store granted scopes somewhere, for example, in chrome.storage.sync, and can't access them like devs on iOS, user.grantedScopes or set include_granted_scopes to true like in PHP and others $client->setIncludeGrantedScopes(true) in getAuthToken, right? As I'm not trying to make the user grant more permissions, just to get the token for already agreed upon.

lettuceleaf

unread,
Apr 20, 2024, 7:51:25 AMApr 20
to Chromium Extensions, lettuceleaf, Patrick Kettner, Chromium Extensions
I guess I can modify my fetchToken function to request a token for some specific scope depending on the url, I just thought that it would be as easy as calling getAuthToken with a flag, something like chrome.identity.getAuthToken({ interactive: false, includeGrantedScopes: true }) and it would automatically include scopes that a user granted.

Patrick Kettner

unread,
Apr 20, 2024, 10:48:33 PMApr 20
to lettuceleaf, Chromium Extensions
I am not sure I follow your intention. 

> I'm not trying to make the user grant more permissions, just to get the token for already agreed upon

If the user has already agreed to a set of scopes, then you would get a response in getAuthToken (both the token, and grantedScopes). If you are not getting that, it would mean they haven't logged in/granted anything.

> I just thought that it would be as easy as calling getAuthToken with a flag, something like chrome.identity.getAuthToken({ interactive: false, includeGrantedScopes: true }) and it would automatically include scopes that a user granted.

I am not sure I understand. What are you expecting that to do? Because calling chrome.identity.getAuthToken({ interactive: false}) should give you the scopes that have already been granted. 

patrick

lettuceleaf

unread,
Apr 21, 2024, 4:11:34 AMApr 21
to Chromium Extensions, Patrick Kettner, Chromium Extensions, lettuceleaf
> Because calling chrome.identity.getAuthToken({ interactive: false}) should give you the scopes that have already been granted.

As I previously explained, it throws an error "It works only if all permissions were granted or scopes array is provided (with permissions that were previously chosen during the authentication)." or if it’s a callback, then scopes is undefined and lastError is not empty. But I don’t know how to explain it differently, so let’s wait if other users ask similar questions. I start to think that I somehow misuse the Identity API given that my messages don’t make much sense to you.

Jackie Han

unread,
Apr 22, 2024, 8:15:16 AMApr 22
to lettuceleaf, Chromium Extensions, Patrick Kettner
You don't need to save granted scopes. I recommend:
1. always check a token before using it (interactive: false)
2. always specify the required scopes (and only the scopes for the current task).
3. always ask for minimum scopes authorization for the current task. (interactive: true)

For example, you have two independent tasks. Task-1 needs scope-A and scope-B, Task-2 needs scope-C.

For task-1:
try {
   let {token} = chrome.identity.getAuthToken({interactive: false, scopes: [scope-A, scope-B]});
   // do your task with token
   .......
} catch(e) {
   // not granted scope-A and/or scope-B
   // show fallback UI to notify user or ignore this task
}

For task-2: do with the same logic, just replace scopes with scope-C.


lettuceleaf

unread,
Apr 22, 2024, 8:51:21 AMApr 22
to Chromium Extensions, Jackie Han, Chromium Extensions, Patrick Kettner, lettuceleaf

Thank you for an extensive reply, very helpful, so it confirms that I’m moving in the right direction. Previously all I was saying was that if someone needs a grantedScopes array laying around to maybe show a warning in the UI, or disable a button, or what have you, depending on what scopes were chosen, it might be convenient, if there was an easy access to it. For example, I have a logout functionality and I think I need a token for all scopes (or maybe it’s possible to call revoke for each scope (?)), this is how it looks:

const token = await fetchToken()
if (token) {
url.searchParams.set('token', token)
await fetch(url.toString())
await chrome.identity.clearAllCachedAuthTokens()
}

So I wrote a function to get all available scopes:

const checkScope = async (scope: string): Promise<boolean> => {
try {
const token = await fetchToken([scope])
return Boolean(token)
} catch (error: unknown) {
return false
}
}
const fetchScopes = async (): Promise<string[]> => {
const statuses = await Promise.all(SCOPES_ARRAY.map((scope) => checkScope(scope)))
return SCOPES_ARRAY.filter((scope, i) => statuses[i])
}

Similarly the logic could be simpler if there was a flag to send already granted scopes automatically, but as you wrote, I can pass a specific scope(s) as an argument, it’s not that hard, so this feature is not as important as identity.grantedScopes. In my case I don't need to ask for more permissions interactively, because without the second scope it's going to work just fine, the only difference is in an input field there will be no autocomplete, so emails have to be typed fully manually. Previously I had a customFetch function that would call dubious fetchToken to add it to the headers, for now it determines which individual scope to pick based on the url:

const calcScopes = (url: URL): Scopes => {
try {
switch(normHostname(url.hostname)) {
case 'googleapis.com': return SCOPES.drive
case 'people.googleapis.com': return SCOPES.people
default: return SCOPES.drive
}
} catch (error: unknown) {
log({ error, isConsoleOnly: true })
}
}

type Fetch = typeof fetch
export const customFetch: Fetch = async (input, init) => {
const scopes = calcScopes(convertInputToUrl(input))
}
Reply all
Reply to author
Forward
0 new messages