From 045b63b5c7356f3a50924ba9c69d18f7462b519e Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Tue, 30 Jun 2020 16:23:20 -0400 Subject: [PATCH 1/5] jank oAuth implement --- backend/conduit/app.py | 7 ++-- backend/conduit/extensions.py | 2 + backend/conduit/settings.py | 3 +- backend/conduit/user/models.py | 9 ++++- backend/conduit/user/views.py | 66 ++++++++++++++++++++++++++++++-- components/profile/LoginForm.tsx | 36 ++++++++++++++++- lib/api/user.ts | 21 ++++++++++ lib/utils/constant.ts | 4 +- 8 files changed, 137 insertions(+), 11 deletions(-) diff --git a/backend/conduit/app.py b/backend/conduit/app.py index 9c71548..2c02e12 100644 --- a/backend/conduit/app.py +++ b/backend/conduit/app.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- """The app module, containing the app factory function.""" from flask import Flask -from conduit.extensions import bcrypt, cache, db, migrate, jwt, cors +from conduit.extensions import bcrypt, cache, db, migrate, jwt, cors, github from conduit import commands, user, profile, articles, organizations, tags from conduit.settings import ProdConfig from conduit.exceptions import InvalidUsage - def create_app(config_object=ProdConfig): """An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. @@ -23,7 +22,6 @@ def create_app(config_object=ProdConfig): register_commands(app) return app - def register_extensions(app): """Register Flask extensions.""" bcrypt.init_app(app) @@ -31,6 +29,9 @@ def register_extensions(app): db.init_app(app) migrate.init_app(app, db) jwt.init_app(app) + app.config['GITHUB_CLIENT_ID'] = '98574e099fa640413899' + app.config['GITHUB_CLIENT_SECRET'] = '272ac3010797de4cc29c5c0caf0bbd9df4d79832' + github.init_app(app) def register_blueprints(app): diff --git a/backend/conduit/extensions.py b/backend/conduit/extensions.py index ed45686..b49eb62 100644 --- a/backend/conduit/extensions.py +++ b/backend/conduit/extensions.py @@ -7,6 +7,7 @@ from flask_jwt_extended import JWTManager from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy, Model +from flask_github import GitHub class CRUDMixin(Model): @@ -51,6 +52,7 @@ def delete(self, commit=True): migrate = Migrate() cache = Cache() cors = CORS() +github = GitHub() from conduit.utils import jwt_identity, identity_loader # noqa diff --git a/backend/conduit/settings.py b/backend/conduit/settings.py index 742f196..7a02c01 100644 --- a/backend/conduit/settings.py +++ b/backend/conduit/settings.py @@ -33,7 +33,8 @@ class Config(object): 'https://bit-next-git-master.bitproject.now.sh', 'https://staging.bitproject.org', 'https://dev.bitproject.org', - 'https://bit-next.now.sh' + 'https://bit-next.now.sh', + 'https://github.com/login/oauth/authorize?client_id=98574e099fa640413899' ] JWT_HEADER_TYPE = 'Token' diff --git a/backend/conduit/user/models.py b/backend/conduit/user/models.py index f0493b1..39ba1f4 100644 --- a/backend/conduit/user/models.py +++ b/backend/conduit/user/models.py @@ -24,13 +24,17 @@ class User(SurrogatePK, Model): linkedinLink = Column(db.String(100), nullable=True) website = Column(db.Text(), nullable = True) isAdmin = Column(db.Boolean, nullable=False, default=False) + github_access_token = Column(db.String(255), nullable = True) + github_id = Column(db.Integer, nullable = True) token: str = '' - def __init__(self, username, email, password=None, **kwargs): + def __init__(self, username, email, password=None, github_access_token=None, **kwargs): """Create instance.""" db.Model.__init__(self, username=username, email=email, **kwargs) if password: self.set_password(password) + if github_access_token: + self.set_github_token(github_access_token) else: self.password = None @@ -42,6 +46,9 @@ def check_password(self, value): """Check password.""" return bcrypt.check_password_hash(self.password, value) + def set_github_token(self, github_access_token): + self.github_access_token = github_access_token + def __repr__(self): """Represent instance as a unique string.""" return ''.format(username=self.username) diff --git a/backend/conduit/user/views.py b/backend/conduit/user/views.py index dc56916..ce0eb59 100644 --- a/backend/conduit/user/views.py +++ b/backend/conduit/user/views.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- """User views.""" -from flask import Blueprint, request +from flask import Blueprint, request, jsonify, g from flask_apispec import use_kwargs, marshal_with from flask_jwt_extended import jwt_required, jwt_optional, create_access_token, current_user from sqlalchemy.exc import IntegrityError from conduit.database import db +from conduit.extensions import github from conduit.exceptions import InvalidUsage from conduit.profile.models import UserProfile from .models import User from .serializers import user_schema +import requests blueprint = Blueprint('user', __name__) - @blueprint.route('/api/users', methods=('POST',)) @use_kwargs(user_schema) @marshal_with(user_schema) @@ -26,7 +27,6 @@ def register_user(username, password, email, **kwargs): raise InvalidUsage.user_already_registered() return userprofile.user - @blueprint.route('/api/users/login', methods=('POST',)) @jwt_optional @use_kwargs(user_schema) @@ -64,3 +64,63 @@ def update_user(**kwargs): kwargs['updated_at'] = user.created_at.replace(tzinfo=None) user.update(**kwargs) return user + +#TODO: +#1) we have to add the state to make sure no third party access when sending code +#2) change this away from username, only allows me to call the thing username cause of user_schema. +#if bit_token invalid and access_tok still valid, just reauthenticate with new code and stuff +#if access_token invalid but bit_token valid, ignore until bit_token gets invalid + +#Note: the parameter is username but it should be changed to github_code +#i just get errors thrown if + +@blueprint.route('/api/user/callback', methods = ('POST',)) +@use_kwargs(user_schema) +@marshal_with(user_schema) +def github_oauth(username, **kwargs): + #refactor and hide these + + #NOTE: use try catch block later + payload = { 'client_id': "98574e099fa640413899", + 'client_secret': "272ac3010797de4cc29c5c0caf0bbd9df4d79832", + 'code': username, + } + header = { + 'Accept': 'application/json', + } + + auth_response = requests.post('https://github.com/login/oauth/access_token', params=payload, headers=header).json() + + #if it's an error response, the access_token will not work (like if code is invalid) + #it won't have access_token key-value pair + #build in try catch! + access_token = auth_response["access_token"] + + auth_header = {"Authorization": "Bearer " + access_token} + data_response = requests.get('https://api.github.com/user', headers=auth_header).json() + email_response = requests.get('https://api.github.com/user/emails', headers=auth_header).json() + + username = data_response["login"] + email = email_response[0]["email"] + github_id = data_response["id"] + + user = User.query.filter_by(email=email).first() + if user is None: + userprofile = UserProfile(User(username, email, github_access_token = access_token).save()).save() + user = userprofile.user + + user.token = create_access_token(identity=user, fresh=True) + return user + +# Flask Migrate + +# write code +# run flaskdb migrate in the code +# flaskdb upgrade in the code +# Code isn't working because staging db uses staging code +# Code isn't working on local because we don't have db + +# When doing github auth, we need to use flask db migrate to be able to add our cols +# to our remote db + + diff --git a/components/profile/LoginForm.tsx b/components/profile/LoginForm.tsx index aad5701..b486276 100644 --- a/components/profile/LoginForm.tsx +++ b/components/profile/LoginForm.tsx @@ -4,6 +4,7 @@ import { mutate } from "swr"; import ListErrors from "../common/ListErrors"; import UserAPI from "../../lib/api/user"; +import Authenticate from "./Authenticate"; const LoginForm = () => { const [isLoading, setLoading] = React.useState(false); @@ -20,6 +21,34 @@ const LoginForm = () => { [] ); + let logging_in; + if (typeof window !== "undefined"){ + const code = new URLSearchParams(window.location.search).get("code"); + if (code){ + logging_in = (

Redirecting to home page...

); + React.useEffect(() => { + + async function post_code(){ + try{ + const {data, status} = await UserAPI.post_code(code); + console.log("begun await"); + if (data?.user){ + console.log(data.user) + window.localStorage.setItem("user", JSON.stringify(data.user)); + mutate("user", data?.user); + Router.push("/"); + } + } catch(error){ + console.error(error); + } + } + + post_code(); + }, []) + } + } + + const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); @@ -77,8 +106,13 @@ const LoginForm = () => { + + Sign in through GitHub REAL + {logging_in} ); -}; +} export default LoginForm; diff --git a/lib/api/user.ts b/lib/api/user.ts index c1e7017..acd173c 100644 --- a/lib/api/user.ts +++ b/lib/api/user.ts @@ -2,6 +2,8 @@ import axios from "axios"; import { SERVER_BASE_URL } from "../utils/constant"; +const url = require('url'); + const UserAPI = { current: async () => { const user: any = window.localStorage.getItem("user"); @@ -135,6 +137,25 @@ const UserAPI = { } }, get: async (username) => axios.get(`${SERVER_BASE_URL}/profiles/${username}`), + + post_code: async (username) => { + try{ + const response = await axios.post( + `${SERVER_BASE_URL}/user/callback`, + JSON.stringify({ user: { username } }), + { + headers: { + "Content-Type": "application/json", + }, + } + ); + return response; + } catch (error){ + return error.response; + } + + }, + }; export default UserAPI; \ No newline at end of file diff --git a/lib/utils/constant.ts b/lib/utils/constant.ts index e24c371..3927144 100644 --- a/lib/utils/constant.ts +++ b/lib/utils/constant.ts @@ -2,10 +2,10 @@ // export const SERVER_BASE_URL = `https://conduit.productionready.io/api`; // Local backend url -// export const SERVER_BASE_URL = `http://127.0.0.1:5000/api`; +export const SERVER_BASE_URL = `http://127.0.0.1:5000/api`; // Staging url -export const SERVER_BASE_URL = `https://bit-devs-staging.herokuapp.com/api`; +// export const SERVER_BASE_URL = `https://bit-devs-staging.herokuapp.com/api`; // Production url. ONLY USE IN PRODUCTION // export const SERVER_BASE_URL = `https://bit-devs.herokuapp.com/api`; From 6b1972ff75de13e07be95c27c5b143e894c17486 Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Tue, 30 Jun 2020 16:41:22 -0400 Subject: [PATCH 2/5] forgot to delete line --- components/profile/LoginForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/profile/LoginForm.tsx b/components/profile/LoginForm.tsx index b486276..c79baf5 100644 --- a/components/profile/LoginForm.tsx +++ b/components/profile/LoginForm.tsx @@ -4,7 +4,6 @@ import { mutate } from "swr"; import ListErrors from "../common/ListErrors"; import UserAPI from "../../lib/api/user"; -import Authenticate from "./Authenticate"; const LoginForm = () => { const [isLoading, setLoading] = React.useState(false); From be8f71d4ef0ba80e3b8b5acd9ab252f9c50a0489 Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Thu, 9 Jul 2020 17:56:52 -0400 Subject: [PATCH 3/5] Moved into .env files and refactored code --- backend/conduit/config.py | 12 +++++ backend/conduit/user/views.py | 91 ++++++++++++-------------------- components/profile/LoginForm.tsx | 17 +++--- lib/api/user.ts | 7 ++- lib/utils/constant.ts | 6 +++ 5 files changed, 66 insertions(+), 67 deletions(-) create mode 100644 backend/conduit/config.py diff --git a/backend/conduit/config.py b/backend/conduit/config.py new file mode 100644 index 0000000..3a165cc --- /dev/null +++ b/backend/conduit/config.py @@ -0,0 +1,12 @@ +import os +from dotenv import load_dotenv +from os.path import dirname, join + +dotenv_path = join(dirname(__file__), '.env') +load_dotenv(dotenv_path) + +GITHUB_CLIENT = os.environ.get('GITHUB_ID') +GITHUB_SECRET = os.environ.get('GITHUB_SECRET') +ACCESS_TOKEN_URL = os.environ.get('ACCESS_TOKEN_URL') +GITHUB_API = os.environ.get('GITHUB_API') +STATE = os.environ.get('STATE') \ No newline at end of file diff --git a/backend/conduit/user/views.py b/backend/conduit/user/views.py index ce0eb59..98b454e 100644 --- a/backend/conduit/user/views.py +++ b/backend/conduit/user/views.py @@ -11,7 +11,9 @@ from conduit.profile.models import UserProfile from .models import User from .serializers import user_schema +from conduit.config import GITHUB_CLIENT, GITHUB_SECRET, ACCESS_TOKEN_URL, GITHUB_API, STATE import requests +import os blueprint = Blueprint('user', __name__) @@ -65,62 +67,39 @@ def update_user(**kwargs): user.update(**kwargs) return user -#TODO: -#1) we have to add the state to make sure no third party access when sending code -#2) change this away from username, only allows me to call the thing username cause of user_schema. -#if bit_token invalid and access_tok still valid, just reauthenticate with new code and stuff -#if access_token invalid but bit_token valid, ignore until bit_token gets invalid - -#Note: the parameter is username but it should be changed to github_code -#i just get errors thrown if - -@blueprint.route('/api/user/callback', methods = ('POST',)) +@blueprint.route('/api/user/callback//', methods = ('GET',)) @use_kwargs(user_schema) @marshal_with(user_schema) -def github_oauth(username, **kwargs): - #refactor and hide these - - #NOTE: use try catch block later - payload = { 'client_id': "98574e099fa640413899", - 'client_secret': "272ac3010797de4cc29c5c0caf0bbd9df4d79832", - 'code': username, - } - header = { - 'Accept': 'application/json', - } - - auth_response = requests.post('https://github.com/login/oauth/access_token', params=payload, headers=header).json() - - #if it's an error response, the access_token will not work (like if code is invalid) - #it won't have access_token key-value pair - #build in try catch! - access_token = auth_response["access_token"] - - auth_header = {"Authorization": "Bearer " + access_token} - data_response = requests.get('https://api.github.com/user', headers=auth_header).json() - email_response = requests.get('https://api.github.com/user/emails', headers=auth_header).json() - - username = data_response["login"] - email = email_response[0]["email"] - github_id = data_response["id"] - - user = User.query.filter_by(email=email).first() - if user is None: - userprofile = UserProfile(User(username, email, github_access_token = access_token).save()).save() - user = userprofile.user - - user.token = create_access_token(identity=user, fresh=True) - return user - -# Flask Migrate - -# write code -# run flaskdb migrate in the code -# flaskdb upgrade in the code -# Code isn't working because staging db uses staging code -# Code isn't working on local because we don't have db - -# When doing github auth, we need to use flask db migrate to be able to add our cols -# to our remote db - +def github_oauth(github_code, state): + try: + if (state.strip() != STATE): + raise InvalidUsage.user_not_found() + + payload = { 'client_id': GITHUB_CLIENT, + 'client_secret': GITHUB_SECRET, + 'code': github_code, + } + header = { + 'Accept': 'application/json', + } + + auth_response = requests.post(ACCESS_TOKEN_URL, params=payload, headers=header).json() + access_token = auth_response["access_token"] + + auth_header = {"Authorization": "Bearer " + access_token} + data_response = requests.get(GITHUB_API + 'user', headers=auth_header).json() + email_response = requests.get(GITHUB_API + 'user/emails', headers=auth_header).json() + + username = data_response["login"] + email = email_response[0]["email"] + github_id = data_response["id"] + + user = User.query.filter_by(email=email).first() + if user is None: + userprofile = UserProfile(User(username, email, github_access_token = access_token).save()).save() + user = userprofile.user + user.token = create_access_token(identity=user, fresh=True) + return user + except: + raise InvalidUsage.user_not_found() diff --git a/components/profile/LoginForm.tsx b/components/profile/LoginForm.tsx index c79baf5..758ab2c 100644 --- a/components/profile/LoginForm.tsx +++ b/components/profile/LoginForm.tsx @@ -1,9 +1,10 @@ import Router from "next/router"; -import React from "react"; +import React, { useEffect } from "react"; import { mutate } from "swr"; import ListErrors from "../common/ListErrors"; import UserAPI from "../../lib/api/user"; +import { CODE_URL, STATE, SCOPE, GITHUB_CLIENT } from "../../lib/utils/constant"; const LoginForm = () => { const [isLoading, setLoading] = React.useState(false); @@ -20,16 +21,20 @@ const LoginForm = () => { [] ); + const authorize_url = CODE_URL + "?client_id=" + GITHUB_CLIENT + "&scope=" + SCOPE + + "&state=" + STATE; + let logging_in; if (typeof window !== "undefined"){ const code = new URLSearchParams(window.location.search).get("code"); + const state = new URLSearchParams(window.location.search).get("state"); if (code){ logging_in = (

Redirecting to home page...

); - React.useEffect(() => { + useEffect(() => { async function post_code(){ try{ - const {data, status} = await UserAPI.post_code(code); + const {data, status} = await UserAPI.post_code(code, state); console.log("begun await"); if (data?.user){ console.log(data.user) @@ -105,10 +110,8 @@ const LoginForm = () => { - - Sign in through GitHub REAL + + Sign in through Github {logging_in} ); diff --git a/lib/api/user.ts b/lib/api/user.ts index acd173c..d05adf5 100644 --- a/lib/api/user.ts +++ b/lib/api/user.ts @@ -138,11 +138,10 @@ const UserAPI = { }, get: async (username) => axios.get(`${SERVER_BASE_URL}/profiles/${username}`), - post_code: async (username) => { + post_code: async (github_code, state) => { try{ - const response = await axios.post( - `${SERVER_BASE_URL}/user/callback`, - JSON.stringify({ user: { username } }), + const response = await axios.get( + `${SERVER_BASE_URL}/user/callback/${encodeURIComponent(github_code)}/${encodeURIComponent(state)}`, { headers: { "Content-Type": "application/json", diff --git a/lib/utils/constant.ts b/lib/utils/constant.ts index 3927144..bbbfd72 100644 --- a/lib/utils/constant.ts +++ b/lib/utils/constant.ts @@ -14,6 +14,12 @@ export const SERVER_BASE_URL = `http://127.0.0.1:5000/api`; export const APP_NAME = `conduit`; +export const CODE_URL = 'https://github.com/login/oauth/authorize'; +export const GITHUB_CLIENT = '98574e099fa640413899'; +export const SCOPE = 'user+repo'; +//must conceal state later +export const STATE = 'd3Asp0fK03M0Ldnwoi2Pnbh9knB2K335Ln'; + export const ARTICLE_QUERY_MAP = { "tab=feed": `${SERVER_BASE_URL}/articles/feed`, "tab=tag": `${SERVER_BASE_URL}/articles/tag` From 73929508b1acc8143a078ae912265de380e369aa Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Thu, 9 Jul 2020 18:05:17 -0400 Subject: [PATCH 4/5] fixed a bug because i left in use_kwargs --- backend/conduit/user/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/conduit/user/views.py b/backend/conduit/user/views.py index 98b454e..86a5afd 100644 --- a/backend/conduit/user/views.py +++ b/backend/conduit/user/views.py @@ -68,7 +68,6 @@ def update_user(**kwargs): return user @blueprint.route('/api/user/callback//', methods = ('GET',)) -@use_kwargs(user_schema) @marshal_with(user_schema) def github_oauth(github_code, state): try: From 4ee884a2a6489a7291e1e349e1a838475852abef Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Thu, 9 Jul 2020 18:11:34 -0400 Subject: [PATCH 5/5] encode state not working? --- components/profile/LoginForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/profile/LoginForm.tsx b/components/profile/LoginForm.tsx index 758ab2c..1029863 100644 --- a/components/profile/LoginForm.tsx +++ b/components/profile/LoginForm.tsx @@ -22,7 +22,7 @@ const LoginForm = () => { ); const authorize_url = CODE_URL + "?client_id=" + GITHUB_CLIENT + "&scope=" + SCOPE - + "&state=" + STATE; + + "&state=" + encodeURIComponent(STATE); let logging_in; if (typeof window !== "undefined"){