The flow, step by step
Register your app with the provider ahead of time and you get an identifier and a secret.
At login, redirect the browser to the authorization server with that identifier, the address to come back to, the scopes you want, and a random value. Your user authenticates there, by password or passkey or hardware key, none of which is your problem, and approves the scopes.
The authorization server sends the browser back to you carrying a short-lived single-use code, and echoing your random value. Your backend then posts that code plus your secret to the token endpoint over TLS and gets tokens back.
Why two steps and not one
Ask why there are two steps instead of just returning a token in the redirect. Because the redirect travels through the browser: the address bar, the history, the access logs, the referrer headers.
The code is safe to expose there precisely because it is useless without your secret, which never leaves your server, and because it is single use and dies within minutes.
Avoid the old flow that skipped the exchange and put the access token straight into the URL. Years of leaked tokens are why the current specification deletes it.
Add two pieces of hardening that are now simply table stakes. The random value you send and verify on return stops an attacker splicing their own code into your user's session.
And a per-login challenge covers clients that cannot keep a static secret. A mobile app or a browser app generates a random verifier each time, sends its hash with the redirect, and presents the original at exchange time. An intercepted code is then worthless on its own. Current guidance is to use it for every client, including the ones that do hold secrets.
Draw this flow on a whiteboard unprompted and most OAuth interview questions collapse into follow-ups.
Worked example
Maya is adding Login with GitHub to her team's deploy dashboard. She registers an OAuth app in GitHub's developer settings, gets a client_id and a secret that goes straight into the server's secret manager. A teammate clicks the button and gets redirected to github.com/login/oauth/authorize with scope=read:user and a random state. GitHub shows the consent page, the teammate approves, and the browser lands back on the dashboard's /callback URL with a code in the query string. Her server checks state matches, exchanges the code at github.com/login/oauth/access_token inside its ten-minute lifetime, and receives an access token; one call to api.github.com/user turns that into a username and avatar. Total implementation: about 60 lines, and her app never sees, stores, or can possibly leak anyone's GitHub password.