add in api auth system; update api docs

This commit is contained in:
Ryan Shea
2014-10-27 17:36:46 -04:00
parent 927661e704
commit 4e5b5dbbb2
22 changed files with 255 additions and 154 deletions

View File

@@ -13,7 +13,7 @@ app = Flask(__name__)
app.config.from_object('api.settings')
# Import functions
import errors, decorators, views
import errors, views
# Add in blueprints
from .docs import docs

View File

@@ -7,6 +7,8 @@
from flask import Blueprint
v1auth = Blueprint('v1auth', __name__, url_prefix='/v1')
v1auth = Blueprint('v1auth', __name__, url_prefix='')
import views
import views
from authentication import auth_required

View File

@@ -0,0 +1,42 @@
from hashlib import sha256
from functools import wraps, update_wrapper
from werkzeug.datastructures import Authorization
from flask import g, request
from .models import User
from ..errors import APIError
def authenticate_user(app_id, app_secret):
app_secret_hash = sha256(app_secret).hexdigest()
users = User.objects(app_id=app_id, app_secret=app_secret)
if users and len(users) == 1:
user = users[0]
user.request_count = user.request_count + 1
try:
user.save()
except:
pass
return True
return False
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
app_id = request.values.get('app_id')
app_secret = request.values.get('app_secret')
if request.authorization:
auth = request.authorization
app_id = request.authorization.username
app_secret = request.authorization.password
elif app_id and app_secet:
auth = Authorization('basic', data={'username': app_id, 'password': app_secret})
else:
raise APIError('API credentials missing', status_code=400)
if not authenticate_user(app_id, app_secret):
raise APIError('Invalid API credentials', status_code=400)
return f(*args, **kwargs)
return decorated_function

24
api/auth/models.py Normal file
View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
Onename API
Copyright 2014 Halfmoon Labs, Inc.
~~~~~
"""
import json, datetime, binascii
from utilitybelt import dev_random_entropy, dev_urandom_entropy
from utils import generate_app_id
from ..db import db
class User(db.Document):
# metadata
created_at = db.DateTimeField(default=datetime.datetime.now, required=True)
# account/auth data
email = db.StringField(max_length=255, unique=True, required=True)
# api keys
app_id = db.StringField(default=generate_app_id(), max_length=255, unique=True, required=True)
app_secret = db.StringField(max_length=255, unique=True)
app_secret_hash = db.StringField(max_length=255, unique=True, required=True)
request_count = db.IntField(min_value=0, default=0)

View File

@@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
"""
Onename API
Copyright 2014 Halfmoon Labs, Inc.
~~~~~
"""
import time, datetime
from hashlib import md5
from ..db import db
nodes = db.nodes
MAX_QUOTA = 25
#---------------------
#Account Creation
#---------------------
def save_user(username, account_type):
""" used for temporary Token generation. (to be replaced)
create a new user (developer) given a username and account_type
"""
user = {}
user['username'] = username
user['account_type'] = account_type
user['access_token'] = generate_token(username)
user['api_quota'] = MAX_QUOTA
user['last_call'] = datetime.datetime.now()
nodes.save(user)
return user['access_token']
#-------------------------------------------------------------
def generate_token(username):
"""Receives username/email as input and generate md5 hash key of the input"""
hash_input = username + str(time.time())
access_token = md5(hash_input).hexdigest()
return access_token
#----------------------------------------------
def validate_token(access_token):
"""Checks if a 'key' is valid"""
if access_token == None:
return False
return nodes.find({'access_token' : access_token}).limit(1).count()
#---------------------
#Token Validation
#---------------------
def initialize_quota(username):
"""Initialize quota for a specific username to some starting value such as 1000 """
user = nodes.find_one({'username' : username})
user['api_quota'] = MAX_QUOTA
nodes.save(user)
#--------------------------------------
def decrement_quota(access_token):
"""returns False if quota associated with 'username' has expired. other
otherwise decrements quota"""
user = nodes.find_one({'access_token' : access_token})
if user['api_quota'] < 1:
return False
else:
user = nodes.find_one({'access_token' : access_token})
time_now = datetime.datetime.now()
difference = time_now - user['last_call']
difference = divmod(difference.days * 86400 + difference.seconds, 60) #format: 0 minutes, 8 seconds
#reset if 15mins have passed since the last call
if difference[0] > 14:
reset_quota(user)
user['last_call'] = datetime.datetime.now()
user['api_quota'] = user['api_quota'] - 1
nodes.save(user)
return True
#--------------------------------------
def reset_quota(user):
"""Reset (initialize) quota for all the users"""
user['api_quota'] = MAX_QUOTA
nodes.save(user)

27
api/auth/registration.py Normal file
View File

@@ -0,0 +1,27 @@
import traceback
from hashlib import sha256
from flask import render_template
from .models import User
from .utils import generate_app_secret
from ..email import send_w_mailgun
from ..errors import APIError
def register_user(email):
app_secret = generate_app_secret()
app_secret_hash = sha256(app_secret).hexdigest()
user = User(email=email, app_secret=app_secret, app_secret_hash=app_secret_hash)
try:
user.save()
except:
traceback.print_exc()
raise APIError('Could not register user')
template = render_template('email/registration.html',
user=user, app_secret=app_secret)
subject = 'Your Onename API Credentials'
send_w_mailgun(subject, user.email.encode('utf8'), template)
return user

8
api/auth/utils.py Normal file
View File

@@ -0,0 +1,8 @@
from binascii import hexlify
from utilitybelt import dev_urandom_entropy
def generate_app_id():
return hexlify(dev_urandom_entropy(16))
def generate_app_secret():
return hexlify(dev_urandom_entropy(16))

View File

@@ -1,15 +1,37 @@
from flask import request, jsonify
from flask import request, jsonify, render_template, redirect, url_for
from . import v1auth
from .rate_limit import save_user
from .core import register_user
from ..decorators import parameters_required
from ..errors import APIError
"""
@v1auth.route('/gen_developer_key/', methods=['GET'])
@parameters_required(parameters=['developer_id'])
def create_account():
""" creates a new dev. account
"""
access_token = save_user(request.values['developer_id'], 'basic')
return jsonify({'developer_id': request.values['developer_id'],
'access_token': access_token}), 200
"""
@v1auth.route('/registered')
def registered():
return render_template('registered.html')
@v1auth.route('/signup', methods=['GET', 'POST'])
def signup():
if request.method == 'POST':
if request.form and 'email' in request.form:
email = request.form['email']
try:
user = register_user(email)
except APIError:
return "user already exists"
return redirect(url_for('v1auth.registered'))
else:
return "something went wrong"
return render_template('signup.html')

View File

@@ -4,12 +4,18 @@
Copyright 2014 Halfmoon Labs, Inc.
~~~~~
"""
import os
from pymongo import MongoClient
from . import app
client = MongoClient(app.config['MONGODB_HOST'], app.config['MONGODB_PORT'])
db = client[app.config['MONGODB_DB']]
#from pymongo import MongoClient
#client = MongoClient(app.config['MONGODB_HOST'], app.config['MONGODB_PORT'])
#db = client[app.config['MONGODB_DB']]
#db.authenticate(app.config['MONGODB_USERNAME'], app.config['MONGODB_PASSWORD'])
db.authenticate(app.config['MONGODB_USERNAME'], app.config['MONGODB_PASSWORD'])
# MongoDB + MongoEngine
from mongoengine import connect
from flask.ext.mongoengine import MongoEngine
if 'MONGODB_URI' in app.config:
connect(app.config['MONGODB_DB'], host=app.config['MONGODB_URI'])
db = MongoEngine(app)

View File

@@ -11,7 +11,6 @@ from werkzeug.datastructures import Authorization
from flask import g, request
from . import app
from .rate_limit import validate_token, decrement_quota
from .errors import APIError
"""
@@ -28,29 +27,6 @@ def per_request_callbacks(response):
return response
"""
def access_token_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
demo_tokens = ['demo-1234']
token = request.values.get('token')
if request.authorization:
auth = request.authorization
token = request.authorization.username
elif token:
auth = Authorization('basic', data={'username':token, 'password':''})
else:
raise APIError('Access token missing', status_code=400)
if token not in demo_tokens:
if not validate_token(token):
raise APIError('Invalid token', status_code=400)
if not decrement_quota(token):
raise APIError('Quota exceeded', status_code=401)
return f(*args, **kwargs)
return decorated_function
def parameters_required(parameters):
def decorator(f):
def decorated_function(*args, **kwargs):

15
api/email.py Normal file
View File

@@ -0,0 +1,15 @@
import requests
from . import app
def send_w_mailgun(subject, recipient, template):
return requests.post(
"https://api.mailgun.net/v2/onename.io/messages",
auth=("api", app.config['MAILGUN_API_KEY']),
data={
"from": app.config['MAIL_USERNAME'],
"to": recipient,
"subject": subject,
"html": template
}
)

View File

@@ -7,10 +7,10 @@ from .samples import ryanshea
from ..errors import APIError, ProfileNotFoundError, BadProfileError, \
UsernameTakenError
from ..crossdomain import crossdomain
from ..decorators import access_token_required
from ..auth import auth_required
@v1profile.route('/openname/<username>')
@access_token_required
@v1profile.route('/users/<username>')
@auth_required
@crossdomain(origin='*')
def api_user(username):
if username == 'ryanshea-example':

View File

@@ -4,15 +4,18 @@ from flask import jsonify, request
from . import v1proofs
from .proofs import profile_to_verifications
from ..errors import APIError
from ..decorators import parameters_required, access_token_required
from ..decorators import parameters_required
from ..crossdomain import crossdomain
from ..profile import get_blockchain_profile
from ..auth import auth_required
@v1proofs.route('/verifications', methods=['POST'])
@parameters_required(parameters=["openname"])
@v1proofs.route('/users/<openname>/verifications')
@auth_required
@crossdomain(origin='*')
def verify_profile():
if not (request.data or request.form):
def verify_profile(openname):
profile = get_blockchain_profile(openname)
"""if not (request.data or request.form):
raise APIError('A payload must be included', status_code=400)
if request.data:
@@ -32,6 +35,7 @@ def verify_profile():
profile = data["profile"]
else:
profile = get_blockchain_profile(openname)
"""
verifications = profile_to_verifications(profile, openname)

View File

@@ -11,11 +11,12 @@ from flask import render_template, send_from_directory, Response, url_for, \
from . import v1search
from ..errors import APIError
from ..decorators import access_token_required, parameters_required
from ..decorators import parameters_required
from ..crossdomain import crossdomain
from ..auth import auth_required
@v1search.route('/search', methods=['GET'])
@access_token_required
@auth_required
@parameters_required(parameters=['query'])
@crossdomain(origin='*')
def search_people():

View File

@@ -40,3 +40,5 @@ else:
from .secrets import *
MONGODB_URI = 'mongodb://' + MONGODB_USERNAME + ':' + MONGODB_PASSWORD + '@' + MONGODB_HOST + ':' + str(MONGODB_PORT) + '/' + MONGODB_DB
MAIL_USERNAME = 'support@onename.io'

View File

@@ -18,7 +18,8 @@
<meta property="og:site_name" content='{{ site_name }}'/>
<meta property="og:description" content=""/>
<link href="{{ url_for('static', filename='css/bootstrap.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/docs.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
<!-- SUPPORT FOR IE6-8 OF HTML5 ELEMENTS -->
@@ -36,7 +37,7 @@
{% endblock %}
</head>
<body>
<div class="navbar navbar-default navbar-subtle">
<div class="navbar navbar-fixed-top navbar-default navbar-subtle">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
@@ -50,6 +51,9 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="/signup">Sign up</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
@@ -58,8 +62,10 @@
</div>
</div>
<div style="margin-top: 60px;">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -2,8 +2,9 @@
{% block content %}
<div ng-app="docApp">
<div class="container-fluid">
<div class="row">
<div class="col-md-4">
<div class="row" style="padding-bottom: 60px;">
<div class="col-md-3">
<div class="bs-docs-sidebar affix" style="margin-top: 60px;">
<div class="list-group">
<a href="/docs" class="list-group-item" ng-class="{active: activetab == '/docs'}">
Getting Started
@@ -11,8 +12,8 @@
<!--<a href="/docs/auth" class="list-group-item" ng-class="{active: activetab == '/docs/auth'}">
Authentication
</a>-->
<a href="/docs/opennames" class="list-group-item" ng-class="{active: activetab == '/docs/opennames'}">
Openname Lookups
<a href="/docs/profiles" class="list-group-item" ng-class="{active: activetab == '/docs/profiles'}">
Profiles
</a>
<a href="/docs/verifications" class="list-group-item" ng-class="{active: activetab == '/docs/verifications'}">
Verifications
@@ -21,6 +22,7 @@
Search
</a>
</div>
</div>
</div>
<div class="col-md-8">
<div ng-view=""></div>

View File

@@ -0,0 +1,17 @@
<p>
Hi there,
</p>
<p>
Thank you for using the Onename API.
</p>
<p>
Your App ID: {{ user.app_id }}
</p>
<p>
Your App Secret: {{ app_secret }}
</p>
<p>
Cheers,<br>The Onename Team
</p>

View File

@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Thanks for signing up!</h3>
<p>
Your API credentials have been sent to you. Check your email to start using the API.
</p>
</div>
</div>
{% endblock %}

26
api/templates/signup.html Normal file
View File

@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Sign Up for an API Key</h3>
<hr>
<div>
<form role="form" method="post">
<div class="form-group">
<input type="email" class="form-control" id="exampleInputEmail1" placeholder="Email" name="email">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
from flask import redirect, url_for
from flask import redirect, url_for, render_template, request
from . import app

View File

@@ -1,11 +1,16 @@
Flask==0.10.1
Flask-WTF==0.10.2
Jinja2==2.7.3
MarkupSafe==0.23
WTForms==2.0.1
Werkzeug==0.9.6
beautifulsoup4==4.3.2
commontools==0.1.0
flask-mongoengine==0.7.1
gunicorn==19.1.1
itsdangerous==0.24
mongoengine==0.8.7
pymongo==2.7.2
requests==2.3.0
utilitybelt==0.1.6
wsgiref==0.1.2