Skip to main content

plain.oauth

Let users log in with OAuth providers like GitHub, Google, and more.

Overview

This package provides a minimal OAuth integration with no dependencies and a single database model. You can let users sign up and log in with GitHub, Google, Twitter, or any other OAuth provider.

Three OAuth flows are supported:

  1. Signup - new user, new OAuth connection
  2. Login - existing user, existing OAuth connection
  3. Connect/disconnect - existing user linking or unlinking an OAuth account

Here is a complete example showing GitHub OAuth login.

# app/oauth.py
import httpx

from plain.oauth.providers import OAuthProvider, OAuthToken, OAuthUser


class GitHubOAuthProvider(OAuthProvider):
    authorization_url = "https://github.com/login/oauth/authorize"

    def get_oauth_token(self, *, code, request):
        response = httpx.post(
            "https://github.com/login/oauth/access_token",
            headers={"Accept": "application/json"},
            data={
                "client_id": self.get_client_id(),
                "client_secret": self.get_client_secret(),
                "code": code,
            },
        )
        response.raise_for_status()
        data = response.json()
        return OAuthToken(access_token=data["access_token"])

    def get_oauth_user(self, *, oauth_token):
        response = httpx.get(
            "https://api.github.com/user",
            headers={
                "Accept": "application/json",
                "Authorization": f"token {oauth_token.access_token}",
            },
        )
        response.raise_for_status()
        data = response.json()
        return OAuthUser(
            provider_id=data["id"],
            user_model_fields={
                "email": data["email"],
                "username": data["login"],
            },
        )

    def refresh_oauth_token(self, *, oauth_token):
        # GitHub tokens don't expire by default
        return oauth_token

Configure the provider in your settings:

# app/settings.py
OAUTH_LOGIN_PROVIDERS = {
    "github": {
        "class": "app.oauth.GitHubOAuthProvider",
        "kwargs": {
            "client_id": environ["GITHUB_CLIENT_ID"],
            "client_secret": environ["GITHUB_CLIENT_SECRET"],
            # "scope" is optional, defaults to ""
        },
    },
}

Add a login button in your template:

<form action="{{ url('oauth:login', provider='github') }}" method="post">
    <button type="submit">Login with GitHub</button>
</form>

The provider name in the URL ('github') must match the key in OAUTH_LOGIN_PROVIDERS. Your callback URL will be https://yoursite.com/oauth/github/callback/.

Creating a provider

You need to subclass OAuthProvider and implement three methods:

  • get_oauth_token - exchanges an authorization code for an access token
  • get_oauth_user - fetches user information using the access token
  • refresh_oauth_token - refreshes an expired access token (return the same token if the provider does not support refresh)

Set the authorization_url class attribute to the provider's OAuth authorization endpoint.

The OAuthToken class accepts these fields:

OAuthToken(
    access_token="...",
    refresh_token="",  # optional
    access_token_expires_at=None,  # optional datetime
    refresh_token_expires_at=None,  # optional datetime
)

The OAuthUser class requires a provider_id and an optional dict of fields to set on your User model:

OAuthUser(
    provider_id="12345",  # unique ID on the provider's system
    user_model_fields={
        "email": "user@example.com",
        "username": "example_user",
    },
)

Connecting and disconnecting accounts

Authenticated users can connect additional OAuth providers or disconnect existing ones. Add forms to a settings page:

<h2>Connected accounts</h2>
<ul>
    {% for connection in get_current_user().oauth_connections.all %}
    <li>
        {{ connection.provider_key }}
        <form action="{{ url('oauth:disconnect', provider=connection.provider_key) }}" method="post">
            <input type="hidden" name="provider_user_id" value="{{ connection.provider_user_id }}">
            <button type="submit">Disconnect</button>
        </form>
    </li>
    {% endfor %}
</ul>

<h2>Add a connection</h2>
<ul>
    {% for provider_key in oauth_provider_keys %}
    <li>
        <form action="{{ url('oauth:connect', provider=provider_key) }}" method="post">
            <button type="submit">Connect {{ provider_key }}</button>
        </form>
    </li>
    {% endfor %}
</ul>

Use get_provider_keys to populate the list of available providers:

from plain.oauth.providers import get_provider_keys
from plain.views import TemplateView


class SettingsView(TemplateView):
    template_name = "settings.html"

    def get_template_context(self):
        context = super().get_template_context()
        context["oauth_provider_keys"] = get_provider_keys()
        return context

Using saved access tokens

The OAuthConnection model stores token data for each connected provider. You can use stored tokens to make API calls on behalf of users.

# Get the connection for a user
connection = user.oauth_connections.get(provider_key="github")

# Check if the token has expired and refresh it
if connection.access_token_expired():
    connection.refresh_access_token()

# Use the token
response = httpx.get(
    "https://api.github.com/user/repos",
    headers={"Authorization": f"token {connection.access_token}"},
)

Customizing the provider

The OAuthProvider class has several methods you can override:

  • get_authorization_url_params - customize the OAuth authorization URL parameters
  • get_login_redirect_url - change where users are redirected after login
  • get_disconnect_redirect_url - change where users are redirected after disconnecting
  • login - customize the login process (uses plain.auth by default)

Settings

Setting Default Env var
OAUTH_LOGIN_PROVIDERS Required PLAIN_OAUTH_LOGIN_PROVIDERS (JSON)

This setting is marked as Secret, so its values are masked in logs. See default_settings.py for more details.

FAQs

How is this different from other OAuth libraries?

This library does less. Popular alternatives like django-allauth provide features like email verification, multiple email addresses, and dozens of pre-configured providers. That adds complexity you may not need. This library focuses on the core OAuth flow with a single database model and no extra dependencies.

Why are providers not included in the library?

Providers are straightforward to implement. You find two OAuth URLs in the provider's docs and write two methods to fetch tokens and user data. This approach means you can fix issues immediately without waiting for upstream updates, and you can customize the implementation for your specific needs.

What if there is a redirect URL mismatch in local development?

If you are using a proxy like ngrok, the callback URL might be built as http instead of https. Add this to your settings:

HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"

What does the preflight check do?

A preflight check warns you if a provider key exists in your database but is missing from your OAUTH_LOGIN_PROVIDERS setting. This prevents errors when users try to use a provider that has been removed from your configuration.

Installation

Install the package from PyPI:

uv add plain.oauth

Add plain.oauth to your INSTALLED_PACKAGES:

# app/settings.py
INSTALLED_PACKAGES = [
    # ...
    "plain.oauth",
]

Include the OAuthRouter in your URLs:

# app/urls.py
from plain.oauth.urls import OAuthRouter
from plain.urls import Router, include


class AppRouter(Router):
    namespace = ""
    urls = (
        include("oauth/", OAuthRouter),
        # ...
    )

Sync the database:

plain postgres sync

Create an OAuth app on your provider's site (GitHub, Google, etc.) and note the client ID and client secret. Set the callback URL to match your configuration, for example http://localhost:8000/oauth/github/callback/ for local development.

Create a provider class following the Overview example and configure OAUTH_LOGIN_PROVIDERS in your settings. Add login buttons to your templates and you are ready to go.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plain_oauth-0.50.4.tar.gz (26.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plain_oauth-0.50.4-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file plain_oauth-0.50.4.tar.gz.

File metadata

  • Download URL: plain_oauth-0.50.4.tar.gz
  • Upload date:
  • Size: 26.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plain_oauth-0.50.4.tar.gz
Algorithm Hash digest
SHA256 a54de7fdc160b6e595510ffbe372856b1193d5f8b4d8a8fa136eb6986a477148
MD5 6fdf9145248d4a45c63610b512fd7bad
BLAKE2b-256 7d3e8b2166a0fb85841d21d5b5ee357bfee429223b1e366e135fca7274296027

See more details on using hashes here.

File details

Details for the file plain_oauth-0.50.4-py3-none-any.whl.

File metadata

  • Download URL: plain_oauth-0.50.4-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for plain_oauth-0.50.4-py3-none-any.whl
Algorithm Hash digest
SHA256 77940179254395dffd3701a3376de9aaf111ba2f362f8f39731a1a525771e9c6
MD5 870cc2c7170b9e67c282cc7c1265384b
BLAKE2b-256 648cfc32f03202f4fad89f9a1dd27155adb83d5a62976e35824132981c19de19

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.50.4 This release

2 files

0.50.3

2 files

0.50.2

2 files

0.50.1

2 files

0.50.0

2 files

0.49.8

2 files

0.49.7

2 files

0.49.6

2 files

0.49.5

2 files

0.49.4

2 files

0.49.3

2 files

0.49.2

2 files

0.49.1

2 files

0.49.0

2 files

0.48.0

2 files

0.47.0

2 files

0.46.0

2 files

0.45.0

2 files

0.44.3

2 files

0.44.2

2 files

0.44.1

2 files

0.44.0

2 files

0.43.0

2 files

0.42.4

2 files

0.42.3

2 files

0.42.2

2 files

0.42.1

2 files

0.42.0

2 files

0.41.0

2 files

0.40.0

2 files

0.39.0

2 files

0.38.0

2 files

0.37.0

2 files

0.36.1

2 files

0.36.0

2 files

0.35.0

2 files

0.34.0

2 files

0.33.0

2 files

0.32.1

2 files

0.32.0

2 files

0.31.2

2 files

0.31.1

2 files

0.31.0

2 files

0.30.0

2 files

0.29.2

2 files

0.29.1

2 files

0.29.0

2 files

0.28.0

2 files

0.27.0

2 files

0.26.0

2 files

0.25.1

2 files

0.25.0

2 files

0.24.2

2 files

0.24.1

2 files

0.24.0

2 files

0.23.0

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.19.1

2 files

0.19.0

2 files

0.18.0

2 files

0.17.0

2 files

0.16.1

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.1

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

0.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page