"""
abc-classroom.auth
==================
"""
# Methods for setting up authorization to the GitHub API. See
# github.py for methods that use the API.
import requests
import os.path as op
from ruamel.yaml import YAML
from .utils import get_request
[docs]def get_github_auth():
"""
Check to see if there is an existing github authentication
and load the authentication.
Returns
-------
ruamel.yaml.comments.CommentedMap
Yaml object that contains the token and id for a github session.
If yaml doesn't exists, return an empty dictionary.
"""
yaml = YAML()
try:
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
config = yaml.load(f)
return config["github"]
except FileNotFoundError:
return {}
[docs]def set_github_auth(auth_info):
"""
Set the github authentication information. Put the token and id
authentication information into a yaml file if it doesn't already exist.
Parameters
----------
auth_info : dictionary
The token and id authentication information from github stored in a
dictionary object.
"""
yaml = YAML()
config = {}
if get_github_auth():
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
config = yaml.load(f)
config["github"] = auth_info
with open(op.expanduser("~/.abc-classroom.tokens.yml"), "w") as f:
yaml.dump(config, f)
[docs]def check_or_generate_token():
"""Check that a valid access token exists for the GitHub API and
generates if one does not exist.
First tries local token file for valid token.
If token does not exist or is not valid, generates a new one.
"""
# TODO: this needs error handling.
# do we have a saved and valid token?
if not _check_local_token():
# if not, generate a new one
_generate_new_token()
def _check_local_token():
"""Checks that there is an access token in the local tokens file
and if so, checks if the token is valid against the GitHub API."""
valid_token_exists = False
auth_info = get_github_auth()
if auth_info:
try:
access_token = auth_info["access_token"]
# if so, is it valid?
user = _get_authenticated_user(access_token)
if user is not None:
print(
"Access token is present and valid; successfully "
"authenticated as user {}".format(user)
)
valid_token_exists = True
except KeyError:
# no access token in the file, so we leave valid_token_exists
# as False and continue on
pass
return valid_token_exists
def _generate_new_token():
"""Generates a new GitHub API access token using the OAuth Device Flow.
https://docs.github.com/en/free-pro-team@latest/developers/apps/
identifying-and-authorizing-users-for-github-apps#device-flow
Saves token to local token file.
"""
print("Generating new access token")
# client id for the abc-classroom-bot GitHub App
client_id = "Iv1.8df72ad9560c774c"
# TODO need to handle cases where the device call fails - wrong client_id,
# the user could ^C or the internet could be out, or some other
# unanticipated woe)
device_code = _get_login_code(client_id)
access_token = _poll_for_status(client_id, device_code)
# test the new access token
user = _get_authenticated_user(access_token)
if user is not None:
print("""Successfully authenticated as user {}""".format(user))
# save the token to the local tokens file
set_github_auth({"access_token": access_token})
def _get_authenticated_user(token):
"""Test the validity of an access token by making an
API call to get the authenticated user.
Parameters
----------
token : string
A string to try as a GitHub access token
Returns
-------
username : string
The GitHub username of the authenticated user if token valid,
otherwise returns None.
"""
url = "https://api.github.com/user"
(status, body) = get_request(url, token)
try:
user = body["login"]
return user
except KeyError:
return None
def _get_login_code(client_id):
"""Prompts the user to authorize abc-classroom-bot.
First part of the Device Flow workflow. Asks user to visit a URL and
enter the provided code. Waits for user to hit RETURN to continue.
Returns the device code.
Parameters
----------
client_id : str
String representing the ID for the abc-classroom bot.
Returns
-------
device_code : str
The device code for the response.
"""
# make the device call
header = {"Content-Type": "application/json", "Accept": "application/json"}
payload = {"client_id": client_id}
link = "https://github.com/login/device/code"
r = requests.post(link, headers=header, json=payload)
# process the response
data = r.json()
status = r.status_code
if status != 200:
# print the response if the call failed
print(r.json())
return None
device_code = data["device_code"]
uri = data["verification_uri"]
user_code = data["user_code"]
# prompt the user to enter the code
print(
"To authorize this app, go to {} and enter the code {}".format(
uri, user_code
)
)
input("\nPress RETURN to continue after inputting the code successfully")
return device_code
def _poll_for_status(client_id, device_code):
"""Polls API to see if user entered the device code
This is the second step of the device flow. Returns an access token.
Parameters
----------
client_id : str
A string representing the client code for the abc-classroom bot.
device_code : str
The device code returned from the API for the user's machine / device.
Returns
-------
Access token provided by GitHub.
"""
header = {"Content-Type": "application/json", "Accept": "application/json"}
payload = {
"client_id": client_id,
"device_code": device_code,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}
r = requests.post(
"https://github.com/login/oauth/access_token",
headers=header,
json=payload,
)
data = r.json()
access_token = data["access_token"]
return access_token