Skip to content

Commit

Permalink
Merge pull request #507 from Turbo87/oauth
Browse files Browse the repository at this point in the history
Replace cookie-based auth by OAuth tokens
  • Loading branch information
Turbo87 authored Oct 25, 2016
2 parents 965a4be + b758a92 commit ab30d94
Show file tree
Hide file tree
Showing 21 changed files with 376 additions and 229 deletions.
21 changes: 0 additions & 21 deletions ember/app/authenticators/cookie.js

This file was deleted.

32 changes: 32 additions & 0 deletions ember/app/authenticators/oauth2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Ember from 'ember';
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrant.extend({
ajax: Ember.inject.service(),

clientId: 'skylines.aero',
serverTokenEndpoint: '/api/oauth/token',
serverTokenRevocationEndpoint: '/api/oauth/revoke',

authenticate() {
return this._super(...arguments)
.then(data => this._addSettings(data));
},

restore() {
return this._super(...arguments)
.then(data => this._addSettings(data));
},

_addSettings(data) {
let headers = {};
if (data.access_token) {
headers['Authorization'] = `Bearer ${data.access_token}`;
}

return this.get('ajax').request('/api/settings', { headers }).then(settings => {
data.settings = settings;
return data;
});
},
});
2 changes: 1 addition & 1 deletion ember/app/components/login-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default Ember.Component.extend({
let { email, password } = this.getProperties('email', 'password');

try {
yield this.get('session').authenticate('authenticator:cookie', email, password);
yield this.get('session').authenticate('authenticator:oauth2', email, password);
} catch (error) {
this.set('error', error);
}
Expand Down
3 changes: 0 additions & 3 deletions ember/app/components/upload-flight-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ export default Ember.Component.extend(Validations, {
let data = new FormData(form);

try {
let csrfToken = yield this.get('ajax').request('/api/flights/upload/csrf').then(it => it.token);
data.append('csrfToken', csrfToken);

let json = yield this.get('ajax').request('/api/flights/upload/', { method: 'POST', data, contentType: false, processData: false });
this.getWithDefault('onUpload', Ember.K)(json);

Expand Down
12 changes: 12 additions & 0 deletions ember/app/services/ajax.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import Ember from 'ember';
import AjaxService from 'ember-ajax/services/ajax';

export default AjaxService.extend({
session: Ember.inject.service(),

headers: Ember.computed('session.data.authenticated.access_token', function() {
let headers = {};
let authToken = this.get('session.data.authenticated.access_token');
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
return headers;
}),

options(url, options = {}) {
if (options.json) {
options.contentType = 'application/json';
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
'flask==0.10.1',
'werkzeug==0.9.6',
'Flask-Babel==0.9',
'Flask-Login==0.2.9',
'Flask-Cache==0.12',
'Flask-Migrate==1.2.0',
'Flask-Script==0.6.7',
Expand Down
10 changes: 4 additions & 6 deletions skylines/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ def add_cache(self):
from skylines.frontend.cache import cache
cache.init_app(self)

def add_login_manager(self):
""" Create and attach Login extension """
from skylines.frontend.login import login_manager
login_manager.init_app(self)

def add_logging_handlers(self):
if self.debug: return

Expand Down Expand Up @@ -87,11 +82,14 @@ def create_http_app(*args, **kw):


def create_frontend_app(*args, **kw):
from skylines.frontend.oauth import oauth

app = create_http_app('skylines.frontend', *args, **kw)

app.add_cache()
app.load_egm96()
app.add_login_manager()

oauth.init_app(app)

import skylines.frontend.views
skylines.frontend.views.register(app)
Expand Down
23 changes: 0 additions & 23 deletions skylines/frontend/login.py

This file was deleted.

170 changes: 170 additions & 0 deletions skylines/frontend/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import time
from functools import wraps

from itsdangerous import JSONWebSignatureSerializer
from flask import Blueprint, request, abort, jsonify, current_app

from flask.ext.oauthlib.provider import OAuth2Provider
from flask_oauthlib.provider import OAuth2RequestValidator
from flask_oauthlib.provider.oauth2 import log
from flask_oauthlib.utils import decode_base64
from oauthlib.common import to_unicode
from oauthlib.oauth2.rfc6749.tokens import random_token_generator

from skylines.database import db
from skylines.model import User, Client, RefreshToken, AccessToken


class CustomProvider(OAuth2Provider):
def __init__(self, *args, **kwargs):
super(CustomProvider, self).__init__(*args, **kwargs)
self.blueprint = Blueprint('oauth', __name__)

def init_app(self, app):
super(CustomProvider, self).init_app(app)
app.config.setdefault('OAUTH2_PROVIDER_TOKEN_GENERATOR', self.generate_token)
app.config.setdefault('OAUTH2_PROVIDER_REFRESH_TOKEN_GENERATOR', random_token_generator)

app.jws = JSONWebSignatureSerializer(app.config.get('SECRET_KEY'))

app.register_blueprint(self.blueprint)

def generate_token(self, request):
token = {
'user': request.user.id,
'exp': int(time.time() + request.expires_in),
}

if request.scopes is not None:
token['scope'] = ' '.join(request.scopes)

return current_app.jws.dumps(token)

def verify_request(self, scopes):
if request.authorization:
from skylines.model import User

user = User.by_credentials(
request.authorization.username,
request.authorization.password,
)

request.user_id = user.id if user else None
return (user is not None), None

else:
valid, req = super(CustomProvider, self).verify_request(scopes)

request.user_id = req.access_token.user_id if valid else None

return valid, req

def required(self, *args, **kwargs):
return self.require_oauth(*args, **kwargs)

def optional(self, *scopes):
"""Enhance resource with specified scopes."""
def wrapper(f):
@wraps(f)
def decorated(*args, **kwargs):
for func in self._before_request_funcs:
func()

if hasattr(request, 'oauth') and request.oauth:
return f(*args, **kwargs)

valid, req = self.verify_request(scopes)

for func in self._after_request_funcs:
valid, req = func(valid, req)

if not valid and (not req or 'Authorization' in req.headers or req.access_token):
if self._invalid_response:
return self._invalid_response(req)
return abort(401)
request.oauth = req
return f(*args, **kwargs)
return decorated
return wrapper


class CustomRequestValidator(OAuth2RequestValidator):
def __init__(self):
super(CustomRequestValidator, self).__init__(
clientgetter=lambda client_id: Client(),
tokengetter=self.tokengetter,
grantgetter=None,
usergetter=User.by_credentials,
tokensetter=self.tokensetter,
)

@staticmethod
def tokengetter(access_token=None, refresh_token=None):
""" Retrieve a token record using submitted access token or refresh token. """
if access_token:
return AccessToken.from_jwt(access_token)

elif refresh_token:
return RefreshToken.query(refresh_token=refresh_token).first()

@staticmethod
def tokensetter(token, request, *args, **kwargs):
""" Save a new token to the database.
:param token: Token dictionary containing access and refresh tokens, plus token type.
:param request: Request dictionary containing information about the client and user.
"""

if request.grant_type != 'refresh_token':
tok = RefreshToken(
refresh_token=token['refresh_token'],
user_id=request.user.id,
)
db.session.add(tok)
db.session.commit()

def rotate_refresh_token(self, request):
return False

def authenticate_client(self, request, *args, **kwargs):

auth = request.headers.get('Authorization', None)
if auth:
try:
_, s = auth.split(' ')
client_id, client_secret = decode_base64(s).split(':')
client_id = to_unicode(client_id, 'utf-8')
except Exception as e:
log.debug('Authenticate client failed with exception: %r', e)
return False
else:
client_id = request.client_id

client = self._clientgetter(client_id)
if not client:
log.debug('Authenticate client failed, client not found.')
return False

return self.authenticate_client_id(client_id, request)


oauth = CustomProvider()
oauth._validator = CustomRequestValidator()


@oauth.blueprint.route('/api/oauth/token', methods=['POST'])
@oauth.token_handler
def access_token(*args, **kwargs):
return None


@oauth.blueprint.route('/api/oauth/revoke', methods=['POST'])
@oauth.revoke_handler
def revoke_token():
pass


@oauth.invalid_response
def invalid_require_oauth(req):
message = req.error_message if req else 'Unauthorized'
return jsonify(error='invalid_token', message=message), 401
2 changes: 0 additions & 2 deletions skylines/frontend/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .errors import register as register_error_handlers
from .i18n import register as register_i18n
from .login import register as register_login

from .about import about_blueprint
from .airport import airport_blueprint
Expand Down Expand Up @@ -30,7 +29,6 @@
def register(app):
register_error_handlers(app)
register_i18n(app)
register_login(app)

app.register_blueprint(assets_blueprint)
app.register_blueprint(files_blueprint)
Expand Down
10 changes: 7 additions & 3 deletions skylines/frontend/views/club.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
from flask import Blueprint, g, request, jsonify
from flask import Blueprint, request, jsonify

from skylines.database import db
from skylines.frontend.oauth import oauth
from skylines.lib.dbutil import get_requested_record
from skylines.model import Club
from skylines.model import Club, User
from skylines.schemas import ClubSchema, ValidationError

club_blueprint = Blueprint('club', 'skylines')


@club_blueprint.route('/clubs/<club_id>', strict_slashes=False)
@oauth.optional()
def read(club_id):
current_user = User.get(request.user_id) if request.user_id else None

club = get_requested_record(Club, club_id)

json = ClubSchema().dump(club).data
json['isWritable'] = club.is_writable(g.current_user)
json['isWritable'] = club.is_writable(current_user)

return jsonify(**json)

Expand Down
Loading

0 comments on commit ab30d94

Please sign in to comment.