Package: loctocat
loctocatloctocat 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 withRFC 8628, making it compatible with any OAuth2-supporting service that
(correctly) implements the standard.InstallationpipinstallloctocatBasic UsageTheAuthenticatorClassEvery authentication flow starts with loctocat'sAuthenticatorclass.fromloctocatimportAuthenticatorauthenticator=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 anAuthenticator, 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 serverPrompt the user to visit the verficiation URL and enter the user codePoll the authorization server for an access tokenReturn the access token as a stringHere's an example of usingAuthenticatorto authenticate with GitHub:# https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flowfromloctocatimportAuthenticatorauthenticator=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, sinceAuthenticator.authenticate()is a blocking call. Fortunately, loctocat has you covered.TheAsyncAuthenticatorClassAsyncAuthenticatoris a subclass ofAuthenticatorthat functions exactly the same except all its methods are
asynchronous (and therefore must be called withawait). Here's an example ofAsyncAuthenticatorin action:fromloctocatimportAsyncAuthenticatorauthenticator=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=awaitauthenticator.authenticate()Whoa. That was easy.AsyncAuthenticator.authenticate()will, in or—wait, I'm getting déjà vu.Advanced UsageMaybeAuthenticator.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 thatAsyncAuthenticatoris a subclass ofAuthenticatorand inherits all of its methods and
attributes.)Authenticator.ping()Authenticator.ping()requests device and user codes from the authorization server, returning aLoctocatAuthInfoobject that looks like this:classLoctocatAuthInfo:device_code:struser_code:strverification_uri:strexpires_in:intinterval:intPretty 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. Youdon'tneed to pass theLoctocatAuthInfoobject returned byAuthenticator.ping()toAuthenticator.poll()— the
authorization info is automatically remembered byAuthenticator. 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 aroundping()andpoll().)Pro UsageMaybeAdvanced Usageisn't advanced enough for you. Maybe you're working with an authorization
server that requires parameters beyond those defined byAuthenticator. Maybe you want to customize the prompts and
messages displayed byAuthenticator.authenticate()without having to useAuthenticator.ping()andAuthenticator.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.FAQMaybe 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 nowA: 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 dudeA: 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!importtimeimportrequestsfromoauthlib.oauth2importDeviceClientdefauthenticate_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"])whileTrue:response=requests.post(poll_uri,headers={"Accept":"application/json"}).json()if"error"inresponse:ifresponse["error"]in["authorization_pending","slow_down"]:time.sleep(response["interval"])continueelse:raiseRuntimeError(response["error"])else:returnresponse["access_token"]Damn, that plate can boil!Now let's do the same thing, but with loctocat.fromloctocat.predefinedimportGitHubAuthenticatordefauthenticate_with_github(client_id:str,scopes:list[str])->str:authenticator=GitHubAuthenticator(client_id=client_id,scopes=scopes)returnauthenticator.authenticate()# "Hey, that's cheating!" Fine, let's do it the hard way.fromloctocatimportAuthenticatordefauthenticate_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)returnauthenticator.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 AAAAGGGGGGHHHHHA: 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 ideaA: You're very welcome. 🙂LicenseIn 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 theMIT License.