skip to content
luminary.blog
by Oz Akan
deer

OAuth 2.0 Flows, Security and Best Practices

Comprehensive OAuth 2.0 reference covering authorization flows, PKCE, token security, SPA patterns, and implementation best practices with detailed diagrams.

/ 17 min read

Updated:
Table of Contents

I’ve always wanted to put all my notes to somewhere so I could find them when I needed. I hope it will be helpful for you as well.

1. What is OAuth 2.0?

Imagine you want to use a third-party application (e.g., a photo printing service) to access your photos stored on another service (e.g., Google Photos). Without a mechanism like OAuth, you might be tempted to give the printing service your Google username and password. This is highly insecure:

  • The printing service gets full access to your entire Google account, not just photos.
  • You have to trust the printing service to store your credentials securely.
  • Changing your Google password breaks the printing service integration.
  • There’s no standard way to revoke access for just the printing service without changing your main password.

OAuth 2.0 solves this by providing a delegated authorization framework. Instead of sharing your credentials, you (the Resource Owner) grant the third-party application (the Client) limited permission (defined by scopes) to access specific resources (your photos) hosted on another server (the Resource Server, e.g., Google Photos API), usually for a limited time. This permission is granted via an Access Token, issued by an Authorization Server that you trust.

Think of your main account password as the master key to your car. You wouldn’t give this to a parking valet. Instead, you give them a valet key. This key can typically only start the engine and lock/unlock the doors, but it cannot open the trunk or the glove compartment. OAuth 2.0 provides the “valet key” (Access Token) for applications, granting specific, limited permissions without handing over the “master key” (your password).

OAuth 2.0 is an Authorization Framework. It’s crucial to understand that OAuth 2.0 itself is primarily about authorization – granting permission to access resources. It’s not inherently an authentication protocol (proving who a user is), although authentication of the resource owner is often a prerequisite step within an OAuth flow. (OpenID Connect builds upon OAuth 2.0 to add a standard authentication layer).

2. Core Concepts & Terminology

  • Resource Owner: The user who owns the data/resource and can grant access to it (e.g., you, the owner of the photos).
  • Client: The application requesting access to the Resource Owner’s resources (e.g., the photo printing service). Clients can be:
    • Confidential: Can securely store credentials (e.g., backend web applications).
    • Public: Cannot securely store credentials (e.g., Single-Page Apps running in a browser, native mobile apps).
  • Authorization Server (AS): The server that authenticates the Resource Owner, obtains their consent, and issues Access Tokens (and optionally Refresh Tokens) to the Client upon successful authorization (e.g., Google’s authentication server). Often, the AS and RS are run by the same entity.
  • Resource Server (RS): The server hosting the protected resources (e.g., Google Photos API server). It accepts and validates Access Tokens from the Client before granting access to the requested resources.
  • Access Token: A credential (often a string, potentially a JWT) representing the authorization granted to the Client. It’s used by the Client to access the Resource Server. Access Tokens are typically short-lived.
  • Refresh Token: An optional credential used by the Client to obtain a new Access Token from the Authorization Server when the current Access Token expires, without requiring the Resource Owner to re-authenticate or re-authorize. Refresh Tokens are typically long-lived but can be revokedNote: Refresh Tokens are not issued by all flows or providers—they are typically not available with Implicit Grant, Client Credentials Grant, and many providers restrict their issuance to public clients (SPAs, mobile apps) for security reasons.
  • Scope: Defines the specific permissions the Client is requesting (e.g., read_photos, write_calendar). Scopes limit the Client’s access to only what’s necessary.
  • Authorization Endpoint: Used by the Client to direct the Resource Owner for authentication and consent. Returns an Authorization Code (in the Authorization Code flow) or an Access Token (in the Implicit flow - legacy).
  • Token Endpoint: Used by the Client (with its credentials and often an authorization code or refresh token) to exchange for an Access Token directly with the Authorization Server (not via the user’s browser).
  • Redirection Endpoint (Redirect URI / Callback URL): The URL within the Client application where the Authorization Server sends the user back after they approve or deny the request. The Authorization Server sends the authorization code or tokens to this URI. Must be pre-registered with the Authorization Server for security.

3. OAuth 2.0 Grant Types (Flows)

Grant Types are the different methods (“flows”) a Client can use to obtain an Access Token. The choice depends on the Client type and use case.

Authorization Code Grant (with PKCE)

  • Best For: Web Applications (confidential clients), Native/Mobile Apps (public clients), Single Page Apps (public clients).

  • Security: High. Considered the most secure and recommended flow, especially with PKCE.

  • Flow:

    1. (Client -> User’s Browser): Redirects user to the Authorization Server’s Authorization Endpoint with client_id, redirect_uri, scope, response_type=code, state, and (for PKCE) code_challenge & code_challenge_method.
    2. (User -> Authorization Server): User logs in and grants consent (if needed).
    3. (Authorization Server -> User’s Browser): Redirects user back to the Client’s pre-registered Redirection Endpoint with an authorization_code and the original state parameter.
    4. (Client Backend <-> Authorization Server): Client (using its client_id, client_secret [for confidential clients], the received authorization_code, redirect_uri, and [for PKCE] code_verifier) contacts the Authorization Server’s Token Endpoint directly (server-to-server).
    5. (Authorization Server -> Client Backend): Authorization Server verifies the code (and PKCE verifier), authenticates the Client, and returns an access_token and optionally a refresh_token.
    6. (Client -> Resource Server): Client uses the access_token to request resources from the Resource Server.
  • PKCE (Proof Key for Code Exchange - RFC 7636): Mitigates authorization code interception attacks, especially for public clients (mobile/SPA) that cannot securely store a client_secret. The client generates a secret (code_verifier), creates a transformed version (code_challenge), sends the challenge in step 1, and sends the verifier in step 4. The AS verifies that the challenge matches the verifier. PKCE is mandatory for public clients and provides additional security even for confidential clients, though most server-side applications do not implement it since they can securely authenticate with client secrets.

Implicit Grant (Legacy)

  • Best For: Originally for JavaScript apps running entirely in the browser (SPAs). Now generally discouraged.
  • Security: Lower. Access Token is returned directly in the URL fragment, exposing it in browser history/logs. No Refresh Tokens are issued in this flow.
  • Flow:
    1. (Client -> User’s Browser): Redirects user to Authorization Endpoint with response_type=token.
    2. (User -> Authorization Server): User logs in and grants consent.
    3. (Authorization Server -> User’s Browser): Redirects user back to Redirection Endpoint with access_token directly in the URL fragment (#).
    4. (Client Frontend): Client extracts token from the URL fragment using JavaScript.
  • Issues: Security risks (token leakage), no Refresh Tokens. Use Authorization Code with PKCE instead.

ROPC - Legacy

Resource Owner Password Credentials Grant

  • Best For: Highly trusted (“first-party”) applications where redirect-based flows are impossible (e.g., some legacy desktop or command-line tools). Highly discouraged for general use.
  • Security: Low. Requires the Client to collect the Resource Owner’s username and password directly, defeating the primary purpose of OAuth (avoiding password sharing).
  • Flow:
    1. (User -> Client): User enters username and password directly into the Client application.
    2. (Client <-> Authorization Server): Client sends username, password, client_id, (optional client_secret), and grant_type=password to the Token Endpoint.
    3. (Authorization Server -> Client): AS validates credentials and returns access_token and optionally refresh_token.
  • Issues: Breaks the principle of delegated authority, increases risk, bypasses AS security features (like MFA prompts). Avoid unless absolutely necessary and only for first-party clients.

Client Credentials Grant

  • Best For: Machine-to-Machine (M2M) communication, where the Client is acting on its own behalf (not on behalf of a user). E.g., backend services accessing other internal APIs.
  • Security: High (when client credentials are kept secure).
  • Flow:
    1. (Client <-> Authorization Server): Client authenticates directly with the Authorization Server’s Token Endpoint using its client_id and client_secret (or other registered credentials like JWT assertions) and grant_type=client_credentials.
    2. (Authorization Server -> Client): AS validates client credentials and returns an access_token. No Refresh Tokens are issued in this flow since the client acts on its own behalf, not a user’s.

Refresh Token Grant

  • Best For: Obtaining a new Access Token when the current one expires, without user interaction. Applicable to flows that issue Refresh Tokens (typically Authorization Code).
  • Security: Depends heavily on how securely the Refresh Token is stored by the Client.
  • Flow:
    1. (Client <-> Authorization Server): Client sends its client_id, client_secret (for confidential clients), grant_type=refresh_token, and the refresh_token to the Token Endpoint.
    2. (Authorization Server -> Client): AS validates the Refresh Token and Client credentials, and if valid, issues a new access_token (and potentially a new refresh_token - see Refresh Token Rotation).

4. Tokens In-Depth

  • Access Tokens:
    • Purpose: Presented to the Resource Server to access protected resources.
    • Format: Can be opaque strings (understood only by the AS/RS) or structured tokens like JSON Web Tokens (JWTs). JWTs contain verifiable claims (like user ID, scopes, expiry) signed by the AS.
    • Bearer Tokens: Most common type. Anyone possessing the token (the “bearer”) can use it. Must be protected carefully (HTTPS, secure storage).
    • Lifetime: Should be short-lived (minutes to hours) to minimize damage if compromised.
  • Refresh Tokens:
    • Purpose: Obtain new Access Tokens without re-authenticating the user.
    • Format: Usually opaque strings.
    • Lifetime: Much longer-lived than Access Tokens (days, weeks, months), but should ideally expire or be revocable.
    • Storage: Must be stored securely by the Client (e.g., encrypted in a database for web apps, secure storage mechanisms on mobile). Never store Refresh Tokens in browser local storage. For SPAs, the most secure approach is to avoid Refresh Tokens entirely (forcing re-authentication when Access Tokens expire) or use a Backend-for-Frontend (BFF) pattern where tokens are managed server-side with secure, HttpOnly cookies.
    • Rotation: A security best practice where using a Refresh Token invalidates it and returns both a new Access Token and a new Refresh Token. Helps detect token theft.
  • Token Introspection (RFC 7662): An endpoint on the Authorization Server where Resource Servers (or Clients) can check the validity and metadata (scopes, expiry, user ID) of a token. Useful for opaque tokens or when the RS doesn’t trust its own validation logic. (Note: Not all OAuth providers implement introspection endpoints—check your provider’s documentation.)
  • Token Revocation (RFC 7009): An endpoint on the Authorization Server allowing Clients or Resource Owners to invalidate Access Tokens or Refresh Tokens before they expire (e.g., user logs out, token compromised). (Note: Revocation endpoint support varies by provider—some offer partial support or alternative revocation methods.)
  • Principle of Least Privilege: Clients should only request the minimum permissions (scopes) necessary for their functionality. Don’t ask for full_account_access if you only need read_profile.
  • User Consent Screen: During flows involving the user (like Authorization Code), the Authorization Server typically presents a screen listing the Client application name and the specific scopes (permissions) it’s requesting. The Resource Owner must explicitly approve (consent) before an authorization code or token is issued. Consent might be remembered for trusted clients.

6. Security Considerations

OAuth 2.0 relies heavily on correct implementation to be secure.

  • Use HTTPS/TLS Everywhere: All communication involving tokens, codes, or redirects MUST use TLS (HTTPS) to prevent eavesdropping and modification.
  • Register and Validate Redirect URIs: The Authorization Server MUST only redirect to pre-registered URIs for a given Client. Use exact matches, not wildcards or partial paths if possible. This prevents codes/tokens from being sent to malicious sites (Open Redirector attacks).
  • Use the state Parameter: In redirect-based flows (Authorization Code, Implicit), the Client generates a unique, unpredictable value (state), includes it in the initial request to the Authorization Endpoint, and verifies that the same value is returned by the Authorization Server in the callback. This prevents Cross-Site Request Forgery (CSRF) attacks.
  • Use PKCE: PKCE is mandatory for public clients (SPAs, Mobile Apps) using the Authorization Code grant to prevent authorization code interception. While not required for confidential clients, it provides additional security.
  • Secure Token Storage:
    • Backend/Confidential Clients: Store Refresh Tokens encrypted at rest.
    • SPAs: Avoid storing tokens in Local Storage (vulnerable to XSS). Modern security guidance recommends either: (1) storing short-lived Access Tokens only in memory and forcing re-authentication when they expire, or (2) using a Backend-for-Frontend (BFF) pattern where all tokens are managed server-side with secure, HttpOnly cookies. Many providers no longer issue Refresh Tokens to browser-based apps.
    • Mobile Apps: Use platform-provided secure storage mechanisms (Keychain on iOS, Keystore/EncryptedSharedPreferences on Android).
  • Validate Access Tokens (Resource Server):
    • Signature: If JWTs are used, verify the signature using the Authorization Server’s public key.
    • Expiry (exp claim): Ensure the token hasn’t expired.
    • Audience (aud claim): Ensure the token was intended for this Resource Server/API.
    • Issuer (iss claim): Ensure the token was issued by the expected Authorization Server.
    • Scopes: Verify the token contains the necessary scope(s) for the requested operation.
  • Scope Validation (Client & Resource Server): Don’t just request scopes, ensure the token received actually contains the granted scopes and that the granted scopes are sufficient for the API call being made.
  • Audience Restriction: Ensure tokens issued are specifically intended for the Resource Server(s) that will consume them.
  • Refresh Token Security: Treat Refresh Tokens as highly sensitive credentials. Implement rotation, secure storage, and provide revocation mechanisms.

7. OAuth 2.0 vs. OpenID Connect (OIDC)

  • Authorization vs. Authentication:
    • OAuth 2.0: Primarily for Authorization (granting access to resources).
    • OpenID Connect (OIDC): A thin identity layer built on top of OAuth 2.0. It standardizes how to perform Authentication (verifying user identity) and obtain basic profile information using OAuth 2.0 flows.
  • ID Token: OIDC introduces the id_token, a specific type of JWT issued alongside the Access Token. It contains verifiable claims about the authenticated user (e.g., user ID (sub), issuer (iss), audience (aud), expiry (exp), issuance time (iat), nonce). The Client validates the ID Token to authenticate the user.
  • Userinfo Endpoint: OIDC defines a standard /userinfo endpoint (protected by the Access Token) where the Client can retrieve additional profile claims about the user.
  • Scope: OIDC uses specific scopes like openid, profile, email, address, phone to request identity information. The openid scope is mandatory for OIDC requests.

In short: If you need to know who the user is, use OpenID Connect. If you only need permission for an application to access an API on the user’s behalf (or its own behalf), OAuth 2.0 is sufficient. Many identity providers (like Google, Microsoft Azure AD, Okta, Amazon Cognito) implement both.

8. When to Use OAuth 2.0

  • Allowing third-party applications to access user data from your service without sharing passwords.
  • Allowing your own frontend (SPA, mobile app) to access your backend APIs securely on behalf of a user.
  • Securing communication between microservices (using Client Credentials grant).
  • Enabling Single Sign-On (SSO) scenarios when combined with OpenID Connect.
  • Delegating authorization decisions to a central Authorization Server.

9. Common Pitfalls & Misconceptions

  • Confusing OAuth 2.0 with Authentication: Remembering that OAuth 2.0 alone doesn’t prove who the user is, only that the user granted permission. Use OIDC for authentication.
  • Using the Wrong Grant Type: Choosing an insecure flow (like Implicit or ROPC) when Authorization Code with PKCE is appropriate.
  • Insecure Redirect URI Handling: Using wildcards or failing to validate URIs properly.
  • Ignoring state Parameter: Exposing the application to CSRF attacks.
  • Not Using PKCE: Exposing public clients to code interception attacks.
  • Insecure Token Storage: Storing tokens (especially Refresh Tokens) in unsafe locations like browser Local Storage.
  • Failing to Validate Tokens Properly: Not checking signature, expiry, audience, issuer, or scopes on the Resource Server.
  • Requesting Overly Broad Scopes: Violating the principle of least privilege.
  • Treating Access Tokens like Session Cookies: Access Tokens (especially Bearer tokens) are valuable and need careful handling; they aren’t protected by browser same-origin policies in the same way cookies are.
  • Real-world session management: Applications often need to bridge browser sessions with token-based auth—recommended approaches include using short-lived Access Tokens (15-30 minutes), implementing token refresh through secure backend endpoints, and considering Backend-for-Frontend (BFF) patterns for browser-based apps where session management occurs server-side.

10. Conclusion

OAuth 2.0 is the industry standard framework for delegated authorization. It enables secure access to resources without sharing primary credentials, improving security and user experience. Understanding the different roles, flows (especially Authorization Code with PKCE), token types, and crucial security considerations (HTTPS, state, PKCE, token validation, secure storage) is vital for correct and secure implementation. Always refer to the official RFCs (RFC 6749, RFC 6750, RFC 7636, etc.) and security best practices guides for authoritative details.

Diagrams

1. Authorization Code Grant with PKCE

Recommended Flow for Web/Mobile Apps

This is the most secure and commonly used flow, especially for applications that have a backend (confidential clients) or for public clients like SPAs and mobile apps (using PKCE).

Resource Server (RS)Authorization Server (AS)Client ApplicationUser (Resource Owner)Resource Server (RS)Authorization Server (AS)Client ApplicationUser (Resource Owner)Generate PKCE code_verifier & code_challengeInitiates action requiring resource access (e.g., clicks 'Login with Service')Redirect User's browser to AS Authorization Endpoint (with client_id, redirect_uri, scope, state, response_type=code, code_challenge, code_challenge_method)Logs in (if not already)Presents Consent Screen (showing Client & Scopes)Grants Consent / Approves RequestRedirect User's browser back to Client's pre-registered Redirect URI (with authorization_code, state)Browser delivers Authorization Code & state to Client Redirect URIVerify received 'state' matches original 'state'Exchange Authorization Code for Tokens (POST /token endpoint) Sends: code, client_id, client_secret (if confidential), redirect_uri, grant_type=authorization_code, code_verifierValidate code, client credentials, PKCE code_verifier against stored code_challenge, redirect_uriReturns Access Token, Refresh Token (optional), ID Token (if OIDC)Request protected resource using Access Token (e.g., Authorization: Bearer [Access_Token])Validate Access Token (signature, expiry, audience, issuer, scope) (May involve call to AS introspection endpoint or checking JWT signature/claims)Returns requested Protected ResourceDisplay requested resource / complete action

2. Client Credentials Grant

Machine-to-Machine

Used when a client (e.g., a backend service, API) needs to access resources using its own identity, not on behalf of a user.

Resource Server (RS)Authorization Server (AS)Client Application (Service)Resource Server (RS)Authorization Server (AS)Client Application (Service)Request Access Token (POST /token endpoint) Sends: grant_type=client_credentials, client_id, client_secret (or other auth method), scopeAuthenticate Client, Validate RequestReturns Access TokenRequest protected resource using Access Token (Authorization: Bearer [Access_Token])Validate Access Token (signature, expiry, audience, issuer, scope)Returns requested Protected Resource / Service Response

3. Refresh Token Grant

Getting a New Access Token

Used when an existing Access Token has expired, and the client uses a long-lived Refresh Token to obtain a new Access Token without user interaction. This follows a flow like Authorization Code grant that previously issued a Refresh Token.

Resource Server (RS)Authorization Server (AS)Client ApplicationResource Server (RS)Authorization Server (AS)Client ApplicationHas valid Refresh Token, but Access Token expired or is invalid.Request protected resource using expired Access TokenValidate Access Token (fails - expired)Error Response (e.g., 401 Unauthorized, indicating invalid token)Request new Access Token using Refresh Token (POST /token endpoint) Sends: grant_type=refresh_token, refresh_token, client_id, client_secret (if confidential), scope (optional)Validate Refresh Token, Client CredentialsReturns *new* Access Token (and potentially a *new* Refresh Token if rotation is enabled)Request protected resource using *new* Access Token (Authorization: Bearer [New_Access_Token])Validate *new* Access Token (signature, expiry, audience, issuer, scope)Returns requested Protected Resource

Simplified Diagrams

1. Authorization Code Grant

ResourceServerAuthServerClientAppUserResourceServerAuthServerClientAppUserWants to loginRedirect to /authorizePrompt for login/consentEnters credentials, grants accessRedirect with auth codeSend auth code + client secret to /tokenAccess token (and refresh token)Request protected resource with access tokenProtected resource

2. Client Credentials Grant

ResourceServerAuthServerClientAppResourceServerAuthServerClientAppRequest token with client_id & client_secretAccess tokenRequest resource with tokenProtected resource