diff --git a/app.py b/app.py index 440cf79..66c4692 100644 --- a/app.py +++ b/app.py @@ -2,12 +2,10 @@ app = create_app({ - 'SECRET_KEY': 'secret', 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 'SQLALCHEMY_DATABASE_URI': 'sqlite:///db.sqlite', }) - @app.cli.command() def initdb(): from website.models import db diff --git a/requirements.txt b/requirements.txt index 2813c42..6f85e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,14 @@ -Flask -Flask-SQLAlchemy -Authlib==0.13 +Authlib==1.0.1 +cffi==1.15.1 +click==8.1.3 +cryptography==37.0.4 +Flask==2.1.3 +Flask-SQLAlchemy==2.5.1 +importlib-metadata==4.12.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +pycparser==2.21 +SQLAlchemy==1.4.39 +Werkzeug==2.2.0 +zipp==3.8.1 diff --git a/website/app.py b/website/app.py index 05a77db..ecb5a07 100644 --- a/website/app.py +++ b/website/app.py @@ -15,7 +15,7 @@ def create_app(config=None): if 'WEBSITE_CONF' in os.environ: app.config.from_envvar('WEBSITE_CONF') - # load app sepcified configuration + # load app specified configuration if config is not None: if isinstance(config, dict): app.config.update(config) diff --git a/website/models.py b/website/models.py index 1623f16..d3d8c18 100644 --- a/website/models.py +++ b/website/models.py @@ -8,6 +8,7 @@ db = SQLAlchemy() +# Resource Owner: user using your service class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(40), unique=True) @@ -19,6 +20,8 @@ def get_user_id(self): return self.id +# Client: Application making protected resource requests on behalf of resource owner +# Registered to developer (user on your site) class OAuth2Client(db.Model, OAuth2ClientMixin): __tablename__ = 'oauth2_client' @@ -28,6 +31,7 @@ class OAuth2Client(db.Model, OAuth2ClientMixin): user = db.relationship('User') +# Common grant type, authorization code exchanged for access code class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): __tablename__ = 'oauth2_code' @@ -37,6 +41,7 @@ class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): user = db.relationship('User') +# Tokens: used to access users' resources class OAuth2Token(db.Model, OAuth2TokenMixin): __tablename__ = 'oauth2_token' diff --git a/website/oauth2.py b/website/oauth2.py index 955360c..e65e500 100644 --- a/website/oauth2.py +++ b/website/oauth2.py @@ -17,14 +17,9 @@ from werkzeug.security import gen_salt from .models import db, User from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token +from flask import current_app -DUMMY_JWT_CONFIG = { - 'key': 'secret-key', - 'alg': 'HS256', - 'iss': 'https://authlib.org', - 'exp': 3600, -} def exists_nonce(nonce, req): exists = OAuth2AuthorizationCode.query.filter_by( @@ -33,31 +28,41 @@ def exists_nonce(nonce, req): return bool(exists) +def get_jwt_config(): + if current_app.config.get('OAUTH2_JWT_ENABLED',False) \ + and 'OAUTH2_JWT_KEY' not in current_app.config: + raise NotImplementedError('"OAUTH2_JWT_KEY" missing from settings') + return { + 'key': current_app.config.get('OAUTH2_JWT_KEY', 'secret-key'), + 'alg': current_app.config.get('OAUTH2_JWT_ALG', 'HS256'), + 'iss': current_app.config.get('OAUTH2_JWT_ISS', 'https://authlib.org'), + 'exp': current_app.config.get('OAUTH2_JWT_EXP', 3600), + } + + def generate_user_info(user, scope): return UserInfo(sub=str(user.id), name=user.username) -def create_authorization_code(client, grant_user, request): - code = gen_salt(48) - nonce = request.data.get('nonce') - item = OAuth2AuthorizationCode( +def save_authorization_code(code, request): + auth_code = OAuth2AuthorizationCode( code=code, - client_id=client.client_id, + client_id=request.client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, - user_id=grant_user.id, - nonce=nonce, + user_id=request.user.id, + nonce=request.data.get('nonce'), # MAY have nonce ) - db.session.add(item) + db.session.add(auth_code) db.session.commit() - return code + return auth_code class AuthorizationCodeGrant(_AuthorizationCodeGrant): - def create_authorization_code(self, client, grant_user, request): - return create_authorization_code(client, grant_user, request) + def save_authorization_code(self, code, request): + return save_authorization_code(code, request) - def parse_authorization_code(self, code, client): + def query_authorization_code(self, code, client): item = OAuth2AuthorizationCode.query.filter_by( code=code, client_id=client.client_id).first() if item and not item.is_expired(): @@ -76,7 +81,7 @@ def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def get_jwt_config(self, grant): - return DUMMY_JWT_CONFIG + return get_jwt_config() def generate_user_info(self, user, scope): return generate_user_info(user, scope) @@ -87,21 +92,21 @@ def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def get_jwt_config(self, grant): - return DUMMY_JWT_CONFIG + return get_jwt_config() def generate_user_info(self, user, scope): return generate_user_info(user, scope) class HybridGrant(_OpenIDHybridGrant): - def create_authorization_code(self, client, grant_user, request): - return create_authorization_code(client, grant_user, request) + def save_authorization_code(self, code, request): + return save_authorization_code(code, request) def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) def get_jwt_config(self): - return DUMMY_JWT_CONFIG + return get_jwt_config() def generate_user_info(self, user, scope): return generate_user_info(user, scope) diff --git a/website/routes.py b/website/routes.py index 78f135e..b8940ff 100644 --- a/website/routes.py +++ b/website/routes.py @@ -8,7 +8,7 @@ from .oauth2 import authorization, require_oauth, generate_user_info -bp = Blueprint(__name__, 'home') +bp = Blueprint('bp', __name__) def current_user(): @@ -76,18 +76,33 @@ def create_client(): @bp.route('/oauth/authorize', methods=['GET', 'POST']) def authorize(): user = current_user() + # TODO Login is required since we need to know the current resource owner. + # It can be done with a redirection to the login page, or a login + # form on this authorization page. if request.method == 'GET': try: - grant = authorization.validate_consent_request(end_user=user) + grant = authorization.get_consent_grant(end_user=user) + client = grant.client + scope = client.get_allowed_scope(grant.request.scope) + + # You may add a function to extract scope into a list of scopes + # with rich information, e.g. + # scopes = describe_scope(scope) # returns [{'key': 'email', 'icon': '...'}] except OAuth2Error as error: return jsonify(dict(error.get_body())) - return render_template('authorize.html', user=user, grant=grant) + return render_template( + 'authorize.html', + user=user, + grant=grant + ) # can add client and scopes here if not user and 'username' in request.form: username = request.form.get('username') user = User.query.filter_by(username=username).first() if request.form['confirm']: + # granted by resource owner grant_user = user else: + # denied by resource owner grant_user = None return authorization.create_authorization_response(grant_user=grant_user) @@ -98,6 +113,6 @@ def issue_token(): @bp.route('/oauth/userinfo') -@require_oauth('profile') +@require_oauth('openid profile') def api_me(): return jsonify(generate_user_info(current_token.user, current_token.scope)) diff --git a/website/settings.py b/website/settings.py index 3d15b10..9c63130 100644 --- a/website/settings.py +++ b/website/settings.py @@ -1,5 +1,11 @@ -OAUTH2_JWT_ENABLED = True +# FLASK +SECRET_KEY = 'secret-key' -OAUTH2_JWT_ISS = 'https://authlib.org' -OAUTH2_JWT_KEY = 'secret-key' -OAUTH2_JWT_ALG = 'HS256' +# OPENID OAUTH2 JWT +# https://docs.authlib.org/en/latest/flask/2/openid-connect.html +# https://openid.net/specs/openid-connect-core-1_0.html#IDToken +OAUTH2_JWT_ENABLED = True # Not implemented +OAUTH2_JWT_KEY = 'secret-key' # REQUIRED Should be strong +OAUTH2_JWT_ISS = 'https://authlib.org' # REQUIRED Should be https, no query/fragment +OAUTH2_JWT_ALG = 'HS256' # REQUIRED unless no token ID +OAUTH2_JWT_EXP = 3600 # REQUIRED \ No newline at end of file