While working on Oatfin, one use case we just finished implementing is using Github to allow users to sign up and login for the app. Users can now easily login with Google, Github, and Gitlab.
Our default login is also password-less meaning a user can login with just an email address. We send the login link directly to the email. This adds a layer of security because the user has to have access to the email to login and also validates that the user is a real person. Another problem password-less solves is syncing users from different sign in providers. We can guarantee a user who signed in with Google is the same user from Github or Gitlab.
Our app uses React with Typescript on the front-end and Python, Flask on the back-end. Here is what it looks like.
Part one is to create an OAuth app in Github as explained here.
Part two is setting up the React/Typescript component. When unauthenticated users visit the home page, they are redirected to the login page. That should be your application’s default behavior already.
When users click your ‘Login with Github’ button, they are first sent to Github’s login page with the scopes you want and your client_id like this:
const onClick = async () => {
window.location.href =
'https://github.com/login/oauth/authorize?scope=user:email&client_id=YOUR_CLIENT_ID'
}
After visiting Github’s website and they login, Github redirects back to your call back page which might still be the login page in our case. In your callback url, there is a now parameter ?code=some_code_text.
Your goal now is to take the code returned from Github and pass it to the Python/Flask app. My login component looks like this:
const Login: React.FC<{}> = () => {
// ...
useEffect(() => {
// see if code was returned, returns an error if the user denies the request
const newUrl = window.location.href;
const hasCode = newUrl.includes('?code=');
if (hasCode) {
// get the code value
const url = newUrl.split('?code=')[1].split('#/login');
const data = {
code: url[0],
};
// send the code to the backend
submitGithub(data as LoginParamsType);
}
}, [submitGithub]);
const onClick = async () => {
window.location.href =
'https://github.com/login/oauth/authorize?scope=user:email&client_id=YOUR_CLIENT_ID'
}
return (
<Button onClick={onClick}
Continue with Github
</Button>
)
}
Here we take the code and call the API, which returns access token to store in localStorage.
const submitGithub = async (values: LoginParamsType) => {
try {
const res = await accountLogin({ ...values });
if (res !== undefined && res.access_token !== undefined) {
window.localStorage.setItem('oatfin_access_token', res.access_token);
} catch (error) {
message.error('Unable to login with Github.');
}
};
Part three is the Python/Flask login API. We make 2 calls to Github: first to exchange the code we got from the front-end with an access token, then to use the access token to get the user details.
import requests
@api.route('/login', methods=['POST'])
def login():
req_data = flask.request.get_json()
code = req_data.get('code')
if code:
data = {
'client_id': app_config.GITHUB_CLIENT_ID,
'client_secret': app_config.GITHUB_CLIENT_SECRET,
'code': code
}
# exchange the 'code' for an access token
res = requests.post(
url='https://github.com/login/oauth/access_token',
data=data,
headers={'Accept': 'application/json'}
)
if res.status_code != 200:
raise UnauthenticatedError()
res_json = res.json()
access_token = res_json['access_token']
# get the user details using the access token
res = requests.get(
url='https://api.github.com/user',
headers={
'Accept': 'application/json',
'Authorization': 'token {}'.format(access_token)
}
)
if res.status_code != 200:
raise UnauthenticatedError()
res_json = res.json()
names = res_json['name'].split()
first_name = names[0]
last_name = names[1]
login = res_json['login'] or res_json['email']
avatar = res_json['avatar_url']
# create the user
user = UserService().create(...)
access_token = create_access_token(identity=user.json())
return flask.jsonify(
access_token=access_token
), 200