> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/fastapi/fastapi/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth2 with Scopes

> Implement fine-grained permissions using OAuth2 scopes for role-based access control

OAuth2 scopes allow you to implement fine-grained permissions in your API. Different endpoints can require different scopes, and tokens can have specific sets of permissions.

## What Are OAuth2 Scopes?

Scopes are permissions that define what a token is allowed to do. For example:

* `me:read` - Read user profile information
* `me:write` - Update user profile
* `items:read` - Read items
* `items:write` - Create or update items

<Note>
  The format like `items:read` is a common convention, but OAuth2 treats scopes as opaque strings. You can use any naming scheme: `read_items`, `items.read`, etc.
</Note>

## Defining Scopes

First, define your scopes when creating the OAuth2 scheme:

```python theme={null}
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={
        "me": "Read information about the current user.",
        "items": "Read items.",
    }
)
```

These scopes will appear in the OpenAPI documentation at `/docs`, allowing users to see what permissions they can request.

## SecurityScopes

FastAPI provides a special `SecurityScopes` class to access required scopes in your dependencies:

```python theme={null}
from fastapi import Security
from fastapi.security import SecurityScopes

async def get_current_user(
    security_scopes: SecurityScopes,
    token: str = Depends(oauth2_scheme)
):
    # security_scopes.scopes contains all required scopes
    # security_scopes.scope_str is all scopes as a space-separated string
    ...
```

<Tip>
  `SecurityScopes` is automatically populated by FastAPI based on all the scopes required by the dependency chain. It's a special parameter like `Request` or `Response`.
</Tip>

## Token with Scopes

Update your token model to include scopes:

```python theme={null}
from pydantic import BaseModel

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: str | None = None
    scopes: list[str] = []
```

## Creating Tokens with Scopes

Modify `create_access_token` to include scopes:

```python theme={null}
def create_access_token(
    data: dict,
    expires_delta: timedelta | None = None
):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt
```

In your login endpoint, include the requested scopes:

```python theme={null}
@app.post("/token")
async def login_for_access_token(
    form_data: OAuth2PasswordRequestForm = Depends(),
) -> Token:
    user = authenticate_user(
        fake_users_db,
        form_data.username,
        form_data.password
    )
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={
            "sub": user.username,
            "scope": " ".join(form_data.scopes)  # Include scopes in token
        },
        expires_delta=access_token_expires
    )
    return Token(access_token=access_token, token_type="bearer")
```

## Validating Scopes

Update `get_current_user` to validate scopes:

```python theme={null}
async def get_current_user(
    security_scopes: SecurityScopes,
    token: str = Depends(oauth2_scheme)
):
    # Build WWW-Authenticate header with required scopes
    if security_scopes.scopes:
        authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
    else:
        authenticate_value = "Bearer"
    
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": authenticate_value},
    )
    
    try:
        # Decode the JWT
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        
        # Extract scopes from token
        scope: str = payload.get("scope", "")
        token_scopes = scope.split(" ")
        token_data = TokenData(scopes=token_scopes, username=username)
    except (InvalidTokenError, ValidationError):
        raise credentials_exception
    
    # Get user from database
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    
    # Check that the token has all required scopes
    for scope in security_scopes.scopes:
        if scope not in token_data.scopes:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Not enough permissions",
                headers={"WWW-Authenticate": authenticate_value},
            )
    
    return user
```

<Steps>
  <Step title="Build authentication header">
    Include required scopes in the `WWW-Authenticate` header
  </Step>

  <Step title="Decode token">
    Extract username and scopes from the JWT
  </Step>

  <Step title="Get user">
    Retrieve user from database
  </Step>

  <Step title="Check scopes">
    Verify the token has all required scopes for this operation
  </Step>
</Steps>

## Using Security() with Scopes

Now you can require specific scopes in your endpoints using `Security()` instead of `Depends()`:

```python theme={null}
from fastapi import Security

@app.get("/users/me/", response_model=User)
async def read_users_me(
    current_user: User = Security(get_current_active_user, scopes=["me"])
):
    return current_user

@app.get("/users/me/items/")
async def read_own_items(
    current_user: User = Security(get_current_active_user, scopes=["items"])
):
    return [{"item_id": "Foo", "owner": current_user.username}]

@app.get("/status/")
async def read_system_status(
    current_user: User = Depends(get_current_user)  # No specific scopes required
):
    return {"status": "ok"}
```

<Note>
  `Security()` is just like `Depends()`, but it allows you to specify required scopes. When you use `Security()` with scopes, FastAPI automatically passes them to your dependency through `SecurityScopes`.
</Note>

## Scope Hierarchy

You can also have dependencies that require different scopes:

```python theme={null}
async def get_current_active_user(
    current_user: User = Security(get_current_user, scopes=["me"])
):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

@app.get("/users/me/items/")
async def read_own_items(
    current_user: User = Security(get_current_active_user, scopes=["items"])
):
    return [{"item_id": "Foo", "owner": current_user.username}]
```

In this case, `read_own_items` requires **both** `me` and `items` scopes because:

1. `read_own_items` requires `items` scope
2. `get_current_active_user` requires `me` scope
3. FastAPI combines all required scopes: `["me", "items"]`

## Complete Example with Scopes

<Accordion title="Click to see full code">
  ```python theme={null}
  from datetime import datetime, timedelta, timezone
  import jwt
  from jwt.exceptions import InvalidTokenError
  from pwdlib import PasswordHash
  from fastapi import Depends, FastAPI, HTTPException, Security, status
  from fastapi.security import (
      OAuth2PasswordBearer,
      OAuth2PasswordRequestForm,
      SecurityScopes,
  )
  from pydantic import BaseModel, ValidationError

  SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
  ALGORITHM = "HS256"
  ACCESS_TOKEN_EXPIRE_MINUTES = 30

  fake_users_db = {
      "johndoe": {
          "username": "johndoe",
          "full_name": "John Doe",
          "email": "johndoe@example.com",
          "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc",
          "disabled": False,
      }
  }

  class Token(BaseModel):
      access_token: str
      token_type: str

  class TokenData(BaseModel):
      username: str | None = None
      scopes: list[str] = []

  class User(BaseModel):
      username: str
      email: str | None = None
      full_name: str | None = None
      disabled: bool | None = None

  class UserInDB(User):
      hashed_password: str

  password_hash = PasswordHash.recommended()
  DUMMY_HASH = password_hash.hash("dummypassword")

  oauth2_scheme = OAuth2PasswordBearer(
      tokenUrl="token",
      scopes={
          "me": "Read information about the current user.",
          "items": "Read items.",
      }
  )

  app = FastAPI()

  def verify_password(plain_password, hashed_password):
      return password_hash.verify(plain_password, hashed_password)

  def get_password_hash(password):
      return password_hash.hash(password)

  def get_user(db, username: str):
      if username in db:
          user_dict = db[username]
          return UserInDB(**user_dict)

  def authenticate_user(fake_db, username: str, password: str):
      user = get_user(fake_db, username)
      if not user:
          verify_password(password, DUMMY_HASH)
          return False
      if not verify_password(password, user.hashed_password):
          return False
      return user

  def create_access_token(data: dict, expires_delta: timedelta | None = None):
      to_encode = data.copy()
      if expires_delta:
          expire = datetime.now(timezone.utc) + expires_delta
      else:
          expire = datetime.now(timezone.utc) + timedelta(minutes=15)
      to_encode.update({"exp": expire})
      encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
      return encoded_jwt

  async def get_current_user(
      security_scopes: SecurityScopes,
      token: str = Depends(oauth2_scheme)
  ):
      if security_scopes.scopes:
          authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
      else:
          authenticate_value = "Bearer"
      
      credentials_exception = HTTPException(
          status_code=status.HTTP_401_UNAUTHORIZED,
          detail="Could not validate credentials",
          headers={"WWW-Authenticate": authenticate_value},
      )
      
      try:
          payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
          username: str = payload.get("sub")
          if username is None:
              raise credentials_exception
          scope: str = payload.get("scope", "")
          token_scopes = scope.split(" ")
          token_data = TokenData(scopes=token_scopes, username=username)
      except (InvalidTokenError, ValidationError):
          raise credentials_exception
      
      user = get_user(fake_users_db, username=token_data.username)
      if user is None:
          raise credentials_exception
      
      for scope in security_scopes.scopes:
          if scope not in token_data.scopes:
              raise HTTPException(
                  status_code=status.HTTP_401_UNAUTHORIZED,
                  detail="Not enough permissions",
                  headers={"WWW-Authenticate": authenticate_value},
              )
      
      return user

  async def get_current_active_user(
      current_user: User = Security(get_current_user, scopes=["me"])
  ):
      if current_user.disabled:
          raise HTTPException(status_code=400, detail="Inactive user")
      return current_user

  @app.post("/token")
  async def login_for_access_token(
      form_data: OAuth2PasswordRequestForm = Depends(),
  ) -> Token:
      user = authenticate_user(fake_users_db, form_data.username, form_data.password)
      if not user:
          raise HTTPException(status_code=400, detail="Incorrect username or password")
      
      access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
      access_token = create_access_token(
          data={"sub": user.username, "scope": " ".join(form_data.scopes)},
          expires_delta=access_token_expires,
      )
      return Token(access_token=access_token, token_type="bearer")

  @app.get("/users/me/", response_model=User)
  async def read_users_me(
      current_user: User = Depends(get_current_active_user)
  ):
      return current_user

  @app.get("/users/me/items/")
  async def read_own_items(
      current_user: User = Security(get_current_active_user, scopes=["items"])
  ):
      return [{"item_id": "Foo", "owner": current_user.username}]

  @app.get("/status/")
  async def read_system_status(
      current_user: User = Depends(get_current_user)
  ):
      return {"status": "ok"}
  ```
</Accordion>

## Testing with Scopes

<Steps>
  <Step title="Login with specific scopes">
    In the `/docs` interface, click "Authorize" and you'll see checkboxes for each scope. Select the scopes you want.

    Or with cURL:

    ```bash theme={null}
    curl -X POST "http://localhost:8000/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "username=johndoe&password=secret&scope=me items"
    ```
  </Step>

  <Step title="Test with full permissions">
    With both `me` and `items` scopes, you can access all endpoints:

    ```bash theme={null}
    curl -X GET "http://localhost:8000/users/me/items/" \
      -H "Authorization: Bearer <your-token>"
    ```
  </Step>

  <Step title="Test with limited permissions">
    Get a token with only `me` scope:

    ```bash theme={null}
    curl -X POST "http://localhost:8000/token" \
      -d "username=johndoe&password=secret&scope=me"
    ```

    Now you can access `/users/me/` but not `/users/me/items/`:

    ```bash theme={null}
    # This works
    curl -X GET "http://localhost:8000/users/me/"

    # This returns 401: Not enough permissions
    curl -X GET "http://localhost:8000/users/me/items/"
    ```
  </Step>
</Steps>

## OAuth2 Scopes in OpenAPI

When you define scopes, they automatically appear in your OpenAPI documentation:

* The "Authorize" dialog shows checkboxes for each scope
* Each endpoint shows which scopes it requires
* The OpenAPI JSON includes the security requirements

<Tip>
  This makes your API documentation interactive and helps developers understand what permissions they need for each operation.
</Tip>

## Advanced Scope Patterns

### Resource-Specific Scopes

```python theme={null}
scopes = {
    "users:read": "Read users",
    "users:write": "Create or update users",
    "items:read": "Read items",
    "items:write": "Create or update items",
    "items:delete": "Delete items",
}
```

### Role-Based Scopes

```python theme={null}
scopes = {
    "user": "Regular user access",
    "admin": "Administrator access",
    "superuser": "Super administrator access",
}
```

### Combining Scopes

You can require multiple scopes for a single endpoint:

```python theme={null}
@app.delete("/items/{item_id}")
async def delete_item(
    item_id: int,
    current_user: User = Security(
        get_current_active_user,
        scopes=["items:delete", "admin"]
    )
):
    # Only users with BOTH items:delete AND admin scopes can access this
    return {"message": f"Item {item_id} deleted"}
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Principle of Least Privilege" icon="shield">
    Request only the scopes you need. Don't request all scopes by default.
  </Card>

  <Card title="Clear Scope Names" icon="tag">
    Use descriptive scope names that clearly indicate what permissions they grant.
  </Card>

  <Card title="Document Scopes" icon="book">
    Provide clear descriptions for each scope in your OAuth2 scheme.
  </Card>

  <Card title="Validate on Server" icon="server">
    Always validate scopes on the server. Never trust client-side validation.
  </Card>
</CardGroup>

## Summary

You now have a complete, production-ready authentication system with:

✅ OAuth2 password flow
✅ JWT tokens with expiration
✅ Secure password hashing with Argon2
✅ Fine-grained permissions with scopes
✅ Automatic OpenAPI documentation
✅ Type-safe dependencies

<Warning>
  **Remember**: This is a strong foundation, but security is complex. For production systems:

  * Store secrets in environment variables
  * Use HTTPS only
  * Implement rate limiting
  * Add logging and monitoring
  * Consider refresh tokens for longer sessions
  * Have a security expert review your implementation
</Warning>

## Further Reading

<CardGroup cols={2}>
  <Card title="HTTP Basic Auth" icon="user" href="/advanced/security/http-basic-auth">
    Learn about simpler HTTP Basic authentication
  </Card>

  <Card title="API Keys" icon="key" href="/advanced/security/api-keys">
    Alternative authentication with API keys
  </Card>
</CardGroup>
