change to hydra as oauth backend

This commit is contained in:
TuxCoder 2020-05-21 13:20:27 +02:00
parent 157bf65635
commit 38932aef44
12 changed files with 266 additions and 147 deletions

View file

@ -1,5 +1,5 @@
# pylint: disable=unused-import
from .oidc import oidc_provider_views
from .auth import auth_views, init_login_manager
from .frontend import frontend_views
from .auth import auth_views
from .frontend import frontend_views, init_login_manager
from .api import api_views

View file

@ -0,0 +1,65 @@
import flask
from flask import Blueprint, redirect, request
from flask import current_app, session
from flask import jsonify
from flask.helpers import make_response
from flask.templating import render_template
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
from pyop.access_token import AccessToken, BearerTokenError
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for
from flask_login import login_required, login_user, logout_user
from werkzeug.utils import redirect
import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import ory_hydra_client as hydra
from requests_oauthlib.oauth2_session import OAuth2Session
import requests
from ..model import User, SecurityUser
from ..model_db import User as DbUser
from ..form.login import LoginForm
from ..auth_providers import LdapAuthProvider
api_views = Blueprint('api', __name__, url_prefix='/api')
@api_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo():
token = request.headers['authorization'].replace('Bearer ', '')
token_info = current_app.hydra_api.introspect_o_auth2_token(token=token)
user_db = DbUser.query.get(token_info.sub)
user = User.query().by_username(user_db.username)
r = requests.get(
"http://127.0.0.1:4444/userinfo",
headers={
'authorization': request.headers['authorization']})
userinfo = r.json()
scopes = token_info.scope.split(' ')
if 'email' in scopes:
userinfo['email'] = str(user.email)
if 'profile' in scopes:
userinfo['username'] = str(user.username)
print(userinfo)
return jsonify(userinfo)
@api_views.route('/users', methods=['GET'])
def user_list():
if 'authorization' not in request.headers:
return '', 403
token = request.headers['authorization'].replace('Bearer ', '')
token_info = current_app.hydra_api.introspect_o_auth2_token(token=token)
if 'lc_i_userlist' not in token_info.scope.split(' '):
return '', 403
return jsonify([{'username': str(user.username), 'email': str(user.email)}
for user in User.query().all()])

View file

@ -20,31 +20,44 @@ from werkzeug.utils import redirect
import logging
from urllib.parse import urlparse
from base64 import b64decode, b64encode
import http
from ..model import User, SecurityUser
from ..model_db import db, User as DbUser
from ..form.login import LoginForm
from ..auth_providers import AUTH_PROVIDER_LIST
from .oidc import do_logout
auth_views = Blueprint('auth', __name__, url_prefix='')
auth_views = Blueprint('auth', __name__, url_prefix='/auth')
@auth_views.route('/consent', methods=['GET', 'POST'])
def consent():
"""Always grant consent."""
# DUMMPY ONLY
def init_login_manager(app):
@app.login_manager.user_loader
def user_loader(username):
return User.query().by_username(username)
remember_me = True
remember_for = 60*60*24*7 # remember for 7 days
@app.login_manager.request_loader
def request_loader(request):
pass
@app.login_manager.unauthorized_handler
def unauthorized():
return redirect(url_for('auth.login', next=b64encode(request.url.encode())))
consent_request = current_app.hydra_api.get_consent_request(request.args['consent_challenge'])
requested_scope = consent_request.requested_scope
resp = current_app.hydra_api.accept_consent_request(consent_request.challenge, body={
'grant_scope': requested_scope,
'remember': remember_me,
'remember_for': remember_for,
})
return redirect(resp.redirect_to)
@auth_views.route('/login', methods=['GET', 'POST'])
def login():
login_challenge = request.args.get('login_challenge')
login_request = current_app.hydra_api.get_login_request(login_challenge)
if login_request.skip:
resp = current_app.hydra_api.accept_login_request(
login_challenge,
body={'subject': login_request.subject})
return redirect(resp.redirect_to)
form = LoginForm()
if form.validate_on_submit():
user = User.query().by_username(form.data['name'])
@ -53,13 +66,14 @@ def login():
else:
session['user'] = None
session['auth_providers'] = []
return redirect(url_for('auth.login_auth', next=flask.request.args.get('next')))
return redirect(url_for('auth.login_auth', login_challenge=login_challenge))
return render_template('frontend/login.html.j2', form=form)
@auth_views.route('/login/auth', methods=['GET', 'POST'])
def login_auth():
login_challenge = request.args.get('login_challenge')
login_request = current_app.hydra_api.get_login_request(login_challenge)
if 'username' not in session:
return redirect(url_for('auth.login'))
auth_forms = {}
@ -74,6 +88,21 @@ def login_auth():
auth_forms[auth_provider.get_name()]=form
if len(session['auth_providers']) >= 2:
remember_me = True
db_user = DbUser.query.filter(DbUser.username == session['username']).one_or_none()
if db_user is None:
db_user = DbUser(username=session['username'])
db.session.add(db_user)
db.session.commit()
subject = db_user.id
resp = current_app.hydra_api.accept_login_request(
login_challenge, body={
'subject': subject,
'remember': remember_me})
return redirect(resp.redirect_to)
login_user(SecurityUser(session['username']))
# TODO use this var
_next = None
@ -89,9 +118,10 @@ def login_auth():
@auth_views.route("/logout")
@login_required
def logout():
logout_user()
do_logout()
return redirect(url_for('.login'))
logout_challenge = request.args.get('logout_challenge')
logout_request = current_app.hydra_api.get_logout_request(logout_challenge)
resp = current_app.hydra_api.accept_logout_request(logout_challenge)
return redirect(resp.redirect_to)

View file

@ -14,15 +14,19 @@ from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, In
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
from pyop.util import should_fragment_encode
from flask import Blueprint, render_template, request, url_for
from flask import Blueprint, render_template, request, url_for, flash
from flask_login import login_required, login_user, logout_user, current_user
from werkzeug.utils import redirect
import logging
from datetime import timedelta
import pyotp
from base64 import b64decode, b64encode
from flask_dance.consumer import oauth_authorized
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.consumer import OAuth2ConsumerBlueprint
from ..model import User, SecurityUser, Totp
from ..model_db import OAuth, db, User as DbUser
from ..form.login import LoginForm
from ..form.frontend import ClientCertForm, TOTPForm, TOTPDeleteForm
from ..auth_providers import AUTH_PROVIDER_LIST
@ -31,6 +35,81 @@ from ..auth_providers import AUTH_PROVIDER_LIST
frontend_views = Blueprint('frontend', __name__, url_prefix='')
def init_login_manager(app):
@app.login_manager.user_loader
def user_loader(username):
return User.query().by_username(username)
@app.login_manager.request_loader
def request_loader(request):
pass
@app.login_manager.unauthorized_handler
def unauthorized():
return redirect(url_for('oauth.login'))
base_url = app.config['HYDRA_PUBLIC_URL']
example_blueprint = OAuth2ConsumerBlueprint(
"oauth", __name__,
client_id=app.config['OAUTH_ID'],
client_secret=app.config['OAUTH_SECRET'],
base_url=base_url,
token_url=f"{base_url}/oauth2/token",
authorization_url=f"{base_url}/oauth2/auth",
scope=['openid', 'profile', 'manage']
)
app.register_blueprint(example_blueprint, url_prefix="/")
app.oauth = example_blueprint
@oauth_authorized.connect_via(app.oauth)
def github_logged_in(blueprint, token):
if not token:
flash("Failed to log in.", category="error")
return False
print(f'debug ---------------{token}')
resp = blueprint.session.get("/userinfo")
if not resp.ok:
msg = "Failed to fetch user info from GitHub."
flash(msg, category="error")
return False
oauth_info = resp.json()
db_user = DbUser.query.get(str(oauth_info["sub"]))
oauth_username = db_user.username
# Find this OAuth token in the database, or create it
query = OAuth.query.filter_by(
provider=blueprint.name,
provider_username=oauth_username,
)
try:
oauth = query.one()
except NoResultFound:
oauth = OAuth(
provider=blueprint.name,
provider_username=oauth_username,
token=token,
)
login_user(SecurityUser(oauth.provider_username))
#flash("Successfully signed in with GitHub.")
# Since we're manually creating the OAuth model in the database,
# we should return False so that Flask-Dance knows that
# it doesn't have to do it. If we don't return False, the OAuth token
# could be saved twice, or Flask-Dance could throw an error when
# trying to incorrectly save it for us.
return True
@frontend_views.route('/logout')
def logout():
logout_user()
return redirect(f'{current_app.config["HYDRA_PUBLIC_URL"]}/oauth2/sessions/logout')
@frontend_views.route('/', methods=['GET'])
@login_required
def index():