Modern applications rely heavily on APIs to connect services, share data, and enable rich user experiences. Securing these APIs is paramount. Without proper safeguards, sensitive user data and system integrity face constant threats. Two foundational protocols, OAuth 2.0 and OpenID Connect, provide the robust framework necessary for secure API authentication and authorization. Understanding their mechanisms and practical application is essential for any developer building connected systems.
This guide breaks down OAuth 2.0 and OpenID Connect, explaining their core concepts, how they work together, and offering actionable insights for implementation.
The Challenge of Secure API Access
Imagine a user wants to grant a third-party application, like a photo printing service, access to their photos stored on a social media platform. The traditional approach would involve the user providing their social media username and password directly to the printing service. This creates a significant security risk. The printing service would then have full access to the user’s account, not just photos, and the user would have no way to revoke that access without changing their password, which would affect all other legitimate applications.
This scenario highlights the need for a standardized, secure method of delegated authorization. Users need to grant limited access to their resources without sharing their primary credentials. This is where OAuth 2.0 steps in.
OAuth 2.0: Delegated Authorization
OAuth 2.0 is an authorization framework that enables an application to obtain limited access to a user’s protected resources on an HTTP service, such as an API. It does this on behalf of the user, without exposing the user’s credentials to the client application. OAuth 2.0 is about authorization: determining what a client can do with a user’s resources. It is not about authentication: verifying who the user is. OpenID Connect handles authentication.
Key Roles in OAuth 2.0
To understand OAuth 2.0, it helps to define the four main roles involved in the interaction:
- Resource Owner: This is the user who owns the protected resources (e.g., their photos, contacts, or profile information) and can grant access to them.
- Client: This is the application requesting access to the Resource Owner’s protected resources. It could be a web application, a mobile app, or another server-side application.
- Authorization Server: This server is responsible for authenticating the Resource Owner and issuing access tokens to the Client after the Resource Owner grants permission.
- Resource Server: This server hosts the protected resources and accepts access tokens to validate requests from the Client. It’s typically the API that the Client wants to access.
Core Concepts and Tokens
Several key concepts and tokens facilitate the OAuth 2.0 flow:
- Access Token: A credential that grants the Client access to specific protected resources on the Resource Server. Access tokens are typically short-lived and should be treated as opaque strings by the Client.
- Refresh Token: A credential used by the Client to obtain new access tokens when the current one expires, without requiring the Resource Owner to re-authorize. Refresh tokens are typically long-lived and must be stored securely.
- Authorization Code: A temporary code exchanged by the Client with the Authorization Server for an access token. This is a crucial part of secure flows.
- Client ID: A unique identifier issued to the Client by the Authorization Server during registration.
- Client Secret: A confidential credential used by the Client to authenticate itself to the Authorization Server. This must be kept secret and is only used by confidential clients (e.g., server-side applications).
- Redirect URI: A URI registered with the Authorization Server. After the Resource Owner grants authorization, the Authorization Server redirects the user’s browser back to this URI, along with the authorization code.
- Scope: A mechanism to limit the access granted to the Client. Scopes define the specific permissions requested by the Client (e.g.,
read:photos,write:profile).
Common OAuth 2.0 Grant Types
OAuth 2.0 defines several “grant types” or “flows” for different client types and scenarios. The choice of grant type depends on the Client’s capabilities and security requirements.
- Authorization Code Flow (with PKCE): This is the most secure and recommended flow for most client types, especially public clients like single-page applications (SPAs) and mobile apps. It involves a redirect to the Authorization Server, where the user authenticates and authorizes the Client. The Authorization Server then returns an authorization code to the Client, which exchanges it for an access token. Proof Key for Code Exchange (PKCE) adds an extra layer of security against interception attacks.
- Client Credentials Flow: Used when the Client itself is the Resource Owner, or when it acts on its own behalf, not a user’s. This is common for machine-to-machine communication where a service needs to access another service’s API directly.
- Implicit Flow: This flow directly returns the access token in the redirect URI fragment. It was once used for SPAs but is now largely deprecated due to security concerns, primarily the risk of token leakage in the browser history or referrer headers.
- Resource Owner Password Credentials Flow: This flow involves the Client collecting the user’s username and password and sending them directly to the Authorization Server. It is highly discouraged and should only be used for trusted, first-party applications where other flows are not feasible, as it bypasses the delegated authorization principle.
Step-by-Step: Authorization Code Flow with PKCE
This is the most robust flow for web and mobile applications.
- Client initiates authorization: The Client application redirects the user’s browser to the Authorization Server’s authorization endpoint. This request includes the Client ID, requested scopes, a
redirect_uri, astateparameter (for CSRF protection), and PKCE parameters (code_challengeandcode_challenge_method). - Resource Owner authenticates and authorizes: The Authorization Server authenticates the Resource Owner (if not already logged in) and prompts them to grant or deny the Client’s requested permissions (scopes).
- Authorization Server redirects with code: If the Resource Owner grants permission, the Authorization Server redirects the user’s browser back to the Client’s registered
redirect_uri. This redirect includes anauthorization_codeand thestateparameter. - Client exchanges code for tokens: The Client receives the
authorization_code. It then makes a direct, back-channel request to the Authorization Server’s token endpoint. This request includes theauthorization_code,redirect_uri, Client ID, and the PKCEcode_verifier. For confidential clients, theclient_secretis also included. - Authorization Server issues tokens: The Authorization Server validates the
authorization_codeand PKCE parameters. If valid, it issues anaccess_tokenand optionally arefresh_tokento the Client. - Client accesses protected resources: The Client uses the
access_tokento make requests to the Resource Server’s API. The Resource Server validates theaccess_tokenbefore granting access to the protected resources. - Token refresh (if needed): When the
access_tokenexpires, the Client can use therefresh_token(if issued) to request a newaccess_tokenfrom the Authorization Server, without user interaction.
OpenID Connect: Adding Identity to OAuth 2.0
While OAuth 2.0 provides authorization, it does not provide a standard way for the Client to verify the identity of the Resource Owner. This is where OpenID Connect (OIDC) comes in. OIDC is an identity layer built on top of OAuth 2.0. It allows Clients to verify the identity of the end-user based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the end-user in an interoperable and REST-like manner.
The ID Token
The core component of OpenID Connect is the ID Token. This is a JSON Web Token (JWT) that contains claims (pieces of information) about the authenticated user. The ID Token is signed by the Authorization Server, allowing the Client to verify its authenticity and integrity.
Common claims found in an ID Token include:
iss: Issuer identifier (the Authorization Server).sub: Subject identifier (unique ID for the user).aud: Audience (the Client ID for which the ID Token is intended).exp: Expiration time of the ID Token.iat: Time at which the ID Token was issued.auth_time: Time when the user last authenticated.name: User’s full name.email: User’s email address.picture: URL of the user’s profile picture.
How OpenID Connect Works with OAuth 2.0
OIDC extends the OAuth 2.0 Authorization Code Flow by introducing the openid scope and requesting an ID Token alongside the access token.
- Client requests
openidscope: In the initial authorization request, the Client includesopenidin thescopeparameter, along with any other desired OAuth 2.0 scopes. - Authorization Server authenticates and issues tokens: After the user authenticates and authorizes, the Authorization Server returns an
authorization_code(as in OAuth 2.0). When the Client exchanges this code for tokens, the Authorization Server now issues both anaccess_tokenand anid_token. - Client verifies ID Token: The Client receives the
id_token(a JWT). It then verifies the signature of the ID Token using the Authorization Server’s public keys (obtained from its discovery endpoint). It also checks claims likeiss,aud, andexpto ensure the token is valid and intended for this Client. - Client extracts user information: Once verified, the Client can safely extract user information (claims) from the ID Token, such as the user’s unique ID (
sub), name, and email. This information can be used to establish a user session in the Client application. - Client uses Access Token for APIs: The
access_tokenis still used to call protected APIs on the Resource Server, just as in pure OAuth 2.0.
Practical Application for Developers
Implementing OAuth 2.0 and OpenID Connect correctly requires careful attention to detail. Here are practical considerations for developers.
Choosing the Right Flow
- Web Applications (Server-Side): Use the Authorization Code Flow with PKCE. The Client Secret can be securely stored on the server.
- Single-Page Applications (SPAs): Use the Authorization Code Flow with PKCE. Since SPAs cannot securely store a Client Secret, PKCE is critical to prevent authorization code interception.
- Mobile Applications: Use the Authorization Code Flow with PKCE. Similar to SPAs, mobile apps are public clients and cannot keep a Client Secret confidential.
- Machine-to-Machine Communication: Use the Client Credentials Flow. This is for services communicating directly with each other without a user context.
Implementing a Client Application
For a web or mobile application acting as a Client:
- Register your application: Register your application with the Authorization Server (e.g., Google, Auth0, Okta, your own identity provider). You will receive a Client ID and, for confidential clients, a Client Secret. Register all valid
redirect_uris. - Initiate the authorization request: Construct the URL to the Authorization Server’s authorization endpoint, including
client_id,redirect_uri,scope(at leastopenidfor OIDC),response_type=code,state, and PKCE parameters (code_challenge,code_challenge_method). - Handle the redirect: After the user authorizes, your
redirect_uriwill receive theauthorization_codeandstate. Verify thestateparameter matches the one sent in the initial request to prevent CSRF attacks. - Exchange code for tokens: Make a POST request from your backend (for web apps) or directly from the client (for mobile/SPA with PKCE) to the Authorization Server’s token endpoint. Include the
authorization_code,redirect_uri,client_id, andcode_verifier. For confidential clients, include theclient_secret. - Process tokens: Store the
access_tokenandrefresh_tokensecurely. Validate theid_token(for OIDC) by checking its signature, issuer, audience, and expiration. Extract user claims from theid_tokento establish a user session.
Implementing a Resource Server (API)
Your API, acting as a Resource Server, needs to validate incoming access tokens.
- Receive access token: The Client sends the
access_tokenin theAuthorizationheader as a Bearer token (e.g.,Authorization: Bearer <access_token>). - Validate the token:
- Local validation (for JWT access tokens): If the access token is a JWT, the Resource Server can validate its signature using the Authorization Server’s public keys. It also checks claims like
iss,aud, andexp. - Introspection endpoint: If the access token is opaque (not a JWT), the Resource Server can send it to the Authorization Server’s introspection endpoint. The Authorization Server will respond with information about the token, including whether it’s active and its associated scopes and user.
- Local validation (for JWT access tokens): If the access token is a JWT, the Resource Server can validate its signature using the Authorization Server’s public keys. It also checks claims like
- Check scopes: After validating the token, the Resource Server must check if the token’s granted scopes permit the requested operation. For example, if the API endpoint requires
write:photosscope, and the token only hasread:photos, access should be denied. - Extract user identity (optional): If the access token is a JWT and contains a
subclaim, the Resource Server can use this to identify the user making the request.
Security Considerations
- Token Storage: Access tokens should be stored in memory or secure, short-lived storage. Refresh tokens, if used, must be stored with extreme care, typically in HTTP-only cookies (for web apps) or secure storage mechanisms provided by mobile operating systems.
- PKCE: Always use PKCE for public clients (SPAs, mobile apps) to mitigate authorization code interception attacks.
- State Parameter: Use the
stateparameter in the authorization request to prevent Cross-Site Request Forgery (CSRF) attacks. Thestatevalue should be a cryptographically random string associated with the user’s session. - Nonce Parameter (OIDC): For OpenID Connect, use the
nonceparameter in the authorization request and verify it against thenonceclaim in the ID Token to mitigate replay attacks. - Token Expiration and Rotation: Access tokens should have short lifespans. Refresh tokens should be rotated after use to enhance security.
- HTTPS Everywhere: All communication between Client, Authorization Server, and Resource Server must occur over HTTPS to protect tokens and codes from interception.
- Client Secret Protection: For confidential clients, the Client Secret must be kept strictly confidential and never exposed in client-side code.
Code Example: Decoding an ID Token (Conceptual)
Here’s a conceptual Python snippet demonstrating how a client might decode and verify an ID Token (JWT) after receiving it. In a real application, you would use a robust JWT library and fetch the public keys from the Authorization Server’s JWKS (JSON Web Key Set) endpoint.
import jwt
import requests
# --- Configuration (replace with actual values) ---
JWKS_URL = "https://your-auth-server.com/.well-known/jwks.json"
CLIENT_ID = "your_client_id"
ISSUER = "https://your-auth-server.com/"
# --- Simulate receiving an ID Token ---
# In a real scenario, this would come from the Authorization Server
sample_id_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1In0.eyJpc3MiOiJodHRwczovL3lvdXItYXV0aC1zZXJ2ZXIuY29tLyIsInN1YiI6InVzZXIxMjMiLCJhdWQiOiJ5b3VyX2NsaWVudF9pZCIsImV4cCI6MTY3ODg4NjQwMCwiaWF0IjoxNjc4ODgyODAwLCJuYW1lIjoiSm9obiBEb2UiLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20ifQ.SIGNATURE"
def get_public_keys(jwks_url):
"""Fetches and returns the public keys from the JWKS endpoint."""
response = requests.get(jwks_url)
response.raise_for_status()
jwks = response.json()
return jwks
def validate_id_token(id_token, jwks, client_id, issuer):
"""Validates and decodes an OpenID Connect ID Token."""
try:
# Get the header to find the key ID (kid)
header = jwt.get_unverified_header(id_token)
kid = header['kid']
# Find the correct key from the JWKS
key = next(k for k in jwks['keys'] if k['kid'] == kid)
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
# Decode and verify the token
decoded_token = jwt.decode(
id_token,
public_key,
algorithms=[header['alg']],
audience=client_id,
issuer=issuer,
options={"require": ["exp", "iat", "sub", "aud", "iss"]}
)
print("ID Token successfully validated and decoded:")
for claim, value in decoded_token.items():
print(f" {claim}: {value}")
return decoded_token
except jwt.ExpiredSignatureError:
print("Error: ID Token has expired.")
except jwt.InvalidAudienceError:
print("Error: ID Token audience is invalid.")
except jwt.InvalidIssuerError:
print("Error: ID Token issuer is invalid.")
except jwt.InvalidSignatureError:
print("Error: ID Token signature is invalid.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
return None
# --- Main execution ---
if __name__ == "__main__":
print("Fetching public keys...")
try:
public_keys = get_public_keys(JWKS_URL)
print("Public keys fetched.")
print("\nAttempting to validate ID Token...")
validated_claims = validate_id_token(sample_id_token, public_keys, CLIENT_ID, ISSUER)
if validated_claims:
print("\nUser 'sub' claim:", validated_claims.get('sub'))
print("User 'name' claim:", validated_claims.get('name'))
except requests.exceptions.RequestException as e:
print(f"Failed to fetch JWKS: {e}")
except StopIteration:
print("Error: No matching public key found in JWKS for the ID Token's 'kid'.")
Note: The sample_id_token in the code is a placeholder and will not actually validate without a corresponding private key to sign it and a real JWKS endpoint. This snippet illustrates the conceptual steps.
Conclusion
OAuth 2.0 and OpenID Connect form the backbone of secure authentication and authorization for modern applications and APIs. OAuth 2.0 provides a robust framework for delegated authorization, allowing users to grant limited access to their resources without compromising their primary credentials. OpenID Connect builds on this by adding an identity layer, enabling clients to verify user identity and obtain basic profile information through cryptographically signed ID Tokens. By understanding and correctly implementing these protocols, developers can build secure, reliable, and user-friendly applications that protect sensitive data and maintain system integrity. Adhering to best practices, such as using PKCE, validating tokens rigorously, and securing token storage, is fundamental to a strong security posture.
Works Cited
- “API Security Best Practices.” expeditedsecurity.com, https://expeditedsecurity.com/api-security-best-practices-megaguide/
- “API Security Checklist for developers.” github.com, https://github.com/shieldfy/API-Security-Checklist