#|default_exp auth
Helpers for creating GitHub API tokens
#|export
from fastcore.all import *
from ghapi.core import *
import webbrowser,time
from urllib.parse import parse_qs,urlsplit
#|export
_scopes =(
'repo','repo:status','repo_deployment','public_repo','repo:invite','security_events','admin:repo_hook','write:repo_hook',
'read:repo_hook','admin:org','write:org','read:org','admin:public_key','write:public_key','read:public_key','admin:org_hook',
'gist','notifications','user','read:user','user:email','user:follow','delete_repo','write:discussion','read:discussion',
'write:packages','read:packages','delete:packages','admin:gpg_key','write:gpg_key','read:gpg_key','workflow'
)
#|export
Scope = AttrDict({o.replace(':','_'):o for o in _scopes})
#|export
def scope_str(*scopes)->str:
"Convert `scopes` into a comma-separated string"
return ','.join(str(o) for o in scopes if o)
scope_str(Scope.repo,Scope.admin_public_key,Scope.public_repo)
'repo,admin:public_key,public_repo'
#|export
_def_clientid = '71604a89b882ab8c8634'
#|export
class GhDeviceAuth(GetAttrBase):
"Get an oauth token using the GitHub API device flow"
_attr="params"
def __init__(self, client_id=_def_clientid, *scopes):
url = 'https://github.com/login/device/code'
self.client_id = client_id
self.params = parse_qs(urlread(url, client_id=client_id, scope=scope_str(*scopes)))
def _getattr(self,v): return v[0]
Creating a GhDeviceAuth
will complete the first step in the GitHub API device flow, getting device and user codes.
ghauth = GhDeviceAuth()
ghauth.device_code,ghauth.user_code
('62956bc850018fb2e5c4b62501df72bbe5583a5a', '247D-B1A6')
#|export
@patch
def url_docs(self:GhDeviceAuth)->str:
"Default instructions on how to authenticate"
return f"""First copy your one-time code: {self.user_code}
Then visit {self.verification_uri} in your browser, and paste the code when prompted."""
You can provide your own instructions on how to authenticate, or just print this out:
print(ghauth.url_docs())
First copy your one-time code: 247D-B1A6 Then visit https://github.com/login/device in your browser, and paste the code when prompted.
#|export
@patch
def open_browser(self:GhDeviceAuth):
"Open a web browser with the verification URL"
webbrowser.open(self.verification_uri)
This uses Python's webbrowser.open
, which will use the user's default web browser. This won't work well if the user is using a remote terminal.
#|export
@patch
def auth(self:GhDeviceAuth)->str:
"Return token if authentication complete, or `None` otherwise"
resp = parse_qs(urlread(
'https://github.com/login/oauth/access_token',
client_id=self.client_id, device_code=self.device_code,
grant_type='urn:ietf:params:oauth:grant-type:device_code'))
err = nested_idx(resp, 'error', 0)
if err == 'authorization_pending': return None
if err: raise Exception(resp['error_description'][0])
return resp['access_token'][0]
Until the user has completed authentication in the browser, this will return None. Normally, you won't call this directly, but will call wait
(see below), which will repeatedly call auth
until authentication is complete.
print(ghauth.auth())
None
#|export
@patch
def wait(self:GhDeviceAuth, cb:callable=None, n_polls=9999)->str:
"Wait up to `n_polls` times for authentication to complete, calling `cb` after each poll, if passed"
interval = int(self.interval)+1
res = None
for i in range(n_polls):
res = self.auth()
if res: return res
if cb: cb()
time.sleep(interval)
If you pass a callback to cb
, it will be called after each unsuccessful check for user authentication. For instance, to print a .
to the screen after each poll, and store the token in a variable token
when complete, you could use:
token = ghauth.wait(lambda: print('.', end=''))
#|hide
# token = ghauth.wait(lambda: print('.', end=''))
#|export
def github_auth_device(wb='', n_polls=9999):
"Authenticate with GitHub, polling up to `n_polls` times to wait for completion"
auth = GhDeviceAuth()
print(f"First copy your one-time code: \x1b[33m{auth.user_code}\x1b[m")
print(f"Then visit {auth.verification_uri} in your browser, and paste the code when prompted.")
if not wb: wb = input("Shall we try to open the link for you? [y/n] ")
if wb[0].lower()=='y': auth.open_browser()
print("Waiting for authorization...", end='')
token = auth.wait(lambda: print('.', end=''), n_polls=n_polls)
if not token: return print('Authentication not complete!')
print("Authenticated with GitHub")
return token
When we run this we'll be shown a URL to visit and a code to enter in order to authenticate. Normally we'll be prompted to open a browser, and the function will wait for authentication to complete -- for demonstrating here we'll skip both of these steps:
github_auth_device('n',n_polls=0)
First copy your one-time code: 4ACE-3C18
Then visit https://github.com/login/device in your browser, and paste the code when prompted.
Waiting for authorization...Authentication not complete!
#|hide
from nbdev import nbdev_export
nbdev_export()
Converted 00_core.ipynb. Converted 01_actions.ipynb. Converted 02_auth.ipynb. Converted 03_page.ipynb. Converted 04_event.ipynb. Converted 10_cli.ipynb. Converted 50_fullapi.ipynb. Converted 80_tutorial_actions.ipynb. Converted 90_build_lib.ipynb. Converted ghapi_demo.ipynb. Converted index.ipynb.