How You Actually Get a Google OAuth Refresh Token for Chrome Web Store Automation
A precise walkthrough of the Google OAuth path to a refresh token: why you must start with a client_id, why Google gives you an authorization code first, why localhost redirects are common, and how that turns into Chrome Web Store automation.
How You Actually Get a Google OAuth Refresh Token for Chrome Web Store Automation
The confusing part of Chrome Web Store automation is usually not the upload API.
It is the moment you ask a very reasonable question:
Why can I not just ask Google for a refresh_token directly?
Why do I have to visit a URL with a client_id, log in, get redirected to localhost, and manually extract an authorization code first?
That is the part worth understanding, because it explains how Google designed the security model.
This post focuses on that path:
- why the
client_idcomes first - why Google gives you an authorization code before a refresh token
- why
localhostredirect URIs are common for local bootstrap scripts - why the refresh token must be minted through user consent
- how that one-time bootstrap turns into unattended Chrome Web Store publishing later
If you want to explore the supporting visual map on the site, the API flow is in /api-design.
One important update up front: the current Chrome Web Store guide now explicitly recommends OAuth Playground as the simplest way to obtain the initial refresh token, and then recommends using that refresh token outside the Playground to mint access tokens and call the API. localhost callback remains a good engineering pattern when you want to own the bootstrap flow yourself.
Start With the Core Idea
Google is protecting a privileged action.
Publishing a Chrome Extension is not a public API read. It is a write action on behalf of a real publisher account.
So Google needs to know two different identities at once:
- which application is requesting access
- which Google user approved that access
That split is the whole reason OAuth exists.
An API key cannot do this well, because an API key identifies only the app.
A password is worse, because it collapses all user power into one secret that you should never put into CI.
OAuth gives Google a controlled delegation model:
- the app identifies itself with
client_id - the user authenticates in Google
- the user consents to a specific scope
- Google issues tokens only after both conditions are satisfied
Why the client_id Comes First
The client_id is not there for convenience. It is there because Google needs to know which app is asking before the user even sees the consent screen.
That lets Google answer questions like:
- Which redirect URI is valid for this app?
- What consent screen should the user see?
- What app name is requesting access?
- Is this app allowed to request this scope?
- Should Google trust this request shape at all?
So the actual order is:
- the app identifies itself with
client_id - Google knows which app policy applies
- the user logs in
- the user consents
- Google issues a short-lived authorization code tied to that app and that consent event
Without the client_id, Google does not yet know what application the user is authorizing.
Why You Do Not Get a Refresh Token Directly
Because a refresh token is not just "a token."
It is a long-lived proof that Google approved offline delegated access for a particular app-user relationship.
If Google let clients directly request refresh tokens without the consent flow, any script that knew a client_id could try to mint long-lived publishing credentials. That would defeat the point of delegated authorization.
Google therefore inserts an intermediate artifact:
- the authorization code
That code means:
- the app identified itself
- the user authenticated
- the user granted consent
- Google is willing to let the backend redeem this event for tokens
Only after that does Google consider issuing a refresh token.
Why Google Gives You an Authorization Code First
The authorization code exists because the browser is a bad place to hand out long-lived machine credentials.
The browser is front-channel. It is exposed to:
- browser history
- extensions
- front-end JavaScript
- accidental logging
- redirect interception mistakes
So Google uses a two-step model:
- browser gets a short-lived authorization code
- backend or bootstrap script exchanges that code over HTTPS for tokens
That keeps the more sensitive token exchange in the back channel, where Google can also require:
client_secret- exact
redirect_urimatch - one-time code redemption
- short validity window
Why access_type=offline Is the Prerequisite for a Refresh Token
This is the most important query parameter in the whole bootstrap flow.
An access token is for the current session. A refresh token is for future sessions when the user is no longer present.
Google therefore treats offline access as a separate level of delegation. If the authorization request does not ask for offline access, Google can satisfy the user’s immediate request with an access token and stop there.
That is why the official Google OAuth docs are explicit: if you want to refresh access later without re-prompting the user, you must request access_type=offline. Source: OAuth web-server applications.
So the correct mental model is:
access_tokenanswers: can this app call the API now?refresh_tokenanswers: can this app come back later, without the user present?
If you do not ask for offline access, Google has no reason to mint the second kind of credential.
Why prompt=consent Is Often Necessary
Google is conservative about issuing refresh tokens.
That is intentional. A refresh token is a long-lived delegated credential, so Google does not treat it like a disposable session artifact that should be reissued casually on every login.
That is why engineers often add:
prompt=consent
This forces Google to show the consent screen again and creates a fresh consent event that can result in a new refresh token.
Without it, you may complete login successfully and still not get a new refresh token back, because Google may decide the existing grant is already sufficient.
So prompt=consent is not always logically required by OAuth, but in practice it is often required when you are trying to bootstrap or recover a refresh token deliberately.
Why OOB Is No Longer Recommended
The old OOB flow used redirect URIs like:
urn:ietf:wg:oauth:2.0:ooburn:ietf:wg:oauth:2.0:oob:autooob
Google deprecated and blocked that flow because it created phishing and app-impersonation risk. The official migration guide says OOB was a legacy copy/paste pattern for clients without a redirect URI, and that it must be replaced with supported alternatives. Source: OOB migration guide.
The security problem is straightforward:
- the user gets a code in a generic copy/paste flow
- the app identity and redirect destination are less tightly bound
- phishing and fake-app interception become easier
So if you still see tutorials recommending urn:ietf:wg:oauth:2.0:oob, treat them as outdated.
Why Official Docs Now Prefer OAuth Playground or localhost Callback
There are really two different “recommended” paths, depending on your goal.
1. Official easiest path for Chrome Web Store API: OAuth Playground
The current Chrome Web Store guide tells you to:
- create a Web application OAuth client
- add
https://developers.google.com/oauthplaygroundas an authorized redirect URI - open OAuth Playground
- choose “Use your own OAuth credentials”
- enter your own
client_idandclient_secret - authorize the
chromewebstorescope - exchange the authorization code for tokens there
That is the path Google documents directly in the Chrome Web Store API guide. Source: Use the Chrome Web Store API.
Why this path is recommended:
- no custom callback server to build
- easy to verify credentials and scopes
- easiest way to bootstrap a refresh token manually
- good for testing and one-time setup
2. Recommended engineering path when you own the bootstrap: localhost callback
If you do not want to depend on OAuth Playground operationally, the more robust engineering path is:
- register a localhost redirect URI
- run a tiny local callback server
- let Google redirect to that callback
- exchange the code yourself
Why this path is attractive:
- you own the full bootstrap logic
- no copy/paste step
- easier to automate internal setup tooling
- closer to how a real production OAuth client behaves
So the distinction is:
- OAuth Playground is the official easiest/manual bootstrap path for this API
- localhost callback is the cleaner engineering path if you want to implement the bootstrap flow yourself
Why localhost Redirect URIs Are Common
When you are doing a one-time local bootstrap, localhost is usually the cleanest redirect target.
Why?
- You are not deploying a public callback service just to get one refresh token.
- The user and the bootstrap script are on the same machine.
- A local HTTP listener can receive the authorization code directly.
- It keeps the code exchange in a local tool you control instead of pasting codes into random places.
That is why many OAuth bootstrap scripts do this:
- open Google’s auth URL in the browser
- Google redirects to
http://localhost:<port>/callback?code=... - your local script receives the request
- your script extracts
code - your script calls Google’s token endpoint
- your script stores the refresh token securely
That said, localhost is a convenience choice for local bootstrap, not a requirement of OAuth itself.
You could also use:
- a private internal callback service
- a custom URI scheme for native apps
- a manual copy-paste flow in some tooling
But for a local engineering workflow, localhost is the least operationally expensive option.
Why People Sometimes Manually Parse the Auth Code
Because the simplest version of the bootstrap flow is often:
- open the consent URL
- let Google redirect to
localhost - copy the
codefrom the browser or server log - paste it into a script
- exchange it for tokens
It is not elegant, but it is explicit and easy to debug.
For a one-time setup flow, that can be perfectly acceptable.
The cleaner version is to run a tiny local callback server and parse the code automatically. That removes copy-paste errors and keeps the code path reproducible.
The Full Path to a Refresh Token
This is the actual sequence.
1. Register the OAuth client
In Google Cloud you define:
- OAuth client
- redirect URI
- consent screen
- requested scope for Chrome Web Store
This creates the app identity.
2. Send the browser to Google’s auth URL
The URL includes:
client_idredirect_uriresponse_type=codescopeaccess_type=offline- often
prompt=consent
This tells Google:
- which app is asking
- where to send the user back
- which scope the app wants
- that the app wants offline access, which is the prerequisite for a refresh token
3. User logs in and consents
Now Google has both halves of the trust model:
- app identity
- user approval
4. Google redirects with an authorization code
Google does not hand the browser a refresh token yet.
It hands back a code tied to:
- this app
- this redirect URI
- this user session
- this consent event
5. Local script exchanges the code
Your script sends the code to Google’s token endpoint together with:
client_idclient_secretredirect_urigrant_type=authorization_code
Now Google can verify that the same registered app that initiated the flow is redeeming the code correctly.
6. Google returns tokens
If everything lines up, Google returns:
access_token- maybe
refresh_token
The refresh token is issued here, not before, because only now has Google validated the entire consent and client-binding chain.
Why access_type=offline Matters
The refresh token is specifically about offline access.
Without an explicit request for offline access, Google may give you only an access token, which is useless for unattended CI because it expires quickly.
So the refresh-token question is not just:
- did the user consent?
It is also:
- did the app ask for offline access?
That is why many broken setups "work" in the sense that login succeeds, but still never return a refresh token.
API Flow Map
The key path is not "upload extension." The key path is "turn a human consent event into a durable machine credential without leaking trust across boundaries."
On the API Design page, the default flow now starts with OAuth Playground, because that is the path currently documented by the Chrome Web Store guide. The localhost callback flow is kept as the engineering alternative.
Why CI Should Not Get the Refresh Token Itself
Because CI is the wrong place for the interactive part of the trust model.
If CI tried to obtain the refresh token directly, you would need to somehow inject:
- a live browser consent session
- a real user login
- a redirect callback target
- a safe place to complete the code exchange
That is a terrible fit for a headless build runner.
The clean design is:
- obtain refresh token once outside CI
- store it securely
- let CI exchange it for short-lived access tokens forever after
That keeps the interactive trust step and the unattended execution step separated.
Operational Advice
- If you want the shortest path that matches the official Chrome Web Store guide, use OAuth Playground first.
- If you want a cleaner engineering-owned bootstrap flow, use a localhost callback server.
- Do not use OOB redirect URIs.
- Request offline access explicitly.
- Expect refresh-token issuance behavior to be conservative, not generous.
- Store only the refresh token long-term.
- Mint access tokens on demand inside CI.
The Main Takeaway
The reason you cannot "just get a refresh token" is that a refresh token is the end product of Google’s delegated trust model.
Google wants a chain that looks like this:
- known app
- known redirect target
- real user login
- explicit consent
- short-lived authorization code
- server-side token exchange
- long-lived refresh token
Once you see that chain clearly, the odd-looking pieces start to make sense:
client_idcomes first because the app identity has to be known before consent- the authorization code exists because the browser is not trusted with long-lived credentials
- OAuth Playground is the current easiest official bootstrap path for Chrome Web Store
localhostis the cleaner engineering alternative when you own the bootstrap flow- the refresh token is issued last because it is the durable machine credential created only after the full consent chain is proven
That is the design.