2. How OAuth moves permission safely

OAuth looks complicated because several short-lived values move between the browser, your application, GitHub, and the API. The safety comes from each value doing one narrow job: identify the app, carry the user to consent, bind the callback to the original request, prove the app started the flow, and finally call the API with a limited token.

Authentication vs authorization

Authentication answers "Who are you?" It proves identity with a password, passkey, hardware key, biometric check, or another login method. Authorization answers "What can this application access?" It decides what actions are allowed after identity has been established.

OAuth is primarily about authorization, but it often sits beside authentication. GitHub authenticates the user when they sign in on GitHub's own site. Your Python app asks for permission to do a limited job on that user's behalf, and this is the authorization step.

GitHub sign-in page titled 'Sign in to GitHub' with the subtitle 'to continue to Dev GitHub Tool'. The form shows a username field, a password field, and a green Sign in button.
Authentication. The user proves who they are to GitHub.
GitHub authorization screen titled 'Authorize Dev GitHub Tool' showing requested permissions and an Authorize button.
Authorization. The user decides what the app may access.

The four OAuth roles

The previous page used simple labels: GitHub as the OAuth provider, the GitHub API as the thing being accessed, and the access token as the permission your app receives. OAuth has more precise names for those parts. Learning those names helps you debug the flow later, because each error usually belongs to one role.

Every OAuth flow involves four roles. In this chapter GitHub plays two of them, which is common for developer APIs.

  • Resource owner: the user who owns the data. Here, that is the GitHub user connecting their account.
  • Client: the application requesting access. Here, that is your Python Dev GitHub Tool.
  • Authorization server: the system that handles login, consent, and token issuing. Here, that is GitHub's OAuth service.
  • Resource server: the API that holds the protected data. Here, that is the GitHub REST API.

You only build the client. GitHub already owns the login screen, consent screen, token endpoint, and API.

In larger systems these roles can be separated. A company might use a dedicated identity provider such as Okta or Auth0 for login and consent while storing the protected business data behind a separate API. The role names help you reason about those larger systems without changing the client-side pattern you are learning here.

The authorization code flow

The flow you will build keeps the access token out of the browser redirect. GitHub sends the user back to your app with a short-lived authorization code, not the final token. Your app then checks that the redirect belongs to the flow it started and trades that code for an access token in a separate request to GitHub.

  1. Your app generates a random state value and a PKCE code_verifier.
  2. Your app derives a code_challenge from the verifier.
  3. Your app sends the browser to GitHub's authorization URL with client_id, redirect_uri, scope, state, and code_challenge.
  4. The user signs in to GitHub and approves or rejects access.
  5. GitHub redirects back to your callback URL with a temporary code and the returned state.
  6. Your app compares the returned state with the stored state. If they do not match, it aborts.
  7. Your app sends the code and original code_verifier to GitHub's token endpoint.
  8. GitHub verifies the code exchange and returns an access token.
  9. Your app sends the access token in the Authorization header when calling the GitHub API.

This is the complete map of the flow. Later in the chapter, you will build each step slowly and see exactly where the code fits.

Sequence diagram showing the OAuth 2.0 Authorization Code flow between the user, the Dev GitHub Tool, and GitHub.

Why code first, then token?

The browser redirect is a noisy place. URLs can be visible in browser history, copied into logs, exposed to extensions, or accidentally shared during debugging. OAuth avoids putting the access token there.

The temporary authorization code is useful only for a short time and only when exchanged correctly. With PKCE, the code is also bound to the verifier your app created before the redirect. If someone intercepts the code but does not have the verifier, the token exchange should fail.

In this chapter, PKCE does not replace the client secret. The client secret still identifies your registered OAuth App during the token exchange. PKCE adds a per-flow proof: this specific token exchange came from the same app instance that created the original authorization URL.

The pieces your app tracks

Piece What it does Where it lives
Client ID Public identifier for your OAuth app. Safe to include in authorization URLs.
Client secret Private credential for your OAuth app. Loaded from .env; never committed.
State Binds the callback to the flow your app started. Generated per flow; checked before token exchange.
Code verifier PKCE secret generated by your app. Kept by your app until token exchange.
Code challenge Hash derived from the verifier. Sent to GitHub in the authorization URL.
Authorization code Temporary proof that the user approved the request. Received on callback; exchanged once.
Access token Credential used to call the API. Kept private; sent only in API request headers.

With the model in place, the next step is setup: register the GitHub OAuth App and create the local project files the code will depend on.