Reading a calendar you cannot log in to
How does a scheduling app read your calendar at another company without knowing your password there? The first version of one meeting-scheduling app did not try: its sign-up form asked for the calendar password and logged in as the user. That password opened the whole account, mail included, when the app needed one calendar. The user could not withdraw it without changing it, and the first time the provider asked for a second factor, the app's login broke. Authentication: passwords, MFA & identity providers explains why apps should not handle passwords at all; this one was not even the app's.
OAuth 2.0 (RFC 6749) replaces the password with a delegation. On the provider's own page, the user approves a limited grant for this app, which can be revoked without changing the password, and the password never leaves the provider.
Four parties and a list of permissions
The protocol names four roles: the resource owner is the user, Kemal, whose calendar it is; the client is the scheduling app; the authorization server is the provider's sign-in and consent service, which issues tokens; the resource server is the calendar API, which accepts them. One company often runs the last two, but they are different jobs.
The app asks for scopes, strings the provider defines, here read-only calendar access. The provider shows Kemal a consent screen naming the app and those scopes, and if he agrees, the app ends up with an access token for the calendar API that carries only what he approved. Narrow scopes are not politeness: they bound what a stolen token can do. What the API does with a scope once the token checks out belongs to Authorization: roles, relationships & policies.
The code crosses the browser, the server keeps the tokens
The usual way to get it is the authorization-code flow, over two paths. The front channel is Kemal's browser, redirected from site to site; anything in its URLs can end up in history and logs. In this app, which has a server, the back channel is a direct HTTPS call from that server to the provider, which no browser sees.
The browser carries only the code, through redirects. The app's server trades it for tokens directly with the provider.
- Kemal clicks Connect calendar; the app redirects his browser to the provider with its client id, redirect URI, scopes, a random
stateand a PKCE challenge (both below). - The provider signs Kemal in, with whatever factors it requires, and shows the consent screen.
- Kemal approves, and the provider redirects the browser to the redirect URI with a short-lived authorization code and the same
state. - The app checks
state, then sends the code over the back channel to the provider's token endpoint, with its own credentials and the PKCE verifier. - The provider checks all of it and answers with an access token, and often a refresh token.
- The app calls the calendar API with the access token.
Only the code crosses the front channel, and it is built to be worth little there: it expires within minutes, works once, and buys nothing unless the rest of step 4 checks out. The tokens never appear in a URL. Where the code lands is set by the redirect URI, which the client registers in advance and the provider must match exactly; the OAuth 2.0 Security Best Current Practice (RFC 9700) lets only the port of a native app's loopback address vary. Accept anything under https://schedule.example/, and an attacker aims the redirect at a page there that forwards visitors elsewhere, an open redirect, to collect codes on their own server.
A callback Kemal never started
Step 3 ends at a URL anyone can send. An attacker starts the flow with their own calendar account, stops before the redirect back, and gets Kemal to open the callback URL with the attacker's code. Without a check, Kemal's scheduling account is now connected to the attacker's calendar; with sign-in, the same trick logs Kemal into the attacker's account. This is login CSRF, cross-site request forgery aimed at the login; the attack in general is in Web application security: injection, XSS, CSRF & SSRF.
state stops it. Before step 1 the app stores a random value in Kemal's session and puts it in the redirect; on the callback it accepts the code only if the returned state matches, then discards it. The attacker's callback carries a value Kemal's session never issued.
An app that cannot keep a secret
In step 4 the web app's server proves it is the client with a client secret or a signed assertion that no user can reach: a confidential client. Then the team ships a mobile app. A secret shipped inside an app on users' devices can be extracted, so a mobile app, like a single-page app in the browser, is a public client, with no secret worth the name; it makes the step 4 exchange itself, so the tokens land in the app or the browser.
On a phone the redirect back often uses a custom URL scheme, which a malicious app can register too, receiving the code; with no secret to check, the code alone would buy tokens. PKCE (Proof Key for Code Exchange, RFC 7636) ties the code to the app instance that asked for it. Before the redirect, the app generates a random code verifier of 43 to 128 characters and sends only its code challenge, the base64url-encoded SHA-256 hash, with the method S256. The provider stores the challenge with the code; at the token endpoint the app sends the verifier, and the provider hashes it and refuses the exchange with invalid_grant if the two differ. The intercepting app has the code, and no way to derive the verifier from the hash. RFC 9700 requires PKCE for public clients and recommends it for confidential ones.
Signing in is a different question
The team adds Sign in with your calendar account, and the first draft uses what it has: get an access token, call the provider's profile API, log in whoever comes back. But an access token is written for the API, often opaque to the app, and is not meant to tell the app who signed in or which app it was issued to. If the app's backend accepts an access token sent to it, another app Kemal has connected to the same provider can send the token it holds for him and, if the profile API accepts that token, be signed in as Kemal. Plain OAuth 2.0 delegates access, not authentication.
OpenID Connect (OIDC) is the identity layer on top of OAuth 2.0 that closes this. The app adds the openid scope to the same flow and a random nonce, kept in Kemal's session and sent in the redirect, and the token response now includes an ID token, a signed JWT. The app checks its signature against the keys the provider publishes, then its claims: iss, the provider that issued it, which must match exactly the provider the app expects; aud, which must contain this app's client id and no audience the app does not trust, so a token issued to another app fails; exp, its expiry; and nonce, which must match the stored value, so a token replayed into another login fails. The user is sub within iss, an identifier the provider never reassigns; an email address can pass to another person, and OpenID Connect Core forbids using it as the unique identifier.
Each token keeps to its audience: the ID token is for the client, the access token for the API, which must refuse an ID token in its place. How the app then keeps Kemal signed in, and how access and refresh tokens expire, rotate and get revoked, is Sessions vs tokens: JWT, refresh & revocation; keeping the tokens out of the browser altogether is The BFF tier: gateway, sessions & fan-out; accepting identities from several providers is Federated Identity.
Two grants retired, one for machines
Two older grants skipped the code. The implicit grant returned the access token in the redirect URL's fragment, readable from browser history and by scripts in the page, and RFC 9700 says clients should not use it; the password grant had the app collect the user's password, the opening problem again, and RFC 9700 says it must not be used. A single-page app uses the code flow with PKCE instead.
A job with no user, such as a nightly call to the app's own reporting API, uses the client-credentials grant: it authenticates as the client and gets a token for itself, with no redirect or consent. Services that prove their identity with certificates instead are in Secrets management & encryption.
The flow as a flowchart
The OAuth Authorization Code Flow Challenge asks for this flow in flowchart shapes, within twelve nodes: an off-page connector for the redirect, labelled with state and the scopes; sign-in and consent at the provider (a manual input fits); a decision on the returned state; a process for the exchange with the PKCE verifier and a decision on its result. Consent denied, a state mismatch and a failed exchange go to error handling, and every path reaches an End terminal. In SSO Identity Provider the same flow runs inside a self-hosted provider; label which of its edges are front channel and which are back channel.