Skip to content

feat: Replace BasicAuth with OAuth2 client credentials flow#867

Open
leewc wants to merge 1 commit into
joostlek:mainfrom
leewc:main
Open

feat: Replace BasicAuth with OAuth2 client credentials flow#867
leewc wants to merge 1 commit into
joostlek:mainfrom
leewc:main

Conversation

@leewc

@leewc leewc commented Jun 7, 2026

Copy link
Copy Markdown

Firstly -- Thank you for making this repo and open source. I use it for home assistant because I didn't want to set up my own software defined antenna.

OpenSky Network dropped basic authentication on March 18, 2026 and now exclusively uses OAuth2 client credentials. This updates the authenticate() method to accept client_id and client_secret instead of BasicAuth, with automatic token refresh. (API docs).

Currently, python-opensky uses aiohttp.BasicAuth in authenticate(), which means authenticated requests no longer work. Anonymous access still functions but with reduced rate limits (400 credits/day, 15-minute resolution).

OAuth2 state (credentials + token + expiry) is bundled into a private _OAuthSession dataclass to keep the OpenSky instance attribute count at 8, matching pre-OAuth2.

Ref: openskynetwork/opensky-api#85
Ref: home-assistant/core#156643

Proposed Changes

  • Replace BasicAuth with OAuth2 client credentials token exchange
  • Token endpoint: https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token
  • authenticate() accepts client_id and client_secret instead of BasicAuth
  • Tokens auto-refresh before expiry (30-minute lifetime, 30s refresh margin)
  • Anonymous access unchanged

Related Issues

Additional disclosure

AI was used in the crafting of this PR, however, I personally validated the code and tested it.

Testing Done

Live Integration Test (2026-06-07)

(Note: Once this is merged I can test the HA integration itself)

Test Script

Save as integration_test.py and run from the repo root with uv run python integration_test.py. Authenticated section runs only if OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET are set.

"""Live integration test against the OpenSky API.

Anonymous always runs. Authenticated runs only if OPENSKY_CLIENT_ID and
OPENSKY_CLIENT_SECRET are set.
"""

import asyncio
import os
import sys

from python_opensky import BoundingBox, OpenSky


async def main() -> int:
    seattle = BoundingBox(
        min_latitude=47.0,
        max_latitude=48.0,
        min_longitude=-123.0,
        max_longitude=-122.0,
    )

    print("=== Anonymous ===")
    async with OpenSky() as opensky:
        states = await opensky.get_states(bounding_box=seattle)
        print(f"  is_authenticated: {opensky.is_authenticated}")
        print(f"  credits: {opensky.opensky_credits}")
        print(f"  aircraft found: {len(states.states)}")
        for s in states.states[:3]:
            print(f"    {s.icao24} {s.callsign or '':<8} alt={s.geo_altitude}m")

    client_id = os.environ.get("OPENSKY_CLIENT_ID")
    client_secret = os.environ.get("OPENSKY_CLIENT_SECRET")
    if not (client_id and client_secret):
        print("\n(Skipping authenticated test — set OPENSKY_CLIENT_ID/SECRET to run)")
        return 0

    print("\n=== Authenticated (OAuth2) ===")
    async with OpenSky() as opensky:
        print(f"  before: is_auth={opensky.is_authenticated} credits={opensky.opensky_credits}")
        await opensky.authenticate(client_id=client_id, client_secret=client_secret)
        print(f"  after:  is_auth={opensky.is_authenticated} credits={opensky.opensky_credits}")
        states = await opensky.get_states(bounding_box=seattle)
        print(f"  remaining credits: {opensky.remaining_credits()}")
        print(f"  aircraft found: {len(states.states)}")
        for s in states.states[:3]:
            print(f"    {s.icao24} {s.callsign or '':<8} alt={s.geo_altitude}m")

    return 0


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))

Output (live OpenSky API, Seattle bounding box)

=== Anonymous ===
  is_authenticated: False
  credits: 400
  aircraft found: 14
    abae62 SWA286   alt=3779.52m
    a44213 UAL2379  alt=1501.14m
    ad2ea1 ASA727   alt=2750.82m

=== Authenticated (OAuth2) ===
  before: is_auth=False credits=400
  after:  is_auth=True credits=4000
  remaining credits: 3998
  aircraft found: 14
    abae62 SWA286   alt=3779.52m
    a44213 UAL2379  alt=1501.14m
    ad2ea1 ASA727   alt=2750.82m

Observations

  • Anonymous tier: 400 credits/day, request succeeds without auth
  • Authenticated tier: 4000 credits/day (10× anonymous), confirms paid client tier
  • Token exchange: POST to https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token returns access token; library transitions is_authenticated False → True
  • Bearer auth: subsequent /api/states/all request uses Authorization: Bearer <token> header (replaces removed BasicAuth)
  • Credit accounting: remaining_credits() returns 3998 — bounding box query costs 1 credit and is counted twice in this test (once in the authenticate roundtrip, once in the explicit get_states call). Matches expected behavior.

Local Quality Gates

$ uv run ruff check src tests
All checks passed!

$ uv run pytest -q
27 passed in 2.25s
Required test coverage of 100% reached. Total coverage: 100.00%

$ uv run mypy src
Success: no issues found in 6 source files

OpenSky Network dropped basic authentication on March 18, 2026
and now exclusively uses OAuth2 client credentials. This updates
the authenticate() method to accept client_id and client_secret
instead of BasicAuth, with automatic token refresh.

OAuth2 state (credentials + token + expiry) is bundled into a private
_OAuthSession dataclass to keep the OpenSky instance attribute count
at 8, matching pre-OAuth2.

Ref: openskynetwork/opensky-api#85
Ref: home-assistant/core#156643
@leewc leewc marked this pull request as draft June 7, 2026 07:34
@leewc leewc marked this pull request as ready for review June 7, 2026 07:35
@leewc

leewc commented Jun 7, 2026

Copy link
Copy Markdown
Author

^ Tried adding labels but it appears I do not have permissions to.. this is an enhancement/new feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant