Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

Understanding OAuth 2.0 Scopes and OpenID Connect Claims in a Practical Tutorial

This tutorial shows how to define OAuth 2.0 scopes, obtain tokens, and read OpenID Connect claims using ID tokens or the UserInfo endpoint.

Focus: OAuth 2.0 scopes

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

OAuth 2.0 Scopes and OpenID Connect Claims: A Practical Tutorial

Overview

What is OAuth 2.0?

OAuth 2.0 is an industry‑standard authorization framework that lets third‑party applications obtain limited access to user resources without handling passwords. It works by issuing short‑lived access tokens after the user authorizes the request.

What is OpenID Connect (OIDC)?

OpenID Connect sits on top of OAuth 2.0 and adds a layer of authentication. After the authorization step, the client receives an ID token (a JWT) that contains claims about the user’s identity, such as sub, email, and name.

Requesting Permissions (Scopes)

Defining Scopes

A scope is a string that describes a specific permission, e.g., read, write, profile. The authorization server documents which scopes it supports.

Using Scopes in the Authorization Request

When the client redirects the user to the authorization endpoint, it includes a scope parameter. The user sees a consent screen listing the requested scopes.

Code snippet
# Example: building the authorization URL
import urllib.parse

base_url = "https://auth.example.com/authorize"
params = {
    "response_type": "code",
    "client_id": "my_client",
    "redirect_uri": "https://myapp.com/callback",
    "scope": "openid profile email",
    "state": "random_state_123"
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
print(url)

Example: requesting read and write scopes

If the app needs to read user data and create new records, it requests read and write scopes:

Code snippet
scope=openid%20profile%20email%20read%20write

Handling Scope Errors

If the user denies a scope, the authorization server returns an error code (e.g., access_denied). The client must handle this gracefully, often by informing the user why the requested feature isn’t available.

Getting User Information (Claims)

ID Token Claims

The ID token is a signed JWT that contains claims. Common claims include:

  • sub – subject (unique user ID)
  • iss – issuer (who issued the token)
  • aud – audience (client that should accept the token)
  • email – user’s email address
  • name – full name
  • exp – expiration time

Access Token and UserInfo Endpoint

An access token may grant API access, but it does not directly expose user details. OIDC provides a UserInfo endpoint (/userinfo) that returns a JSON payload with claims, using the access token as a bearer token.

Code snippet
import requests

token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
userinfo_url = "https://auth.example.com/userinfo"

response = requests.get(
    userinfo_url,
    headers={"Authorization": f"Bearer {token}"}
)
userinfo = response.json()
print(userinfo)
# Example output:
# {
#   "sub": "12345",
#   "name": "Alice Example",
#   "email": "alice@example.com",
#   "email_verified": true,
#   "iat": 1727251200,
#   "exp": 1727337600
# }

Decoding the ID Token (Python)

If you prefer to decode the JWT yourself, you can use the pyjwt library. Remember to verify the signature with the server’s public key.

Code snippet
import jwt
import requests

# Fetch the JWKS (JSON Web Key Set) from the discovery document
jwks_response = requests.get("https://auth.example.com/.well-known/openid-configuration")
jwks = jwks_response.json()["jwks"]

def get_signing_key(token):
    header = jwt.get_unverified_header(token)
    key_id = header["kid"]
    for key in jwks["keys"]:
        if key["kid"] == key_id:
            return jwt.algorithms.RSAAlgorithm.from_jwk(key)
    raise ValueError("Unable to find signing key")

token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
key = get_signing_key(token)
payload = jwt.decode(token, key, algorithms=["RS256"])
print(payload)
# Output includes `sub`, `email`, `name`, etc.

Putting It All Together – A Real‑World Flow

Step‑by‑step example with Flask

Below is a minimal Flask app that performs the OAuth 2.0 Authorization Code flow, exchanges the code for tokens, and reads user info.

Code snippet
from flask import Flask, redirect, request, session
import requests
import urllib.parse

app = Flask(__name__)
app.secret_key = "super_secret_key"

# Configuration (replace with real values)
AUTH_URL = "https://auth.example.com/authorize"
TOKEN_URL = "https://auth.example.com/token"
REDIRECT_URI = "https://myapp.com/callback"
CLIENT_ID = "my_client"
CLIENT_SECRET = "client_secret"

@app.route("/login")
def login():
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "openid profile email",
        "state": "random_state"
    }
    return redirect(f"{AUTH_URL}?{urllib.parse.urlencode(params)}")

@app.route("/callback")
def callback():
    error = request.args.get("error")
    if error:
        return f"Error: {error}"

    code = request.args.get("code")
    token_data = {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
    token_resp = requests.post(TOKEN_URL, data=token_data)
    tokens = token_resp.json()
    session["access_token"] = tokens["access_token"]
    session["id_token"] = tokens["id_token"]
    return redirect("/profile")

@app.route("/profile")
def profile():
    id_token = session.get("id_token")
    if not id_token:
        return "Not logged in"

    # Decode ID token (same helper as earlier)
    header = jwt.get_unverified_header(id_token)
    jwks = requests.get("https://auth.example.com/.well-known/openid-configuration").json()["jwks"]
    key = jwt.algorithms.RSAAlgorithm.from_jwk(next(k for k in jwks["keys"] if k["kid"] == header["kid"]))
    claims = jwt.decode(id_token, key, algorithms=["RS256"])
    return f"Hello {claims['name']} ({claims['email']})"

if __name__ == "__main__":
    app.run(debug=True)

Security considerations

  • Never expose client_secret in public code.
  • Validate the state parameter to prevent CSRF.
  • Check token expiration (exp claim) before using it.
  • Prefer the UserInfo endpoint over parsing the ID token directly for sensitive data, because the endpoint can enforce additional policies.

Summary

OAuth 2.0 scopes define what a client may request from a user, and the authorization request includes them in the scope parameter. OpenID Connect adds an ID token whose JWT‑encoded claims (e.g., sub, email, name) provide authenticated user information. After obtaining an access token, you can either decode the ID token yourself or call the UserInfo endpoint for a consistent, server‑verified profile. A typical flow involves redirecting the user to the authorization endpoint, handling the redirect with the authorization code, exchanging it for tokens, and finally reading user data securely. Following best practices — state handling, token expiration checks, and using the UserInfo endpoint — ensures a robust and user‑friendly authentication experience for junior developers building real‑world applications.

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.