Bootcamp
OAuth2 explained step by step
Photo by Alessandro Matonti on Unsplash
You’ve pressed “Sign in with Google” a hundred times without thinking about what happens underneath. Something quietly remarkable occurs: an app gets working access to parts of your account without ever seeing your password. That something is OAuth2, and it has a reputation for being confusing that it doesn’t entirely deserve.
The confusion melts once you see it as a sequence of exchanges, each one simple on its own. This article walks the whole flow in the order it actually happens, using a real integration between Bubble and Xero, the accounting platform, and starts by placing OAuth2 against the simpler methods it improves on, so you can see the full authentication spectrum.
OAuth2 is an authorisation flow that lets a user grant your app access to their account on another service without handing over their password. Instead of a permanent credential, your app receives a short-lived token limited to an agreed scope of permissions.
The auth spectrum: from passwords to tokens
To appreciate what OAuth2 does, look at what sits below it.
Basic Auth is the simplest method: a username and password supplied with each API call, encoded into the request header. In Bubble’s API Connector it’s a built-in option with two fields; enter the credentials and you’re connected. You still see it occasionally, though it’s less common than key-based methods.
Base64 encoding often travels with it, and it can sneak up on you. Cliniko, an allied health practice management platform used in Australia and a few other countries, is a real example: its docs say to authenticate with Basic plus the Base64-encoded value of your API key followed by a colon. The docs express that in code, something like "Basic " + base64(api_key + ":"), and the trap is treating the syntax as literal text. The quote marks and plus signs are just concatenation in the example code; what you actually encode is your key with a colon appended, nothing else. If you’re not working in a code language, search for a Base64 encode tool, paste in the key-plus-colon, and use the encoded output after the word Basic in your authorization header. (You can verify by checking that the last few characters of your result match the documentation’s worked example.) Other APIs vary the recipe, concatenating an ID with the token or putting the colon in the middle, but the process is identical. One thing worth knowing: encoding is formatting, not secrecy; it’s part of the protocol, not what keeps the credential safe.
Both of these are static: one credential, full access, forever, until someone revokes it. OAuth2 replaces that with something dynamic.
What OAuth2 actually does
Strip away the jargon and OAuth2 is three exchanges, common to more or less every implementation:
- Authorisation. Your app sends the user to the service’s own site, where they log in and approve access. The service redirects them back to your app carrying a one-time authorisation code.
- The code-for-token exchange. Your app sends that code (plus proof of its own identity) to the service’s token endpoint and receives an access token.
- Access. Your app uses the token to call the API, again and again, until the token expires.
Tokens are deliberately short-lived; Xero’s expire every 30 minutes. A refresh token lets your app maintain a persistent connection by trading it for a fresh access token, without dragging the user back through the approval screen. And everything the token can do is limited by scopes, the specific permissions the user granted.
That’s the entire idea. Everything below is those three exchanges made concrete.
Setting up: register an app with Xero
Xero is a popular accounting tool across Australia, New Zealand, the UK and beyond, and its API is a genuinely useful integration we’ve built several times. It’s also one of the more complex, so don’t be discouraged if it takes a couple of passes; it did for us the first time too.
- Create a Xero account and the demo company. Developer access is free if you build against the demo company. One caution: the demo company resets every 24 hours, so anything you create there is temporary.
- Create a “Xero app” in the developer portal. This is a separate entity from your Xero organisation, which trips people up. Give it a name, choose web app, and enter your app’s URL. An app can connect up to 25 organisations before you need to go through certification.
- Create a redirect page in Bubble. OAuth needs somewhere to send the user back to. Make a new page,
xero_redirect; it doesn’t need anything on it. Preview it, copy the URL from the browser without any query string on the end, and paste it into the Xero app’s redirect URI field. You’ll need to add a second redirect URI for production later, since dev and live URLs differ. - Note your client ID and generate a client secret. Both live in the app’s configuration screen. Generate the secret now: skipping this step is a classic cause of the flow failing before it starts.
Exchange one: send the user to authorise
The documentation’s code flow section gives you an authorisation URL template. In Bubble, build it on a button:
- On a test page, add a “Connect to Xero” button with a workflow that opens an external website.
- Paste the authorisation URL from the docs and replace the placeholders, splitting the URL into arbitrary text pieces around each dynamic value so it stays easy to edit: your client ID; the redirect URI; the default scopes from the docs; and a state value.
- For the redirect URI, use Bubble’s
Website home URLexpression to build the address instead of hard-coding it, so the value adjusts itself between your development and live versions.
Press the button. If you’re greeted with “unauthorized client: unknown client or client not enabled”, that’s the missing client secret; generate it and try again. On success you’ll see Xero’s own consent screen, pick the demo company, approve, and land back on your empty xero_redirect page.
Now look at the URL you came back with. Copy it into a note and pull it apart: there’s a code, the scope, your state value echoed back, and a session state. That code is the prize, and the next exchange trades it in.
Exchange two: trade the code for tokens
On the redirect page, add a workflow that runs when Get code from page URL is not empty. That’s the trigger; the action is a new API call.
- In the API Connector, inside your Xero API, create a call named
access token. It’s a POST to the token endpoint from the docs, used as an action. - Authenticate the exchange itself with Basic auth. Concatenate your client ID, a colon, and your client secret, Base64 encode the result (the same manual technique from earlier), and add a header on this call:
Authorizationwith the valueBasicplus that encoded string. Add it at the call level and leave it private, not in shared headers: the rest of Xero’s calls don’t use it; it exists purely to get the token in the first place. - Add the content type the docs specify for this endpoint:
application/x-www-form-urlencoded. - Add three parameters:
grant_type=authorization_code,code= the code from your redirect URL (not private, since the app must supply it dynamically), andredirect_uri= the same redirect address as before. - Initialise. Two failure modes to expect here. First, authorisation codes are only valid for a few minutes, so if you get “invalid request”, run the connect flow again and grab a fresh code. Second, and this one cost us some trial and error: because the content type is URL-encoded form data, the parameters must be sent as form data, not as a querystring or JSON body. Switch that setting and the call succeeds.
What comes back is the payoff: an ID token, an access token, an expiry in seconds (1800, those 30 minutes), the token type and the granted scope.
Exchange three: use the token
Now the token unlocks the actual API, with a couple of Xero-specific wrinkles that teach general lessons.
- Get your tenant. Create a GET call to Xero’s connections endpoint, with an
Authorizationheader ofBearerplus the access token (the access token, note, not the ID token). No moreBasic; that was only for the exchange. Back comes your tenant ID. A tenant is Xero’s word for an organisation; here it’s the demo company. - Get contacts. Xero’s accounting API has a contact database much like any CRM. Create a GET call to the contacts endpoint with the same bearer token. The reference shows optional refinements worth noting for later: filter to contacts modified after a date so you only capture changes, paginate results, add where conditions, pass IDs, or request summary-only responses.
- Add the headers you didn’t know you needed. Our first attempt failed, and the docs’ API Explorer (a try-it-live feature that shows auto-generated headers for any endpoint) revealed why: two headers were missing. Add
accept=application/jsonandxero-tenant-id= your tenant ID. Watch the key name on that second one; the explorer also makes it easy to connect as the wrong organisation, so check you’re on the right company. - Expand the scopes if you’re still blocked. Our contacts call only succeeded after widening the scope list in the authorisation URL and running the flow again with a fresh code. The token can only do what the user approved: that’s not an error, that’s OAuth2 working.
With the wider scopes, something else appears in the token response: a refresh token, courtesy of the offline access scope. That’s the key to a persistent connection.
What a production build adds
This walkthrough deliberately stops at the working skeleton. A full implementation needs a few more steps: save the tokens in your database, use the refresh endpoint to obtain a new access token when the 30 minutes are up, and send the user back through the authorisation flow if the connection has fully expired. Until you’re persisting tokens, expect to repeat the connect flow constantly while testing; the expiry clock is always running.
Step back and compare this with a static API key. The key gave whoever held it access to effectively everything, forever. The OAuth2 connection is scoped to exactly what the user approved, expires in minutes, renews under your control, and never involved the user’s password at all. That’s why the industry keeps moving this way, and why the extra setup is worth understanding rather than avoiding.
This skill in the AI era
OAuth2 is having a second life as the handshake behind AI tooling: it’s how an agent gets scoped, revocable access to a calendar, an inbox or an accounting file, and the consent-code-token-refresh loop in this article is exactly what runs underneath when we build an AI executive assistant that works across someone’s Google Workspace and Xero. AI coding tools can generate the flow in minutes, but deciding the scopes, where tokens live and what happens when they expire is still a human’s call. Understanding the exchange is one of the Bubble-era skills that transfers straight into AI-assisted development.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.