Simple yet flexible OAuth 2.0 device flow authentication for Python
Project description
loctocat
loctocat brings simple yet flexible OAuth 2.0 device flow authentication to Python. It has built-in asyncio support and even predefined authenticators for popular services. Plus, it's fully compliant with RFC 8628, making it compatible with any OAuth2-supporting service that (correctly) implements the standard.
Installation
pip install loctocat
Basic Usage
The Authenticator
Class
Every authentication flow starts with loctocat's Authenticator
class.
from loctocat import Authenticator
authenticator = Authenticator(
client_id="your_client_id",
auth_url="https://example.com/oauth2/authorize",
token_url="https://example.com/oauth2/token",
scopes=["list", "of", "scopes"],
)
It's pretty simple — just instantiate the class with your client ID, authorization URL (where you'll get your device and user codes), token URL (where you'll poll the authorization server for an access token), and a list of any scopes you need.
Once you've got an Authenticator
, getting an access token is as simple as:
token = authenticator.authenticate()
Whoa. That was easy.
Authenticator.authenticate()
will, in order:
- Obtain device and user codes from the authorization server
- Prompt the user to visit the verficiation URL and enter the user code
- Poll the authorization server for an access token
- Return the access token as a string
Here's an example of using Authenticator
to authenticate with GitHub:
# https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flow
from loctocat import Authenticator
authenticator = Authenticator(
client_id="github_client_id", # Replace this with your app's actual client ID, obviously.
auth_url="https://github.com/login/device/code",
token_url="https://github.com/login/oauth/access_token",
scopes=["repo"], # https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
)
token = authenticator.authenticate()
Like I said, easy. Unless you're building an asynchronous application, in which case this doesn't work at all, since
Authenticator.authenticate()
is a blocking call. Fortunately, loctocat has you covered.
The AsyncAuthenticator
Class
AsyncAuthenticator
is a subclass of Authenticator
that functions exactly the same except all its methods are
asynchronous (and therefore must be called with await
). Here's an example of AsyncAuthenticator
in action:
from loctocat import AsyncAuthenticator
authenticator = AsyncAuthenticator(
client_id="your_client_id",
auth_url="https://example.com/oauth2/authorize",
token_url="https://example.com/oauth2/token",
scopes=["list", "of", "scopes"],
)
token = await authenticator.authenticate()
Whoa. That was easy.
AsyncAuthenticator.authenticate()
will, in or—wait, I'm getting déjà vu.
Advanced Usage
Maybe Authenticator.authenticate()
is too simplistic for you. Maybe you'd rather, I don't know,
handle the user-facing authentication prompt yourself, or control when loctocat starts polling for access tokens.
Fortunately, locotcat has you covered.
(Keep in mind that AsyncAuthenticator
is a subclass of Authenticator
and inherits all of its methods and
attributes.)
Authenticator.ping()
Authenticator.ping()
requests device and user codes from the authorization server, returning a LoctocatAuthInfo
object that looks like this:
class LoctocatAuthInfo:
device_code: str
user_code: str
verification_uri: str
expires_in: int
interval: int
Pretty self-explanatory. You can do whatever you want with this information (aside from change it
— LoctocatAuthInfo
's attributes are read-only). For example, you could prompt the user with some custom text
containing the user code and verification URI:
auth_info = authenticator.ping()
print(f"Check it out, yo! This is some epic text telling YOU to go {auth_info.verification_uri} and enter {auth_info.user_code}! Swag!")
Authenticator.poll()
Authenticator.poll()
polls the authorization server for an access token, returning it as a string. You don't
need to pass the LoctocatAuthInfo
object returned by Authenticator.ping()
to Authenticator.poll()
— the
authorization info is automatically remembered by Authenticator
. All you have to do is call the method:
# Authenticator.ping() must have been called on the Authenticator object already or this will not work.
token = authenticator.poll()
Whoa. That was easy.
(Fun fact: Authenticator.authenticate()
is just a wrapper around ping()
and poll()
.)
Pro Usage
Maybe Advanced Usage isn't advanced enough for you. Maybe you're working with an authorization
server that requires parameters beyond those defined by Authenticator
. Maybe you want to customize the prompts and
messages displayed by Authenticator.authenticate()
without having to use Authenticator.ping()
and
Authenticator.poll()
. Maybe loctocat has a predefined authenticator for a service you like, and you want to use it.
Unfortunately, loctocat doesn't have you covered.
...
Okay, loctocat actually does have you covered, but that stuff is the domain of loctocat's unfinished documentation site. Emphasis on unfinished. It's not finished yet.
Fortunately, loctocat has you covered. loctocat's a pretty small library and it's public modules and classes are all properly documented in the source code, so you're welcome to learn by example(?) and take a look around.
Or you could wait until I finish the documentation site. I'm not your mother.
FAQ
Maybe you have questions about loctocat that haven't been answered by the rest of this README. Maybe you just want to see me talk to myself for like, two paragraphs. Fortunately, loctocat has you covered.
Q: loctowhat now
A: Lock + Octocat. loctocat was born out of my need for a Python library that implemented OAuth 2.0 device flow authentication for GitHub.
Q: pretty sure I can do this with requests-oauthlib or [INSERT OAUTH LIBRARY HERE] just fine dude
A: Sure you can. In fact, loctocat uses requests and oauthlib under the hood. So let's leave loctocat behind and write a function to authenticate with GitHub using requests and oauthlib, together!
import time
import requests
from oauthlib.oauth2 import DeviceClient
def authenticate_with_github(client_id: str, scopes: list[str]) -> str:
auth_url = "https://github.com/login/device/code"
token_url = "https://github.com/login/oauth/access_token"
client = DeviceClient(client_id=client_id, scope=scopes)
ping_uri = client.prepare_request_uri(auth_url)
response = requests.post(ping_uri, headers={"Accept": "application/json"}).json()
poll_uri = client.prepare_request_uri(token_url, code=response["device_code"])
while True:
response = requests.post(poll_uri, headers={"Accept": "application/json"}).json()
if "error" in response:
if response["error"] in ["authorization_pending", "slow_down"]:
time.sleep(response["interval"])
continue
else:
raise RuntimeError(response["error"])
else:
return response["access_token"]
Damn, that plate can boil!
Now let's do the same thing, but with loctocat.
from loctocat.predefined import GitHubAuthenticator
def authenticate_with_github(client_id: str, scopes: list[str]) -> str:
authenticator = GitHubAuthenticator(client_id=client_id, scopes=scopes)
return authenticator.authenticate()
# "Hey, that's cheating!" Fine, let's do it the hard way.
from loctocat import Authenticator
def authenticate_with_github(client_id: str, scopes: list[str]) -> str:
authenticator = Authenticator(
client_id=client_id,
auth_url="https://github.com/login/device/code",
token_url="https://github.com/login/oauth/access_token",
scopes=scopes
)
return authenticator.authenticate()
Whoa. That was easy.
Part of the reason I made loctocat is that no other library capable of doing what loctocat does does it in a way that doesn't SUCK. The first example SUCKS. The second example is AWESOME. Case closed.
Q: loctocat isn't working with [INSERT SERVICE HERE] and I'm FRUSTRATED AAAAGGGGGGHHHHH
A: loctocat is compliant with the OAuth 2.0 Device Authorization Grant standard so it's probably the service's fault. Make sure the service actually does support the device flow and is generally compliant with RFC 8628. If you're sure loctocat is the problem, open an issue.
Q: oh my god thank you I've been looking for a library like this forever you have no idea
A: You're very welcome. 🙂
License
In an age where developers must take great caution not to tread on the intellectual property of others, you must be hoping that a such an incredible library is made available under a permissive license. Fortunately, loctocat has you covered.
loctocat is licensed under the MIT License.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file loctocat-1.0.3.tar.gz
.
File metadata
- Download URL: loctocat-1.0.3.tar.gz
- Upload date:
- Size: 12.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.11.0 Darwin/21.6.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ddb1131c5d8c3564f9f3eda074413c70fa82c0fd3dcb6a15401d0ffea980af48 |
|
MD5 | d5b6f8196b8d3ba5e039d2ce2a80b90a |
|
BLAKE2b-256 | ebb38adb71fe77384c22851c73f04ee4951ea7f45a4762da818d90dbeffc5ebc |
File details
Details for the file loctocat-1.0.3-py3-none-any.whl
.
File metadata
- Download URL: loctocat-1.0.3-py3-none-any.whl
- Upload date:
- Size: 14.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.11.0 Darwin/21.6.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 903285c98ad3b806735a5b5dca2c65c084caffb8e5ffdb8817b4371de47633e0 |
|
MD5 | 823c808cfe13e3c7c6fb0786458502f5 |
|
BLAKE2b-256 | 9e384b628515c740dd31b32f7704a1d69392b19f2e424068eec705ffbefa6314 |