Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.json
.gitignore
.travis.yml
/google_secrets.json
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ ENV/
.idea

#secrets
coder_directory_api/prod_settings.json
coder_directory_api/prod_settings.json
/google_secrets.json
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ install:
services:
- mongodb

before_install:
- openssl aes-256-cbc -K $encrypted_fedd13c2de32_key -iv $encrypted_fedd13c2de32_iv -in google_secrets.json.enc -out google_secrets.json -d

# Seed the mongodb with some test data
before_script:
- bash mock_data/db_init.sh
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ coders and programming languages.
| /register | Registers a user/app to use api |
| /login | Login user to obtain token |
| /login/token | Send your tokens here to refresh your access before it expires |
| /google | Sign in to google and get access token |
| /users | Access users resource for GET/POST |
| /users/{id} | Access users resource for GET/PATCH/DELETE for 1 user |
| /languages | Access language resource for GET/POST |
| /languages/{id} | Access language resource for GET/PATCH/DELETE of 1 language |

With the exception of the `register` and `login` endpoints all resources

With the exception of the `register`, `google`, and `login` endpoints all resources
require a jwt to be sent in the `Authorization` header with `Bearer` scheme.

## Development
Expand Down
7 changes: 5 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def create_app() -> Flask:
"""

api = Flask('__name__')
api.secret_key = SECRET_KEY
CORS(api)
api.wsgi_app = ProxyFix(api.wsgi_app)
api.url_map.strict_slashes = False
Expand Down Expand Up @@ -49,7 +50,9 @@ def register_resources(api, bp, route=None):

if __name__ == '__main__':
app = create_app()
app.run(host=HOST,
app.run(
host=HOST,
port=PORT,
debug=DEBUG,
threaded=MULTITHREADING)
threaded=MULTITHREADING,
)
1 change: 1 addition & 0 deletions coder_directory_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def create_app() -> Flask:
"""

api = Flask('__name__')
api.secret_key = SECRET_KEY
CORS(api)
api.wsgi_app = ProxyFix(api.wsgi_app)
api.url_map.strict_slashes = False
Expand Down
4 changes: 3 additions & 1 deletion coder_directory_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
from .home import api as home_api
from .login import api as login_api
from .register import api as register_api
from .google import api as google_api

# Create a list of resource objects to register in api
api_resources = [
{'bp': languages_api, 'route': 'languages'},
{'bp': users_api, 'route': 'users'},
{'bp': home_api, 'route': None},
{'bp': login_api, 'route': 'login'},
{'bp': register_api, 'route': 'register'}
{'bp': register_api, 'route': 'register'},
{'bp': google_api, 'route': 'google'}
]
111 changes: 111 additions & 0 deletions coder_directory_api/resources/google.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
Google resource for Coder Directory

Copyright (c) 2017 by Mike Tung.
MIT License, see LICENSE for details.
"""

from flask import Blueprint, jsonify, redirect, request
from coder_directory_api.settings import GOOGLE_SECRETS
from coder_directory_api.engines.auth_engine import AuthEngine
import coder_directory_api.auth as auth

import google_auth_oauthlib.flow
import googleapiclient.discovery
from google.oauth2 import id_token
from google.auth.transport import requests

api = Blueprint('google', __name__)
auth_engine = AuthEngine()

# setup a flow object to manage OAuth exchange.
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
GOOGLE_SECRETS,
scopes=[
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/plus.me',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
])
flow.redirect_uri = 'http://localhost:3000/api/google/callback'
authorization_url, state = flow.authorization_url(
access_type='offline',
include_granted_scopes='true'
)

# setup white list for valid issuers
white_list = ['accounts.google.com', 'https://accounts.google.com']


@api.route('/', methods=['GET', 'POST'])
def google_login() -> redirect:
"""
Google OAuth login resource for initiating OAuth Protocol.
Returns:
redirect to google login page.
"""
if request.method == 'GET':
return redirect(authorization_url)
elif request.method == 'POST':
data = request.get_json()
token = data['token']
client_id = data['clientId']

is_valid, data = _validate_google_token(
token=token,
client_id=client_id
)

if is_valid:
user = auth_engine.find_one(data)
if not user:
return jsonify({'message': 'User not registered!'}), 404
return jsonify(user)
else:
return jsonify({'message': data})


@api.route('/callback')
def callback_url() -> None:
"""
Callback uri for handling the second piece of google OAuth after user has
consented.
Returns:
Nothing.
"""
authorization_response = request.url
flow.fetch_token(authorization_response=authorization_response)
credentials = flow.credentials
d = googleapiclient.discovery.build('oauth2', 'v2', credentials=credentials)
data = d.userinfo().v2().me().get().execute()
auth_doc = {'user': data['email'], 'googleId': data['id']}
auth_engine.add_one(auth_doc)
payload = auth.make_token(auth_doc['user'])
return jsonify(payload)


def _validate_google_token(token, client_id) -> tuple:
"""
Helper function to validate a google oauth token

Args:
token: google token
client_id: application id which was used to get token.

Returns:
indicator as to whether google token is valid or not and
message/username.
"""

try:
id_info = id_token.verify_oauth2_token(
token,
requests.Request(),
client_id)
if id_info['iss'] not in white_list:
raise ValueError('Wrong Issuer!')
user_name = id_info['email']
except ValueError:
return False, 'Invalid Google token!'

return True, user_name
3 changes: 2 additions & 1 deletion coder_directory_api/resources/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def home() -> tuple:
'languages': 'Programming languages resource.',
'users': 'Users resource.',
'login': 'Login to api for access.',
'register': 'Registration route for access to api.'
'register': 'Registration route for access to api.',
'google': 'OAuth2 Google sign in for access'
}
]

Expand Down
7 changes: 7 additions & 0 deletions coder_directory_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ENV = os.environ.get('API_ENV', 'DEV')
BASE_URL = os.environ.get('API_BASE_URL', '/dev')


with open(SECRET, 'r') as s:
creds = json.load(s)

Expand All @@ -26,9 +27,15 @@

SECRET_KEY = creds['secretKey']

GOOGLE_SECRETS = os.environ.get('GOOGLE', None)
with open(GOOGLE_SECRETS, 'r') as g:
google_secrets = json.load(g)


if ENV == 'DEV':
DEBUG = True
MULTITHREADING = False
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

elif ENV == 'PROD':
DEBUG = False
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ services:
- HOST=0.0.0.0
- API_ENV=PROD
- API_SECRET=/app/coder_directory_api/prod_settings.json
- GOOGLE=/app/google_secrets.json
volumes:
- ./coder_directory_api/prod_settings.json:/app/coder_directory_api/prod_settings.json
- ./google_secrets.json:/app/google_secrets.json
links:
- mongodb
mongodb:
Expand Down
Binary file added google_secrets.json.enc
Binary file not shown.
2 changes: 1 addition & 1 deletion mock_data/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"refresh_token": {
"iss": "coder directory",
"sub": "seekheart",
"sub": "test credentials",
"created": "12/06/2017 01:54:11",
"jti": "928f8e06-453c-436d-8b11-6fe21a2614d2",
"iat": "1512543251000"
Expand Down
17 changes: 16 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
aniso8601==1.3.0
certifi==2017.11.5
asn1crypto==0.24.0
cachetools==2.0.1
certifi==2018.1.18
cffi==1.11.5
chardet==3.0.4
click==6.7
cryptography==2.1.4
Flask==0.12.2
Flask-Cors==3.0.3
google-api-python-client==1.6.5
google-auth==1.4.1
google-auth-httplib2==0.0.3
google-auth-oauthlib==0.2.0
httplib2==0.10.3
idna==2.6
itsdangerous==0.24
Jinja2==2.10
Expand All @@ -12,15 +21,21 @@ lazy==1.3
MarkupSafe==1.0
mistune==0.8.1
nose==1.3.7
oauth2client==4.1.2
oauthlib==2.0.6
pyasn1==0.4.2
pyasn1-modules==0.2.1
pycparser==2.18
PyJWT==1.5.3
pymongo==3.5.1
python-dateutil==2.6.1
pytz==2017.3
PyYAML==3.12
requests==2.18.4
requests-oauthlib==0.8.0
rsa==3.4.2
six==1.11.0
uritemplate==3.0.0
urllib3==1.22
URLObject==2.4.3
Werkzeug==0.12.2
28 changes: 28 additions & 0 deletions tests/api/test_api_google.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Tests for Google Resource

Copyright (c) 2017 by Mike Tung.
MIT License, see LICENSE for details.
"""

from .common_test_setup import CommonApiTest
import json
from coder_directory_api.engines import AuthEngine

class GoogleResourceTest(CommonApiTest):
def setUp(self):
"""Setup Google Tests"""
super(GoogleResourceTest, self).setUp()
self.endpoint = '{}/google'.format(self.base_url)
self.engine = AuthEngine()

def tearDown(self):
"""Teardown method for Google Tests"""
super(GoogleResourceTest, self).tearDown()

def test_redirect(self):
"""Test google oauth sign in redirect"""
result = self.app.get(self.endpoint)
self.assertEqual(result.status_code,
302,
msg='Expected 301 redirect to Google')