From 2d66ac96ab220a374e1bcbddc823b341f842b907 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Mon, 8 Jun 2026 22:36:02 +0530 Subject: [PATCH] feat: add Reddit OAuth2 bundled provider --- .../auth/bundled_providers/reddit.json | 24 +++++++++++++++++++ src/authsome/auth/flows/base.py | 11 ++++++--- src/authsome/auth/flows/device_code.py | 18 ++++++++++---- src/authsome/auth/flows/pkce.py | 13 +++++++--- src/authsome/auth/models/provider.py | 2 ++ uv.lock | 2 +- 6 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 src/authsome/auth/bundled_providers/reddit.json diff --git a/src/authsome/auth/bundled_providers/reddit.json b/src/authsome/auth/bundled_providers/reddit.json new file mode 100644 index 00000000..89b7b01b --- /dev/null +++ b/src/authsome/auth/bundled_providers/reddit.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "name": "reddit", + "display_name": "Reddit", + "logo": "img.logo.dev/name/reddit", + "description": "Authenticate with Reddit API.", + "auth_type": "oauth2", + "flow": "pkce", + "oauth": { + "base_url": "https://oauth.reddit.com", + "authorization_url": "https://www.reddit.com/api/v1/authorize", + "token_url": "https://www.reddit.com/api/v1/access_token", + "authorization_method": "basic", + "authorization_params": { + "duration": "permanent" + }, + "_comment": "Reddit API strictly requires HTTP Basic Auth for token exchanges rather than payload credentials. See: https://github.com/reddit-archive/reddit/wiki/OAuth2#retrieving-the-access-token" + }, + "api_url": "https://oauth.reddit.com", + "docs_url": "https://github.com/reddit-archive/reddit/wiki/OAuth2", + "export": { + "REDDIT_ACCESS_TOKEN": "ACCESS_TOKEN" + } +} diff --git a/src/authsome/auth/flows/base.py b/src/authsome/auth/flows/base.py index aeb8c6f3..3e580fdf 100644 --- a/src/authsome/auth/flows/base.py +++ b/src/authsome/auth/flows/base.py @@ -133,15 +133,20 @@ def refresh( payload: dict[str, str] = { "grant_type": "refresh_token", "refresh_token": record.refresh_token, - "client_id": client_id, } - if client_secret: - payload["client_secret"] = client_secret + auth: tuple[str, str] | None = None + if provider.oauth.authorization_method == "basic": + auth = (client_id, client_secret or "") + else: + payload["client_id"] = client_id + if client_secret: + payload["client_secret"] = client_secret resp = http_client.post( provider.oauth.token_url, data=payload, headers={"Accept": "application/json"}, + auth=auth, timeout=30, ) resp.raise_for_status() diff --git a/src/authsome/auth/flows/device_code.py b/src/authsome/auth/flows/device_code.py index c1b57b90..fc4436c0 100644 --- a/src/authsome/auth/flows/device_code.py +++ b/src/authsome/auth/flows/device_code.py @@ -195,12 +195,20 @@ async def poll_for_token( # noqa: PLR0912, PLR0913 "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code, } - if client_id: - payload["client_id"] = client_id - if client_secret: - payload["client_secret"] = client_secret + auth: tuple[str, str] | None = None + if provider.oauth.authorization_method == "basic": + auth = (client_id or "", client_secret or "") + else: + if client_id: + payload["client_id"] = client_id + if client_secret: + payload["client_secret"] = client_secret resp = requests.post( - provider.oauth.token_url, data=payload, headers={"Accept": "application/json"}, timeout=30 + provider.oauth.token_url, + data=payload, + headers={"Accept": "application/json"}, + auth=auth, + timeout=30, ) except requests.RequestException as exc: logger.warning("Token poll request failed: {}, retrying...", exc) diff --git a/src/authsome/auth/flows/pkce.py b/src/authsome/auth/flows/pkce.py index e07f475b..09ea27cf 100644 --- a/src/authsome/auth/flows/pkce.py +++ b/src/authsome/auth/flows/pkce.py @@ -46,6 +46,7 @@ async def begin( # noqa: PLR0913 state = secrets.token_urlsafe(32) auth_params: dict[str, str] = { + **provider.oauth.authorization_params, "response_type": "code", "client_id": client_id, "redirect_uri": redirect_uri, @@ -147,17 +148,23 @@ async def _exchange_code( # noqa: PLR0913 "grant_type": "authorization_code", "code": auth_code, "redirect_uri": redirect_uri, - "client_id": client_id, "code_verifier": code_verifier, } - if client_secret: - payload["client_secret"] = client_secret + + auth: tuple[str, str] | None = None + if provider.oauth.authorization_method == "basic": + auth = (client_id, client_secret or "") + else: + payload["client_id"] = client_id + if client_secret: + payload["client_secret"] = client_secret try: resp = http_client.post( provider.oauth.token_url, data=payload, headers={"Accept": "application/json"}, + auth=auth, timeout=30, ) resp.raise_for_status() diff --git a/src/authsome/auth/models/provider.py b/src/authsome/auth/models/provider.py index a8a626fb..51f76bd7 100644 --- a/src/authsome/auth/models/provider.py +++ b/src/authsome/auth/models/provider.py @@ -19,10 +19,12 @@ class OAuthConfig(BaseModel): #: ``json`` = poll token URL with ``POST`` JSON body ``{"device_code": "..."}`` (e.g. Postiz CLI auth). device_token_request: Literal["oauth2_form", "json"] = "oauth2_form" scopes: list[str] = Field(default_factory=list) + authorization_params: dict[str, str] = Field(default_factory=dict) pkce: bool = True supports_device_code: bool = False supports_dcr: bool = False base_url: str | None = None + authorization_method: Literal["body", "basic"] = "body" model_config = {"extra": "allow"} diff --git a/uv.lock b/uv.lock index f20928d0..ab47b84d 100644 --- a/uv.lock +++ b/uv.lock @@ -162,7 +162,7 @@ wheels = [ [[package]] name = "authsome" -version = "0.6.0" +version = "0.6.1" source = { editable = "." } dependencies = [ { name = "aiosqlite" },