From a33da6fca245fd4ca8347d4b5ebf5d2333a9ee7e Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 29 Jul 2021 01:59:42 +0530 Subject: [PATCH 01/65] Initial OAuth Work commit --- metabrainz/new_oauth/__init__.py | 0 metabrainz/new_oauth/authorization_grant.py | 33 +++++++++++++++++++++ metabrainz/new_oauth/models/__init__.py | 3 ++ metabrainz/new_oauth/models/client.py | 13 ++++++++ metabrainz/new_oauth/models/code.py | 10 +++++++ metabrainz/new_oauth/models/token.py | 19 ++++++++++++ metabrainz/new_oauth/models/user.py | 6 ++++ metabrainz/new_oauth/provider.py | 21 +++++++++++++ metabrainz/new_oauth/views.py | 30 +++++++++++++++++++ requirements.txt | 5 ++++ 10 files changed, 140 insertions(+) create mode 100644 metabrainz/new_oauth/__init__.py create mode 100644 metabrainz/new_oauth/authorization_grant.py create mode 100644 metabrainz/new_oauth/models/__init__.py create mode 100644 metabrainz/new_oauth/models/client.py create mode 100644 metabrainz/new_oauth/models/code.py create mode 100644 metabrainz/new_oauth/models/token.py create mode 100644 metabrainz/new_oauth/models/user.py create mode 100644 metabrainz/new_oauth/provider.py create mode 100644 metabrainz/new_oauth/views.py diff --git a/metabrainz/new_oauth/__init__.py b/metabrainz/new_oauth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py new file mode 100644 index 00000000..69300145 --- /dev/null +++ b/metabrainz/new_oauth/authorization_grant.py @@ -0,0 +1,33 @@ +from authlib.oauth2.rfc6749 import grants + +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.code import OAuth2AuthorizationCode +from metabrainz.new_oauth.models.user import User + + +class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): + def save_authorization_code(self, code, request): + client = request.client + auth_code = OAuth2AuthorizationCode( + code=code, + client_id=client.client_id, + redirect_uri=request.redirect_uri, + scope=request.scope, + user_id=request.user.id, + ) + db.session.add(auth_code) + db.session.commit() + return auth_code + + 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(): + return item + + def delete_authorization_code(self, authorization_code): + db.session.delete(authorization_code) + db.session.commit() + + def authenticate_user(self, authorization_code): + return User.query.get(authorization_code.user_id) diff --git a/metabrainz/new_oauth/models/__init__.py b/metabrainz/new_oauth/models/__init__.py new file mode 100644 index 00000000..2e1eeb63 --- /dev/null +++ b/metabrainz/new_oauth/models/__init__.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py new file mode 100644 index 00000000..a5deba81 --- /dev/null +++ b/metabrainz/new_oauth/models/client.py @@ -0,0 +1,13 @@ +from metabrainz.new_oauth.models import db +from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin + + +class OAuth2Client(db.Model, OAuth2ClientMixin): + __tablename__ = 'oauth2_client' + + id = db.Column(db.Integer, primary_key=True) + + # we probably want to do the migration in stages, so if the users are migrated + # at a different time than clients, we can temporarily drop the FK. + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') diff --git a/metabrainz/new_oauth/models/code.py b/metabrainz/new_oauth/models/code.py new file mode 100644 index 00000000..1b6290bd --- /dev/null +++ b/metabrainz/new_oauth/models/code.py @@ -0,0 +1,10 @@ +from authlib.integrations.sqla_oauth2 import OAuth2AuthorizationCodeMixin +from metabrainz.new_oauth.models import db + + +class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): + __tablename__ = 'oauth2_code' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py new file mode 100644 index 00000000..b60c8e03 --- /dev/null +++ b/metabrainz/new_oauth/models/token.py @@ -0,0 +1,19 @@ +import time + +from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin + +from metabrainz.model import db + + +class OAuth2Token(db.Model, OAuth2TokenMixin): + __tablename__ = 'oauth2_token' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) + user = db.relationship('User') + + def is_refresh_token_active(self): + if self.revoked: + return False + expires_at = self.issued_at + self.expires_in * 2 + return expires_at >= time.time() diff --git a/metabrainz/new_oauth/models/user.py b/metabrainz/new_oauth/models/user.py new file mode 100644 index 00000000..7d82f47c --- /dev/null +++ b/metabrainz/new_oauth/models/user.py @@ -0,0 +1,6 @@ +from mbdata.models import Editor + + +class User(Editor): + def get_user_id(self): + return self.id diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py new file mode 100644 index 00000000..1c765e8c --- /dev/null +++ b/metabrainz/new_oauth/provider.py @@ -0,0 +1,21 @@ +from authlib.integrations.sqla_oauth2 import ( + create_query_client_func, + create_save_token_func +) +from authlib.integrations.flask_oauth2 import AuthorizationServer + +from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.client import OAuth2Client +from metabrainz.new_oauth.models.token import OAuth2Token + +query_client = create_query_client_func(db.session, OAuth2Client) +save_token = create_save_token_func(db.session, OAuth2Token) + +# TODO: We can also configure the expiry time and token generation function +# for the server. Its simple and will also be nice to prefix our tokens +# with meb. Rationale: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ +authorization_server = AuthorizationServer(query_client=query_client, save_token=save_token) + +authorization_server.register_grant(AuthorizationCodeGrant) + diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py new file mode 100644 index 00000000..f15035b8 --- /dev/null +++ b/metabrainz/new_oauth/views.py @@ -0,0 +1,30 @@ +from authlib.oauth2 import OAuth2Error +from flask import Blueprint, request, render_template +from flask_login import login_required, current_user + +from metabrainz.new_oauth.provider import authorization_server +from metabrainz.utils import build_url + +new_oauth_bp = Blueprint('new_oauth', __name__) + + +@new_oauth_bp.route('/authorize', methods=['GET', 'POST']) +@login_required +def authorize_prompt(): + """ OAuth 2.0 authorization endpoint. """ + redirect_uri = request.args.get('redirect_uri') + if request.method == 'GET': # Client requests access + try: + grant = authorization_server.validate_consent_request(end_user=current_user) + except OAuth2Error as error: + return error.error # FIXME: Add oauth error page + return render_template('oauth/prompt.html', client=grant.client, scope=grant.request.scope, + cancel_url=build_url(redirect_uri, dict(error='access_denied')), + hide_navbar_links=True, hide_footer=True) + if request.method == 'POST': # User grants access to the client + return authorization_server.create_authorization_response(grant_user=current_user) + + +@new_oauth_bp.route('/oauth/token', methods=['POST']) +def issue_token(): + return authorization_server.create_token_response() diff --git a/requirements.txt b/requirements.txt index 497b21ed..c34b2781 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,11 @@ sentry-sdk[flask]==1.14.0 six==1.16.0 SQLAlchemy==1.4.18 sqlalchemy-dst>=1.0.1 +MarkupSafe==2.0.1 +itsdangerous==2.0.1 +importlib-metadata>=3.10.0 +sqlalchemy-dst==1.0.1 +Authlib==0.15.4 stripe==2.60.0 Werkzeug==2.3.3 WTForms==2.3.3 From e0bda641effacbe6182fe37aabcd2fa36ac933ed Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 29 Jul 2021 17:11:09 +0530 Subject: [PATCH 02/65] Add CodeChallenge extension to authorization code flow --- metabrainz/new_oauth/authorization_grant.py | 6 ++++++ metabrainz/new_oauth/provider.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index 69300145..e58ed492 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -8,12 +8,18 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): def save_authorization_code(self, code, request): client = request.client + + code_challenge = request.data.get('code_challenge') + code_challenge_method = request.data.get('code_challenge_method') + auth_code = OAuth2AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, scope=request.scope, user_id=request.user.id, + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, ) db.session.add(auth_code) db.session.commit() diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index 1c765e8c..3d32dd1e 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -3,6 +3,7 @@ create_save_token_func ) from authlib.integrations.flask_oauth2 import AuthorizationServer +from authlib.oauth2.rfc7636 import CodeChallenge from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant from metabrainz.new_oauth.models import db @@ -17,5 +18,5 @@ # with meb. Rationale: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ authorization_server = AuthorizationServer(query_client=query_client, save_token=save_token) -authorization_server.register_grant(AuthorizationCodeGrant) +authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) From e628c59c654c01c2f49cd558c297844887889114 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 29 Jul 2021 17:26:48 +0530 Subject: [PATCH 03/65] Add ImplicitGrant --- metabrainz/new_oauth/provider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index 3d32dd1e..da5e74a5 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -3,6 +3,7 @@ create_save_token_func ) from authlib.integrations.flask_oauth2 import AuthorizationServer +from authlib.oauth2.rfc6749 import ImplicitGrant from authlib.oauth2.rfc7636 import CodeChallenge from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant @@ -19,4 +20,5 @@ authorization_server = AuthorizationServer(query_client=query_client, save_token=save_token) authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) +authorization_server.register_grant(ImplicitGrant) From 88f773add8bc5a8f8d6578fc733ef24f5859b3e9 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 29 Jul 2021 18:49:29 +0530 Subject: [PATCH 04/65] Add RefreshTokenGrant --- metabrainz/new_oauth/provider.py | 3 ++- metabrainz/new_oauth/refresh_grant.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 metabrainz/new_oauth/refresh_grant.py diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index da5e74a5..20d03fa8 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -7,6 +7,7 @@ from authlib.oauth2.rfc7636 import CodeChallenge from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant +from metabrainz.new_oauth.refresh_grant import RefreshTokenGrant from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.token import OAuth2Token @@ -21,4 +22,4 @@ authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) authorization_server.register_grant(ImplicitGrant) - +authorization_server.register_grant(RefreshTokenGrant) diff --git a/metabrainz/new_oauth/refresh_grant.py b/metabrainz/new_oauth/refresh_grant.py new file mode 100644 index 00000000..6f29910c --- /dev/null +++ b/metabrainz/new_oauth/refresh_grant.py @@ -0,0 +1,21 @@ +from authlib.oauth2.rfc6749 import grants + +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.token import OAuth2Token +from metabrainz.new_oauth.models.user import User + + +class RefreshTokenGrant(grants.RefreshTokenGrant): + + def authenticate_refresh_token(self, refresh_token): + token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first() + if token and token.is_refresh_token_active(): + return token + + def authenticate_user(self, credential): + return User.query.get(credential.user_id) + + def revoke_old_credential(self, credential): + credential.revoked = True + db.session.add(credential) + db.session.commit() From 53d5c133cb26cd7d8e7b18ba31a2f1d89dc1f863 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Sat, 31 Jul 2021 18:08:52 +0530 Subject: [PATCH 05/65] Add revocation endpoint --- metabrainz/new_oauth/provider.py | 5 ++++- metabrainz/new_oauth/views.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index 20d03fa8..7e225c1c 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -1,6 +1,7 @@ from authlib.integrations.sqla_oauth2 import ( create_query_client_func, - create_save_token_func + create_save_token_func, + create_revocation_endpoint ) from authlib.integrations.flask_oauth2 import AuthorizationServer from authlib.oauth2.rfc6749 import ImplicitGrant @@ -14,6 +15,7 @@ query_client = create_query_client_func(db.session, OAuth2Client) save_token = create_save_token_func(db.session, OAuth2Token) +revoke_token = create_revocation_endpoint(db.session, OAuth2Token) # TODO: We can also configure the expiry time and token generation function # for the server. Its simple and will also be nice to prefix our tokens @@ -23,3 +25,4 @@ authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) authorization_server.register_grant(ImplicitGrant) authorization_server.register_grant(RefreshTokenGrant) +authorization_server.register_endpoint(revoke_token) diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index f15035b8..2f271abf 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -2,6 +2,7 @@ from flask import Blueprint, request, render_template from flask_login import login_required, current_user +from metabrainz.decorators import nocache, crossdomain from metabrainz.new_oauth.provider import authorization_server from metabrainz.utils import build_url @@ -25,6 +26,13 @@ def authorize_prompt(): return authorization_server.create_authorization_response(grant_user=current_user) -@new_oauth_bp.route('/oauth/token', methods=['POST']) -def issue_token(): +@new_oauth_bp.route('/token', methods=['POST']) +@nocache +@crossdomain() +def oauth_token_handler(): return authorization_server.create_token_response() + + +@new_oauth_bp.route('/revoke', methods=['POST']) +def revoke_token(): + return authorization_server.create_endpoint_response('revocation') From 847f0549041a71311638e6248c598bee97abf62a Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 15:34:46 +0530 Subject: [PATCH 06/65] second commit --- admin/sql/oauth/create_tables.sql | 41 +++++++++++++++++ docker/docker-compose.dev.yml | 2 + metabrainz/__init__.py | 31 +++++++++++++ metabrainz/new_oauth/authorization_grant.py | 7 ++- metabrainz/new_oauth/forms.py | 32 ++++++++++++++ metabrainz/new_oauth/models/__init__.py | 2 +- metabrainz/new_oauth/models/client.py | 9 ++-- metabrainz/new_oauth/models/code.py | 9 ++-- metabrainz/new_oauth/models/token.py | 12 +++-- metabrainz/new_oauth/models/user.py | 15 ++++++- metabrainz/new_oauth/provider.py | 9 ---- metabrainz/new_oauth/refresh_grant.py | 4 +- metabrainz/new_oauth/views.py | 44 ++++++++++++++++++- metabrainz/templates/oauth/create_client.html | 41 +++++++++++++++++ 14 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 admin/sql/oauth/create_tables.sql create mode 100644 metabrainz/new_oauth/forms.py create mode 100644 metabrainz/templates/oauth/create_client.html diff --git a/admin/sql/oauth/create_tables.sql b/admin/sql/oauth/create_tables.sql new file mode 100644 index 00000000..e9a80a7b --- /dev/null +++ b/admin/sql/oauth/create_tables.sql @@ -0,0 +1,41 @@ +CREATE TABLE oauth.client ( + id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, + client_id TEXT NOT NULL, -- PK + client_secret TEXT, -- public clients won't have client_secret so NULLABLE + owner_id INTEGER, -- (maybe FK?), user + client_name TEXT NOT NULL, + description TEXT NOT NULL, + website TEXT NOT NULL, + redirect_uris TEXT[] NOT NULL +); + +CREATE TABLE oauth.scopes ( + id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK + name TEXT NOT NULL, + description TEXT NOT NULL +); + +CREATE TABLE oauth.token ( + id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK + user_id INTEGER NOT NULL, -- FK, user + client_id INTEGER NOT NULL, -- FK, client + access_token TEXT NOT NULL, + refresh_token TEXT, + scopes INTEGER[] NOT NULL, + issued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + expires_in INTEGER NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE oauth.code ( + id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK + user_id INTEGER NOT NULL, -- FK, user + client_id INTEGER NOT NULL, -- FK, client + code TEXT NOT NULL UNIQUE, + redirect_uri TEXT NOT NULL, + response_type TEXT NOT NULL, + scopes INTEGER[] NOT NULL, + code_challenge TEXT, + code_challenge_method TEXT, + granted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 41837906..c4dbc299 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -11,6 +11,8 @@ services: context: .. dockerfile: ./docker/Dockerfile.dev command: python /code/manage.py runserver -h 0.0.0.0 -p 80 + environment: + AUTHLIB_INSECURE_TRANSPORT: true volumes: - ../data/replication_packets:/data/replication_packets - ../data/json_dumps:/data/json_dumps diff --git a/metabrainz/__init__.py b/metabrainz/__init__.py index f9107207..94003152 100644 --- a/metabrainz/__init__.py +++ b/metabrainz/__init__.py @@ -9,12 +9,23 @@ from metabrainz.admin.quickbooks.views import QuickBooksView from time import sleep +from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant +from metabrainz.new_oauth.provider import authorization_server, revoke_token + +from authlib.oauth2.rfc6749 import ImplicitGrant +from authlib.oauth2.rfc7636 import CodeChallenge + +from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant +from metabrainz.new_oauth.refresh_grant import RefreshTokenGrant + + # Check to see if we're running under a docker deployment. If so, don't second guess # the config file setup and just wait for the correct configuration to be generated. deploy_env = os.environ.get('DEPLOY_ENV', '') CONSUL_CONFIG_FILE_RETRY_COUNT = 10 + def create_app(debug=None, config_path = None): app = CustomFlask( @@ -120,6 +131,14 @@ def create_app(debug=None, config_path = None): LOGO_UPLOAD_SET, ]) + from metabrainz.new_oauth.models import db as new_oauth_db + + @app.before_first_request + def create_tables(): + new_oauth_db.create_all() + + config_oauth(app) + # Blueprints _register_blueprints(app) @@ -193,9 +212,21 @@ def _register_blueprints(app): from metabrainz.oauth.views import oauth_bp app.register_blueprint(oauth_bp, url_prefix='/oauth') + from metabrainz.new_oauth.views import new_oauth_bp + app.register_blueprint(new_oauth_bp, url_prefix='/new-oauth') from metabrainz.api.views.index import api_index_bp app.register_blueprint(api_index_bp, url_prefix='/api') from metabrainz.api.views.user import api_user_bp app.register_blueprint(api_user_bp, url_prefix='/api/user') from metabrainz.api.views.musicbrainz import api_musicbrainz_bp app.register_blueprint(api_musicbrainz_bp, url_prefix='/api/musicbrainz') + + +def config_oauth(app): + authorization_server.init_app(app) + + authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) + authorization_server.register_grant(ImplicitGrant) + authorization_server.register_grant(RefreshTokenGrant) + authorization_server.register_endpoint(revoke_token) + diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index e58ed492..422760d8 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -2,10 +2,13 @@ from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.code import OAuth2AuthorizationCode -from metabrainz.new_oauth.models.user import User +from metabrainz.new_oauth.models.user import OAuth2User class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): + + TOKEN_ENDPOINT_AUTH_METHODS = ['none', 'client_secret_post'] + def save_authorization_code(self, code, request): client = request.client @@ -36,4 +39,4 @@ def delete_authorization_code(self, authorization_code): db.session.commit() def authenticate_user(self, authorization_code): - return User.query.get(authorization_code.user_id) + return OAuth2User.query.get(authorization_code.user_id) diff --git a/metabrainz/new_oauth/forms.py b/metabrainz/new_oauth/forms.py new file mode 100644 index 00000000..357e7440 --- /dev/null +++ b/metabrainz/new_oauth/forms.py @@ -0,0 +1,32 @@ +from flask_wtf import FlaskForm +from flask_babel import lazy_gettext +from wtforms import StringField, validators, FieldList + + +class ApplicationForm(FlaskForm): + client_name = StringField(lazy_gettext('Application Name'), [ + validators.InputRequired(message=lazy_gettext("Application name field is empty.")), + validators.Length(min=3, message=lazy_gettext("Application name needs to be at least 3 characters long.")), + validators.Length(max=64, message=lazy_gettext("Application name needs to be at most 64 characters long.")) + ]) + description = StringField(lazy_gettext('Description'), [ + validators.InputRequired(message=lazy_gettext("Client description field is empty.")), + validators.Length(min=3, message=lazy_gettext("Client description needs to be at least 3 characters long.")), + validators.Length(max=512, message=lazy_gettext("Client description needs to be at most 512 characters long.")) + ]) + website = StringField(lazy_gettext('Homepage'), [ + validators.InputRequired(message=lazy_gettext("Homepage field is empty.")), + validators.URL(require_tld=False, message=lazy_gettext("Homepage is not a valid URI.")) + ]) + redirect_uri = FieldList(StringField(lazy_gettext('Authorization callback URL'), [ + validators.InputRequired(message=lazy_gettext("Authorization callback URL field is empty.")), + validators.URL(require_tld=False, message=lazy_gettext("Authorization callback URL is invalid.")) + ])) + + def validate_redirect_uri(self, field): + if not field.data.startswith(("http://", "https://")): + raise validators.ValidationError(lazy_gettext('Authorization callback URL must use http or https')) + + def validate_website(self, field): + if not field.data.startswith(("http://", "https://")): + raise validators.ValidationError(lazy_gettext('Homepage URL must use http or https')) diff --git a/metabrainz/new_oauth/models/__init__.py b/metabrainz/new_oauth/models/__init__.py index 2e1eeb63..f0b13d6f 100644 --- a/metabrainz/new_oauth/models/__init__.py +++ b/metabrainz/new_oauth/models/__init__.py @@ -1,3 +1,3 @@ from flask_sqlalchemy import SQLAlchemy -db = SQLAlchemy() \ No newline at end of file +db = SQLAlchemy() diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py index a5deba81..ceb235f1 100644 --- a/metabrainz/new_oauth/models/client.py +++ b/metabrainz/new_oauth/models/client.py @@ -3,11 +3,14 @@ class OAuth2Client(db.Model, OAuth2ClientMixin): - __tablename__ = 'oauth2_client' + __tablename__ = 'client' + __table_args__ = { + 'schema': 'oauth' + } id = db.Column(db.Integer, primary_key=True) # we probably want to do the migration in stages, so if the users are migrated # at a different time than clients, we can temporarily drop the FK. - user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) - user = db.relationship('User') + user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) + user = db.relationship('OAuth2User') diff --git a/metabrainz/new_oauth/models/code.py b/metabrainz/new_oauth/models/code.py index 1b6290bd..7a828453 100644 --- a/metabrainz/new_oauth/models/code.py +++ b/metabrainz/new_oauth/models/code.py @@ -3,8 +3,11 @@ class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): - __tablename__ = 'oauth2_code' + __tablename__ = 'code' + __table_args__ = { + 'schema': 'oauth' + } id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) - user = db.relationship('User') + user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) + user = db.relationship('OAuth2User') diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index b60c8e03..2e408238 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -2,15 +2,19 @@ from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin -from metabrainz.model import db +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.user import OAuth2User class OAuth2Token(db.Model, OAuth2TokenMixin): - __tablename__ = 'oauth2_token' + __tablename__ = 'token' + __table_args__ = { + 'schema': 'oauth' + } id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) - user = db.relationship('User') + user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) + user = db.relationship(OAuth2User) def is_refresh_token_active(self): if self.revoked: diff --git a/metabrainz/new_oauth/models/user.py b/metabrainz/new_oauth/models/user.py index 7d82f47c..97eb3315 100644 --- a/metabrainz/new_oauth/models/user.py +++ b/metabrainz/new_oauth/models/user.py @@ -1,6 +1,17 @@ -from mbdata.models import Editor +from metabrainz.new_oauth.models import db -class User(Editor): +class OAuth2User(db.Model): + __tablename__ = "user" + __table_args__ = { + "schema": "oauth" + } + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), nullable=False) + + def __str__(self): + return self.name + def get_user_id(self): return self.id diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index 7e225c1c..9555578c 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -4,11 +4,7 @@ create_revocation_endpoint ) from authlib.integrations.flask_oauth2 import AuthorizationServer -from authlib.oauth2.rfc6749 import ImplicitGrant -from authlib.oauth2.rfc7636 import CodeChallenge -from metabrainz.new_oauth.authorization_grant import AuthorizationCodeGrant -from metabrainz.new_oauth.refresh_grant import RefreshTokenGrant from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.token import OAuth2Token @@ -21,8 +17,3 @@ # for the server. Its simple and will also be nice to prefix our tokens # with meb. Rationale: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ authorization_server = AuthorizationServer(query_client=query_client, save_token=save_token) - -authorization_server.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=False)]) -authorization_server.register_grant(ImplicitGrant) -authorization_server.register_grant(RefreshTokenGrant) -authorization_server.register_endpoint(revoke_token) diff --git a/metabrainz/new_oauth/refresh_grant.py b/metabrainz/new_oauth/refresh_grant.py index 6f29910c..bbf5b079 100644 --- a/metabrainz/new_oauth/refresh_grant.py +++ b/metabrainz/new_oauth/refresh_grant.py @@ -2,7 +2,7 @@ from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.token import OAuth2Token -from metabrainz.new_oauth.models.user import User +from metabrainz.new_oauth.models.user import OAuth2User class RefreshTokenGrant(grants.RefreshTokenGrant): @@ -13,7 +13,7 @@ def authenticate_refresh_token(self, refresh_token): return token def authenticate_user(self, credential): - return User.query.get(credential.user_id) + return OAuth2User.query.get(credential.user_id) def revoke_old_credential(self, credential): credential.revoked = True diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index 2f271abf..f7994fe9 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -1,14 +1,56 @@ +import time + from authlib.oauth2 import OAuth2Error -from flask import Blueprint, request, render_template +from flask import Blueprint, request, render_template, redirect from flask_login import login_required, current_user from metabrainz.decorators import nocache, crossdomain +from metabrainz.new_oauth.forms import ApplicationForm +from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.provider import authorization_server +from metabrainz.new_oauth.models import db from metabrainz.utils import build_url +from werkzeug.security import gen_salt new_oauth_bp = Blueprint('new_oauth', __name__) +@new_oauth_bp.route('/create_client', methods=('GET', 'POST')) +@login_required +def create_client(): + form = ApplicationForm() + if form.validate_on_submit(): + client_id = gen_salt(24) + client_id_issued_at = int(time.time()) + client = OAuth2Client( + client_id=client_id, + client_id_issued_at=client_id_issued_at, + user_id=current_user.id, + ) + client_metadata = { + "client_name": form.client_name, + "client_uri": form["client_uri"], + "grant_types": split_by_crlf(form["grant_type"]), + "redirect_uris": split_by_crlf(form["redirect_uri"]), + "response_types": split_by_crlf(form["response_type"]), + "scope": form["scope"], + "token_endpoint_auth_method": form["token_endpoint_auth_method"] + } + client.set_client_metadata(client_metadata) + + if form['token_endpoint_auth_method'] == 'none': + client.client_secret = '' + else: + client.client_secret = gen_salt(48) + + db.session.add(client) + db.session.commit() + else: + return render_template('oauth/create_client.html', form=form) + + return redirect('/') + + @new_oauth_bp.route('/authorize', methods=['GET', 'POST']) @login_required def authorize_prompt(): diff --git a/metabrainz/templates/oauth/create_client.html b/metabrainz/templates/oauth/create_client.html new file mode 100644 index 00000000..62e090c6 --- /dev/null +++ b/metabrainz/templates/oauth/create_client.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} + +{% block title %}{{ _('Create new application') }} - MetaBrainz{% endblock %} + +{% block content %} +

{{ _('Create new application') }}

+
+ + {% for field in form.errors %} + {% for error in form.errors[field] %} +
{{ error }}
+ {% endfor %} + {% endfor %} + +
+ {{ form.hidden_tag() }} +
+
+ +
{{ form.name(class="form-control", required="required") }}
+
+
+ +
{{ form.desc(class="form-control", required="required") }}
+
+
+ +
{{ form.website(class="form-control", required="required") }}
+
+
+ +
{{ form.redirect_uri(class="form-control", required="required") }}
+
+
+
+
+ +
+
+
+{% endblock %} From 2f34f153eba4c984ff7186f840fc51f2f22ee12d Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 18:53:50 +0530 Subject: [PATCH 07/65] Update models --- metabrainz/new_oauth/models/__init__.py | 2 + metabrainz/new_oauth/models/client.py | 52 ++++++++++++++++--- metabrainz/new_oauth/models/code.py | 41 ++++++++++++--- metabrainz/new_oauth/models/relation_scope.py | 24 +++++++++ metabrainz/new_oauth/models/scope.py | 15 ++++++ metabrainz/new_oauth/models/token.py | 48 ++++++++++++----- metabrainz/new_oauth/models/user.py | 28 ++++++---- requirements.txt | 10 ++-- 8 files changed, 173 insertions(+), 47 deletions(-) create mode 100644 metabrainz/new_oauth/models/relation_scope.py create mode 100644 metabrainz/new_oauth/models/scope.py diff --git a/metabrainz/new_oauth/models/__init__.py b/metabrainz/new_oauth/models/__init__.py index f0b13d6f..7e6ea2f3 100644 --- a/metabrainz/new_oauth/models/__init__.py +++ b/metabrainz/new_oauth/models/__init__.py @@ -1,3 +1,5 @@ from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import declarative_base db = SQLAlchemy() +Base = declarative_base() diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py index ceb235f1..c2951acf 100644 --- a/metabrainz/new_oauth/models/client.py +++ b/metabrainz/new_oauth/models/client.py @@ -1,16 +1,52 @@ -from metabrainz.new_oauth.models import db -from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin +from authlib.oauth2.rfc6749 import ClientMixin +from sqlalchemy import Column, Text, ForeignKey, Integer, ARRAY, Identity +from sqlalchemy.orm import relationship -class OAuth2Client(db.Model, OAuth2ClientMixin): +from metabrainz.new_oauth.models import Base + + +class OAuth2Client(Base, ClientMixin): + __tablename__ = 'client' __table_args__ = { 'schema': 'oauth' } - id = db.Column(db.Integer, primary_key=True) + id = Column(Integer, Identity(), primary_key=True) + client_id = Column(Text, nullable=False) + client_secret = Column(Text) + owner_id = Column(Integer, ForeignKey('oauth.user.id', ondelete='CASCADE'), nullable=False) + name = Column(Text, nullable=False) + description = Column(Text, nullable=False) + website = Column(Text) + redirect_uris = Column(ARRAY(Text), nullable=False) + + user = relationship('OAuth2User') + + def get_client_id(self): + pass + + def get_default_redirect_uri(self): + pass + + def get_allowed_scope(self, scope): + pass + + def check_redirect_uri(self, redirect_uri): + pass + + def has_client_secret(self): + pass + + def check_client_secret(self, client_secret): + pass + + def check_token_endpoint_auth_method(self, method): + pass + + def check_response_type(self, response_type): + pass - # we probably want to do the migration in stages, so if the users are migrated - # at a different time than clients, we can temporarily drop the FK. - user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) - user = db.relationship('OAuth2User') + def check_grant_type(self, grant_type): + pass diff --git a/metabrainz/new_oauth/models/code.py b/metabrainz/new_oauth/models/code.py index 7a828453..d304b9a4 100644 --- a/metabrainz/new_oauth/models/code.py +++ b/metabrainz/new_oauth/models/code.py @@ -1,13 +1,38 @@ -from authlib.integrations.sqla_oauth2 import OAuth2AuthorizationCodeMixin -from metabrainz.new_oauth.models import db +from authlib.oauth2.rfc6749 import AuthorizationCodeMixin +from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime +from sqlalchemy.orm import relationship +from sqlalchemy.sql.schema import Identity +from metabrainz.new_oauth.models import Base +from metabrainz.new_oauth.models.client import OAuth2Client +from metabrainz.new_oauth.models.relation_scope import OAuth2CodeScope +from metabrainz.new_oauth.models.user import OAuth2User -class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): - __tablename__ = 'code' + +class OAuth2AuthorizationCode(Base, AuthorizationCodeMixin): + + __tablename__ = "code" __table_args__ = { - 'schema': 'oauth' + "schema": "oauth" } - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) - user = db.relationship('OAuth2User') + id = Column(Integer, Identity(), primary_key=True) + user_id = Column(Integer, ForeignKey("oauth.user.id", ondelete="CASCADE"), nullable=False) + client_id = Column(Integer, ForeignKey("oauth.client.id", ondelete="CASCADE"), nullable=False) + code = Column(Text, nullable=False, unique=True) + redirect_uri = Column(Text, nullable=False) + response_type = Column(Text, nullable=False) + code_challenge = Column(Text) + code_challenge_method = Column(Text) + granted_at = Column(DateTime(timezone=True)) + + user = relationship(OAuth2User) + client = relationship(OAuth2Client) + scopes = relationship("OAuth2Scope", secondary=OAuth2CodeScope) + + def get_redirect_uri(self): + pass + + def get_scope(self): + pass + diff --git a/metabrainz/new_oauth/models/relation_scope.py b/metabrainz/new_oauth/models/relation_scope.py new file mode 100644 index 00000000..53a8f1fb --- /dev/null +++ b/metabrainz/new_oauth/models/relation_scope.py @@ -0,0 +1,24 @@ +from sqlalchemy import Integer +from sqlalchemy.sql.schema import Identity, Column, ForeignKey + +from metabrainz.new_oauth.models import Base + + +class OAuth2TokenScope(Base): + __tablename__ = "l_token_scope" + __table_args__ = { + "schema": "oauth" + } + id = Column(Integer, Identity(), primary_key=True) + token_id = Column(Integer, ForeignKey("oauth.token.id", ondelete="CASCADE"), nullable=False) + scope_id = Column(Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False) + + +class OAuth2CodeScope(Base): + __tablename__ = "l_code_scope" + __table_args__ = { + "schema": "oauth" + } + id = Column(Integer, Identity(), primary_key=True) + code_id = Column(Integer, ForeignKey("oauth.code.id", ondelete="CASCADE"), nullable=False) + scope_id = Column(Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False) diff --git a/metabrainz/new_oauth/models/scope.py b/metabrainz/new_oauth/models/scope.py new file mode 100644 index 00000000..20942456 --- /dev/null +++ b/metabrainz/new_oauth/models/scope.py @@ -0,0 +1,15 @@ +from sqlalchemy import Integer, Text +from sqlalchemy.sql.schema import Identity, Column + +from metabrainz.new_oauth.models import Base + + +class OAuth2Scope(Base): + __tablename__ = 'scope' + __table_args__ = { + 'schema': 'oauth' + } + + id = Column(Integer, Identity(), primary_key=True) + name = Column(Text, nullable=False) + description = Column(Text, nullable=False) diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index 2e408238..ca0e28d7 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -1,23 +1,43 @@ -import time +from authlib.oauth2.rfc6749 import TokenMixin +from sqlalchemy import func, Column, Integer, DateTime, Text, ForeignKey, Boolean, Identity +from sqlalchemy.orm import relationship -from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin - -from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models import Base +from metabrainz.new_oauth.models.client import OAuth2Client +from metabrainz.new_oauth.models.relation_scope import OAuth2TokenScope from metabrainz.new_oauth.models.user import OAuth2User -class OAuth2Token(db.Model, OAuth2TokenMixin): - __tablename__ = 'token' +class OAuth2Token(Base, TokenMixin): + __tablename__ = "token" __table_args__ = { - 'schema': 'oauth' + "schema": "oauth" } - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('oauth.user.id', ondelete='CASCADE')) - user = db.relationship(OAuth2User) + id = Column(Integer, Identity(), primary_key=True) + user_id = Column(Integer, ForeignKey("oauth.user.id", ondelete="CASCADE"), nullable=False) + client_id = Column(Integer, ForeignKey("oauth.client.id", ondelete="CASCADE"), nullable=False) + access_token = Column(Text, nullable=False, unique=True) + refresh_token = Column(Text, index=True) # nullable, because not all grants have refresh token + issued_at = Column(DateTime(timezone=True), default=func.now()) + expires_in = Column(Integer) + revoked = Column(Boolean, default=False) + + user = relationship(OAuth2User) + client = relationship(OAuth2Client) + scopes = relationship("OAuth2Scope", secondary=OAuth2TokenScope) + + def get_client_id(self): + return self.client_id + + def get_scope(self): + pass + + def get_expires_in(self): + return self.expires_in + + def get_expires_at(self): + pass def is_refresh_token_active(self): - if self.revoked: - return False - expires_at = self.issued_at + self.expires_in * 2 - return expires_at >= time.time() + return self.revoked diff --git a/metabrainz/new_oauth/models/user.py b/metabrainz/new_oauth/models/user.py index 97eb3315..dede769a 100644 --- a/metabrainz/new_oauth/models/user.py +++ b/metabrainz/new_oauth/models/user.py @@ -1,17 +1,25 @@ -from metabrainz.new_oauth.models import db +from sqlalchemy import Column, Integer, Identity, Text, DateTime, func, Date, Boolean +from metabrainz.new_oauth.models import Base -class OAuth2User(db.Model): + +class OAuth2User(Base): __tablename__ = "user" __table_args__ = { "schema": "oauth" } - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), nullable=False) - - def __str__(self): - return self.name - - def get_user_id(self): - return self.id + id = Column(Integer, Identity(), primary_key=True) + name = Column(Text, nullable=False) + email = Column(Text) + unconfirmed_email = Column(Text) + website = Column(Text) + member_since = Column(DateTime(timezone=True), default=func.now()) + email_confirm_date = Column(DateTime(timezone=True)) + last_login_date = Column(DateTime(timezone=True), default=func.now()) + last_updated = Column(DateTime(timezone=True), default=func.now()) + birth_date = Column(Date) + gender = Column(Integer) # TODO: add FK to gender.id + password = Column(Text, nullable=False) + ha1 = Column(Text, nullable=False) + deleted = Column(Boolean, default=False) diff --git a/requirements.txt b/requirements.txt index c34b2781..a5271068 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +Authlib==0.15.4 certifi click==8.1.3 email_validator==1.0.5 @@ -32,13 +33,8 @@ reportlab==3.6.12 requests==2.28.2 sentry-sdk[flask]==1.14.0 six==1.16.0 -SQLAlchemy==1.4.18 -sqlalchemy-dst>=1.0.1 -MarkupSafe==2.0.1 -itsdangerous==2.0.1 -importlib-metadata>=3.10.0 -sqlalchemy-dst==1.0.1 -Authlib==0.15.4 stripe==2.60.0 +SQLAlchemy==1.4.41 +sqlalchemy-dst>=1.0.1 Werkzeug==2.3.3 WTForms==2.3.3 From eeee36a43bcc1c06576c6b147658e31cf55afe7a Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 19:14:43 +0530 Subject: [PATCH 08/65] misc updates --- metabrainz/new_oauth/authorization_grant.py | 17 +++++++++++++---- metabrainz/new_oauth/models/client.py | 3 ++- metabrainz/new_oauth/models/scope.py | 7 +++++++ metabrainz/new_oauth/refresh_grant.py | 12 ++++++++++-- metabrainz/new_oauth/views.py | 10 +++++----- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index 422760d8..91d41de6 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -2,6 +2,7 @@ from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.code import OAuth2AuthorizationCode +from metabrainz.new_oauth.models.scope import get_scopes from metabrainz.new_oauth.models.user import OAuth2User @@ -15,11 +16,13 @@ def save_authorization_code(self, code, request): code_challenge = request.data.get('code_challenge') code_challenge_method = request.data.get('code_challenge_method') + scopes = get_scopes(db.session, request.scope) + auth_code = OAuth2AuthorizationCode( code=code, client_id=client.client_id, redirect_uri=request.redirect_uri, - scope=request.scope, + scopes=scopes, user_id=request.user.id, code_challenge=code_challenge, code_challenge_method=code_challenge_method, @@ -29,8 +32,10 @@ def save_authorization_code(self, code, request): return auth_code def query_authorization_code(self, code, client): - item = OAuth2AuthorizationCode.query.filter_by( - code=code, client_id=client.client_id).first() + item = db.session\ + .query(OAuth2AuthorizationCode)\ + .filter_by(code=code, client_id=client.client_id)\ + .first() if item and not item.is_expired(): return item @@ -39,4 +44,8 @@ def delete_authorization_code(self, authorization_code): db.session.commit() def authenticate_user(self, authorization_code): - return OAuth2User.query.get(authorization_code.user_id) + # TODO: fix impl + # return db.session\ + # .query(OAuth2User)\ + # .filter_by(user_id=authorization_code.user_id) + pass diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py index c2951acf..7eacc33f 100644 --- a/metabrainz/new_oauth/models/client.py +++ b/metabrainz/new_oauth/models/client.py @@ -1,5 +1,5 @@ from authlib.oauth2.rfc6749 import ClientMixin -from sqlalchemy import Column, Text, ForeignKey, Integer, ARRAY, Identity +from sqlalchemy import Column, Text, ForeignKey, Integer, ARRAY, Identity, DateTime, func from sqlalchemy.orm import relationship @@ -21,6 +21,7 @@ class OAuth2Client(Base, ClientMixin): description = Column(Text, nullable=False) website = Column(Text) redirect_uris = Column(ARRAY(Text), nullable=False) + client_id_issued_at = Column(DateTime(timezone=True), nullable=False, default=func.now()) user = relationship('OAuth2User') diff --git a/metabrainz/new_oauth/models/scope.py b/metabrainz/new_oauth/models/scope.py index 20942456..4fab4649 100644 --- a/metabrainz/new_oauth/models/scope.py +++ b/metabrainz/new_oauth/models/scope.py @@ -1,3 +1,4 @@ +import sqlalchemy.orm from sqlalchemy import Integer, Text from sqlalchemy.sql.schema import Identity, Column @@ -13,3 +14,9 @@ class OAuth2Scope(Base): id = Column(Integer, Identity(), primary_key=True) name = Column(Text, nullable=False) description = Column(Text, nullable=False) + + +def get_scopes(session: sqlalchemy.orm.Session, scope) -> list[OAuth2Scope]: + """ Given a comma separated scope string return associated scope objects from db """ + scopes = scope.split(",") + return session.query(OAuth2Scope).filter(OAuth2Scope.name.in_(scopes)) diff --git a/metabrainz/new_oauth/refresh_grant.py b/metabrainz/new_oauth/refresh_grant.py index bbf5b079..f82f7df9 100644 --- a/metabrainz/new_oauth/refresh_grant.py +++ b/metabrainz/new_oauth/refresh_grant.py @@ -8,12 +8,20 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token): - token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first() + token = db.session\ + .query(OAuth2Token)\ + .filter_by(refresh_token=refresh_token)\ + .first() if token and token.is_refresh_token_active(): return token def authenticate_user(self, credential): - return OAuth2User.query.get(credential.user_id) + # TODO: fix impl + # return db.session\ + # .query(OAuth2User)\ + # .filter_by(user_id=credential.user_id)\ + # .first() + pass def revoke_old_credential(self, credential): credential.revoked = True diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index f7994fe9..4863c18e 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -1,5 +1,3 @@ -import time - from authlib.oauth2 import OAuth2Error from flask import Blueprint, request, render_template, redirect from flask_login import login_required, current_user @@ -21,11 +19,9 @@ def create_client(): form = ApplicationForm() if form.validate_on_submit(): client_id = gen_salt(24) - client_id_issued_at = int(time.time()) client = OAuth2Client( client_id=client_id, - client_id_issued_at=client_id_issued_at, - user_id=current_user.id, + owner_id=current_user.id, ) client_metadata = { "client_name": form.client_name, @@ -78,3 +74,7 @@ def oauth_token_handler(): @new_oauth_bp.route('/revoke', methods=['POST']) def revoke_token(): return authorization_server.create_endpoint_response('revocation') + + +def split_by_crlf(s): + return [v for v in s.splitlines() if v] From 015775b63205b30f12d52805a2e94a45c51cc77d Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 19:47:07 +0530 Subject: [PATCH 09/65] update authlib and implementation --- metabrainz/new_oauth/authorization_grant.py | 2 +- metabrainz/new_oauth/models/client.py | 20 +++++++++++--------- metabrainz/new_oauth/models/code.py | 6 +++--- metabrainz/new_oauth/models/scope.py | 3 ++- metabrainz/new_oauth/models/token.py | 9 ++++++--- metabrainz/new_oauth/models/user.py | 3 +++ metabrainz/new_oauth/views.py | 21 ++++++++++++--------- 7 files changed, 38 insertions(+), 26 deletions(-) diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index 91d41de6..f4b5fba2 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -44,7 +44,7 @@ def delete_authorization_code(self, authorization_code): db.session.commit() def authenticate_user(self, authorization_code): - # TODO: fix impl + # TODO: fix authenticate_user # return db.session\ # .query(OAuth2User)\ # .filter_by(user_id=authorization_code.user_id) diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py index 7eacc33f..8b13486c 100644 --- a/metabrainz/new_oauth/models/client.py +++ b/metabrainz/new_oauth/models/client.py @@ -26,28 +26,30 @@ class OAuth2Client(Base, ClientMixin): user = relationship('OAuth2User') def get_client_id(self): - pass + return self.client_id def get_default_redirect_uri(self): - pass + if self.redirect_uris: + return self.redirect_uris[0] + return None def get_allowed_scope(self, scope): - pass + pass # TODO: Fix allowed scopes def check_redirect_uri(self, redirect_uri): - pass + return redirect_uri in self.redirect_uris def has_client_secret(self): - pass + return bool(self.client_secret) def check_client_secret(self, client_secret): - pass + return self.client_secret == client_secret def check_token_endpoint_auth_method(self, method): - pass + return True # TODO: Fix token endpoint auth def check_response_type(self, response_type): - pass + return True # TODO: Fix response types def check_grant_type(self, grant_type): - pass + return True # TODO: Fix grant types diff --git a/metabrainz/new_oauth/models/code.py b/metabrainz/new_oauth/models/code.py index d304b9a4..27fdfa30 100644 --- a/metabrainz/new_oauth/models/code.py +++ b/metabrainz/new_oauth/models/code.py @@ -1,4 +1,5 @@ from authlib.oauth2.rfc6749 import AuthorizationCodeMixin +from authlib.oauth2.rfc6749.util import scope_to_list from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime from sqlalchemy.orm import relationship from sqlalchemy.sql.schema import Identity @@ -31,8 +32,7 @@ class OAuth2AuthorizationCode(Base, AuthorizationCodeMixin): scopes = relationship("OAuth2Scope", secondary=OAuth2CodeScope) def get_redirect_uri(self): - pass + return self.redirect_uri def get_scope(self): - pass - + return scope_to_list([s.name for s in self.scopes]) diff --git a/metabrainz/new_oauth/models/scope.py b/metabrainz/new_oauth/models/scope.py index 4fab4649..2185aa8c 100644 --- a/metabrainz/new_oauth/models/scope.py +++ b/metabrainz/new_oauth/models/scope.py @@ -1,4 +1,5 @@ import sqlalchemy.orm +from authlib.oauth2.rfc6749.util import scope_to_list from sqlalchemy import Integer, Text from sqlalchemy.sql.schema import Identity, Column @@ -18,5 +19,5 @@ class OAuth2Scope(Base): def get_scopes(session: sqlalchemy.orm.Session, scope) -> list[OAuth2Scope]: """ Given a comma separated scope string return associated scope objects from db """ - scopes = scope.split(",") + scopes = scope_to_list(scope) return session.query(OAuth2Scope).filter(OAuth2Scope.name.in_(scopes)) diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index ca0e28d7..3088f627 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -1,8 +1,11 @@ +from datetime import timedelta + from authlib.oauth2.rfc6749 import TokenMixin +from authlib.oauth2.rfc6749.util import scope_to_list from sqlalchemy import func, Column, Integer, DateTime, Text, ForeignKey, Boolean, Identity from sqlalchemy.orm import relationship -from metabrainz.new_oauth.models import Base +from metabrainz.new_oauth.models import Base, db from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.relation_scope import OAuth2TokenScope from metabrainz.new_oauth.models.user import OAuth2User @@ -31,13 +34,13 @@ def get_client_id(self): return self.client_id def get_scope(self): - pass + return scope_to_list([s.name for s in self.scopes]) def get_expires_in(self): return self.expires_in def get_expires_at(self): - pass + return self.issued_at + timedelta(seconds=self.expires_in) def is_refresh_token_active(self): return self.revoked diff --git a/metabrainz/new_oauth/models/user.py b/metabrainz/new_oauth/models/user.py index dede769a..799c4e2d 100644 --- a/metabrainz/new_oauth/models/user.py +++ b/metabrainz/new_oauth/models/user.py @@ -23,3 +23,6 @@ class OAuth2User(Base): password = Column(Text, nullable=False) ha1 = Column(Text, nullable=False) deleted = Column(Boolean, default=False) + + def get_user_id(self): + return self.id diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index 4863c18e..f372d10a 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -5,6 +5,7 @@ from metabrainz.decorators import nocache, crossdomain from metabrainz.new_oauth.forms import ApplicationForm from metabrainz.new_oauth.models.client import OAuth2Client +from metabrainz.new_oauth.models.scope import get_scopes from metabrainz.new_oauth.provider import authorization_server from metabrainz.new_oauth.models import db from metabrainz.utils import build_url @@ -22,20 +23,21 @@ def create_client(): client = OAuth2Client( client_id=client_id, owner_id=current_user.id, + name=form.client_name, + website=form["client_uri"], + redirect_uris=split_by_crlf(form["redirect_uri"]), + ) + # TODO: Fix use of these columns client_metadata = { - "client_name": form.client_name, - "client_uri": form["client_uri"], "grant_types": split_by_crlf(form["grant_type"]), - "redirect_uris": split_by_crlf(form["redirect_uri"]), "response_types": split_by_crlf(form["response_type"]), "scope": form["scope"], "token_endpoint_auth_method": form["token_endpoint_auth_method"] } - client.set_client_metadata(client_metadata) - if form['token_endpoint_auth_method'] == 'none': - client.client_secret = '' + if form["token_endpoint_auth_method"] == "none": + client.client_secret = "" else: client.client_secret = gen_salt(48) @@ -58,9 +60,10 @@ def authorize_prompt(): except OAuth2Error as error: return error.error # FIXME: Add oauth error page return render_template('oauth/prompt.html', client=grant.client, scope=grant.request.scope, - cancel_url=build_url(redirect_uri, dict(error='access_denied')), + cancel_url=build_url(redirect_uri, {"error": "access_denied"}), hide_navbar_links=True, hide_footer=True) - if request.method == 'POST': # User grants access to the client + else: + # TODO: Validate that user grants access to the client return authorization_server.create_authorization_response(grant_user=current_user) @@ -73,7 +76,7 @@ def oauth_token_handler(): @new_oauth_bp.route('/revoke', methods=['POST']) def revoke_token(): - return authorization_server.create_endpoint_response('revocation') + return authorization_server.create_endpoint_response("revocation") def split_by_crlf(s): From 94fdd52b9648a6565c4e5e627d8096d90b669560 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 20:04:10 +0530 Subject: [PATCH 10/65] more updates --- metabrainz/new_oauth/authorization_grant.py | 10 +++++----- metabrainz/new_oauth/models/token.py | 2 +- metabrainz/new_oauth/refresh_grant.py | 11 +++++------ metabrainz/new_oauth/views.py | 4 +--- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index f4b5fba2..539add54 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -44,8 +44,8 @@ def delete_authorization_code(self, authorization_code): db.session.commit() def authenticate_user(self, authorization_code): - # TODO: fix authenticate_user - # return db.session\ - # .query(OAuth2User)\ - # .filter_by(user_id=authorization_code.user_id) - pass + # TODO: Do we need to verify the client_id / client_secret associated with the code here? + return db.session\ + .query(OAuth2User)\ + .filter_by(user_id=authorization_code.user_id)\ + .first() diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index 3088f627..27ada4dc 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -5,7 +5,7 @@ from sqlalchemy import func, Column, Integer, DateTime, Text, ForeignKey, Boolean, Identity from sqlalchemy.orm import relationship -from metabrainz.new_oauth.models import Base, db +from metabrainz.new_oauth.models import Base from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.relation_scope import OAuth2TokenScope from metabrainz.new_oauth.models.user import OAuth2User diff --git a/metabrainz/new_oauth/refresh_grant.py b/metabrainz/new_oauth/refresh_grant.py index f82f7df9..13ce90be 100644 --- a/metabrainz/new_oauth/refresh_grant.py +++ b/metabrainz/new_oauth/refresh_grant.py @@ -16,12 +16,11 @@ def authenticate_refresh_token(self, refresh_token): return token def authenticate_user(self, credential): - # TODO: fix impl - # return db.session\ - # .query(OAuth2User)\ - # .filter_by(user_id=credential.user_id)\ - # .first() - pass + # TODO: Do we need to verify the client_id / client_secret / token associated with the code here? + return db.session\ + .query(OAuth2User)\ + .filter_by(user_id=credential.user_id)\ + .first() def revoke_old_credential(self, credential): credential.revoked = True diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index f372d10a..b06be6ad 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -25,14 +25,12 @@ def create_client(): owner_id=current_user.id, name=form.client_name, website=form["client_uri"], - redirect_uris=split_by_crlf(form["redirect_uri"]), - + redirect_uris=split_by_crlf(form["redirect_uri"]) ) # TODO: Fix use of these columns client_metadata = { "grant_types": split_by_crlf(form["grant_type"]), "response_types": split_by_crlf(form["response_type"]), - "scope": form["scope"], "token_endpoint_auth_method": form["token_endpoint_auth_method"] } From 0bd7f36cb678bc3016901b0ee8497670fbe7fe17 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 21:26:40 +0530 Subject: [PATCH 11/65] misc fixes --- metabrainz/new_oauth/forms.py | 4 +-- metabrainz/new_oauth/models/relation_scope.py | 35 +++++++++---------- metabrainz/new_oauth/models/scope.py | 2 +- metabrainz/new_oauth/models/user.py | 3 +- metabrainz/new_oauth/views.py | 30 ++++++++-------- metabrainz/templates/oauth/create_client.html | 8 ++--- 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/metabrainz/new_oauth/forms.py b/metabrainz/new_oauth/forms.py index 357e7440..95296dd9 100644 --- a/metabrainz/new_oauth/forms.py +++ b/metabrainz/new_oauth/forms.py @@ -18,10 +18,10 @@ class ApplicationForm(FlaskForm): validators.InputRequired(message=lazy_gettext("Homepage field is empty.")), validators.URL(require_tld=False, message=lazy_gettext("Homepage is not a valid URI.")) ]) - redirect_uri = FieldList(StringField(lazy_gettext('Authorization callback URL'), [ + redirect_uri = StringField(lazy_gettext('Authorization callback URL'), [ validators.InputRequired(message=lazy_gettext("Authorization callback URL field is empty.")), validators.URL(require_tld=False, message=lazy_gettext("Authorization callback URL is invalid.")) - ])) + ]) def validate_redirect_uri(self, field): if not field.data.startswith(("http://", "https://")): diff --git a/metabrainz/new_oauth/models/relation_scope.py b/metabrainz/new_oauth/models/relation_scope.py index 53a8f1fb..66606eb2 100644 --- a/metabrainz/new_oauth/models/relation_scope.py +++ b/metabrainz/new_oauth/models/relation_scope.py @@ -1,24 +1,23 @@ from sqlalchemy import Integer -from sqlalchemy.sql.schema import Identity, Column, ForeignKey +from sqlalchemy.sql.schema import Identity, Column, ForeignKey, Table from metabrainz.new_oauth.models import Base -class OAuth2TokenScope(Base): - __tablename__ = "l_token_scope" - __table_args__ = { - "schema": "oauth" - } - id = Column(Integer, Identity(), primary_key=True) - token_id = Column(Integer, ForeignKey("oauth.token.id", ondelete="CASCADE"), nullable=False) - scope_id = Column(Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False) +OAuth2TokenScope = Table( + "l_token_scope", + Base.metadata, + Column("id", Integer, Identity(), primary_key=True), + Column("token_id", Integer, ForeignKey("oauth.token.id", ondelete="CASCADE"), nullable=False), + Column("scope_id", Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False), + schema="oauth" +) - -class OAuth2CodeScope(Base): - __tablename__ = "l_code_scope" - __table_args__ = { - "schema": "oauth" - } - id = Column(Integer, Identity(), primary_key=True) - code_id = Column(Integer, ForeignKey("oauth.code.id", ondelete="CASCADE"), nullable=False) - scope_id = Column(Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False) +OAuth2CodeScope = Table( + "l_code_scope", + Base.metadata, + Column("id", Integer, Identity(), primary_key=True), + Column("code_id", Integer, ForeignKey("oauth.code.id", ondelete="CASCADE"), nullable=False), + Column("scope_id", Integer, ForeignKey("oauth.scope.id", ondelete="CASCADE"), nullable=False), + schema="oauth" +) diff --git a/metabrainz/new_oauth/models/scope.py b/metabrainz/new_oauth/models/scope.py index 2185aa8c..a2d0cbbf 100644 --- a/metabrainz/new_oauth/models/scope.py +++ b/metabrainz/new_oauth/models/scope.py @@ -17,7 +17,7 @@ class OAuth2Scope(Base): description = Column(Text, nullable=False) -def get_scopes(session: sqlalchemy.orm.Session, scope) -> list[OAuth2Scope]: +def get_scopes(session: sqlalchemy.orm.Session, scope): """ Given a comma separated scope string return associated scope objects from db """ scopes = scope_to_list(scope) return session.query(OAuth2Scope).filter(OAuth2Scope.name.in_(scopes)) diff --git a/metabrainz/new_oauth/models/user.py b/metabrainz/new_oauth/models/user.py index 799c4e2d..1714a4d5 100644 --- a/metabrainz/new_oauth/models/user.py +++ b/metabrainz/new_oauth/models/user.py @@ -1,9 +1,10 @@ +from flask_login import UserMixin from sqlalchemy import Column, Integer, Identity, Text, DateTime, func, Date, Boolean from metabrainz.new_oauth.models import Base -class OAuth2User(Base): +class OAuth2User(Base, UserMixin): __tablename__ = "user" __table_args__ = { "schema": "oauth" diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index b06be6ad..295a5dd3 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -1,5 +1,5 @@ from authlib.oauth2 import OAuth2Error -from flask import Blueprint, request, render_template, redirect +from flask import Blueprint, request, render_template, redirect, url_for from flask_login import login_required, current_user from metabrainz.decorators import nocache, crossdomain @@ -23,28 +23,30 @@ def create_client(): client = OAuth2Client( client_id=client_id, owner_id=current_user.id, - name=form.client_name, - website=form["client_uri"], - redirect_uris=split_by_crlf(form["redirect_uri"]) + name=form.client_name.data, + description=form.description.data, + website=form.website.data, + redirect_uris=[form.redirect_uri.data] ) # TODO: Fix use of these columns - client_metadata = { - "grant_types": split_by_crlf(form["grant_type"]), - "response_types": split_by_crlf(form["response_type"]), - "token_endpoint_auth_method": form["token_endpoint_auth_method"] - } + # client_metadata = { + # "grant_types": split_by_crlf(form["grant_type"]), + # "response_types": split_by_crlf(form["response_type"]), + # "token_endpoint_auth_method": form["token_endpoint_auth_method"] + # } - if form["token_endpoint_auth_method"] == "none": - client.client_secret = "" - else: - client.client_secret = gen_salt(48) + # if form["token_endpoint_auth_method"] == "none": + # client.client_secret = "" + # else: + # client.client_secret = gen_salt(48) + client.client_secret = gen_salt(48) db.session.add(client) db.session.commit() else: return render_template('oauth/create_client.html', form=form) - return redirect('/') + return redirect(url_for("index.home")) @new_oauth_bp.route('/authorize', methods=['GET', 'POST']) diff --git a/metabrainz/templates/oauth/create_client.html b/metabrainz/templates/oauth/create_client.html index 62e090c6..f99807d0 100644 --- a/metabrainz/templates/oauth/create_client.html +++ b/metabrainz/templates/oauth/create_client.html @@ -16,12 +16,12 @@

{{ _('Create new application') }}

{{ form.hidden_tag() }}
- -
{{ form.name(class="form-control", required="required") }}
+ +
{{ form.client_name(class="form-control", required="required") }}
- -
{{ form.desc(class="form-control", required="required") }}
+ +
{{ form.description(class="form-control", required="required") }}
From f1ee2782772a2015580db30aefb19f30d888efb2 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 6 Oct 2022 23:01:29 +0530 Subject: [PATCH 12/65] misc fixes - 2 --- metabrainz/new_oauth/authorization_grant.py | 13 +++---- metabrainz/new_oauth/models/client.py | 2 +- metabrainz/new_oauth/models/code.py | 4 +- metabrainz/new_oauth/models/scope.py | 2 +- metabrainz/new_oauth/models/token.py | 26 +++++++++++-- metabrainz/new_oauth/provider.py | 3 +- metabrainz/new_oauth/refresh_grant.py | 2 +- metabrainz/new_oauth/views.py | 43 +++++++++++++++++++-- 8 files changed, 73 insertions(+), 22 deletions(-) diff --git a/metabrainz/new_oauth/authorization_grant.py b/metabrainz/new_oauth/authorization_grant.py index 539add54..69b7ea04 100644 --- a/metabrainz/new_oauth/authorization_grant.py +++ b/metabrainz/new_oauth/authorization_grant.py @@ -11,8 +11,6 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = ['none', 'client_secret_post'] def save_authorization_code(self, code, request): - client = request.client - code_challenge = request.data.get('code_challenge') code_challenge_method = request.data.get('code_challenge_method') @@ -20,7 +18,7 @@ def save_authorization_code(self, code, request): auth_code = OAuth2AuthorizationCode( code=code, - client_id=client.client_id, + client_id=request.client.id, redirect_uri=request.redirect_uri, scopes=scopes, user_id=request.user.id, @@ -32,12 +30,11 @@ def save_authorization_code(self, code, request): return auth_code def query_authorization_code(self, code, client): - item = db.session\ + # TODO: Consider adding expiry for auth code + return db.session\ .query(OAuth2AuthorizationCode)\ - .filter_by(code=code, client_id=client.client_id)\ + .filter_by(code=code, client_id=client.id)\ .first() - if item and not item.is_expired(): - return item def delete_authorization_code(self, authorization_code): db.session.delete(authorization_code) @@ -47,5 +44,5 @@ def authenticate_user(self, authorization_code): # TODO: Do we need to verify the client_id / client_secret associated with the code here? return db.session\ .query(OAuth2User)\ - .filter_by(user_id=authorization_code.user_id)\ + .filter_by(id=authorization_code.user_id)\ .first() diff --git a/metabrainz/new_oauth/models/client.py b/metabrainz/new_oauth/models/client.py index 8b13486c..c56a6642 100644 --- a/metabrainz/new_oauth/models/client.py +++ b/metabrainz/new_oauth/models/client.py @@ -45,7 +45,7 @@ def has_client_secret(self): def check_client_secret(self, client_secret): return self.client_secret == client_secret - def check_token_endpoint_auth_method(self, method): + def check_endpoint_auth_method(self, method, endpoint): return True # TODO: Fix token endpoint auth def check_response_type(self, response_type): diff --git a/metabrainz/new_oauth/models/code.py b/metabrainz/new_oauth/models/code.py index 27fdfa30..bf832298 100644 --- a/metabrainz/new_oauth/models/code.py +++ b/metabrainz/new_oauth/models/code.py @@ -1,6 +1,6 @@ from authlib.oauth2.rfc6749 import AuthorizationCodeMixin from authlib.oauth2.rfc6749.util import scope_to_list -from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime +from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime, func from sqlalchemy.orm import relationship from sqlalchemy.sql.schema import Identity @@ -25,7 +25,7 @@ class OAuth2AuthorizationCode(Base, AuthorizationCodeMixin): response_type = Column(Text, nullable=False) code_challenge = Column(Text) code_challenge_method = Column(Text) - granted_at = Column(DateTime(timezone=True)) + granted_at = Column(DateTime(timezone=True), default=func.now(), nullable=False) user = relationship(OAuth2User) client = relationship(OAuth2Client) diff --git a/metabrainz/new_oauth/models/scope.py b/metabrainz/new_oauth/models/scope.py index a2d0cbbf..324440cd 100644 --- a/metabrainz/new_oauth/models/scope.py +++ b/metabrainz/new_oauth/models/scope.py @@ -20,4 +20,4 @@ class OAuth2Scope(Base): def get_scopes(session: sqlalchemy.orm.Session, scope): """ Given a comma separated scope string return associated scope objects from db """ scopes = scope_to_list(scope) - return session.query(OAuth2Scope).filter(OAuth2Scope.name.in_(scopes)) + return session.query(OAuth2Scope).filter(OAuth2Scope.name.in_(scopes)).all() diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index 27ada4dc..9ae16e98 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -1,11 +1,11 @@ -from datetime import timedelta +from datetime import timedelta, datetime, timezone from authlib.oauth2.rfc6749 import TokenMixin from authlib.oauth2.rfc6749.util import scope_to_list from sqlalchemy import func, Column, Integer, DateTime, Text, ForeignKey, Boolean, Identity from sqlalchemy.orm import relationship -from metabrainz.new_oauth.models import Base +from metabrainz.new_oauth.models import Base, db from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.relation_scope import OAuth2TokenScope from metabrainz.new_oauth.models.user import OAuth2User @@ -31,7 +31,7 @@ class OAuth2Token(Base, TokenMixin): scopes = relationship("OAuth2Scope", secondary=OAuth2TokenScope) def get_client_id(self): - return self.client_id + return self.client.client_id def get_scope(self): return scope_to_list([s.name for s in self.scopes]) @@ -43,4 +43,22 @@ def get_expires_at(self): return self.issued_at + timedelta(seconds=self.expires_in) def is_refresh_token_active(self): - return self.revoked + return not self.revoked + + def check_client(self, client): + return self.client_id == client.id + + def is_expired(self): + return datetime.now(tz=timezone.utc) >= self.get_expires_at() + + +def save_token(token_data, request): + # TODO: Handle refresh token + token = OAuth2Token( + client_id=request.client.id, + user_id=request.user.id, + access_token=token_data["access_token"], + expires_in=token_data["expires_in"] + ) + db.session.add(token) + db.session.commit() diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index 9555578c..a069e694 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -7,10 +7,9 @@ from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.client import OAuth2Client -from metabrainz.new_oauth.models.token import OAuth2Token +from metabrainz.new_oauth.models.token import OAuth2Token, save_token query_client = create_query_client_func(db.session, OAuth2Client) -save_token = create_save_token_func(db.session, OAuth2Token) revoke_token = create_revocation_endpoint(db.session, OAuth2Token) # TODO: We can also configure the expiry time and token generation function diff --git a/metabrainz/new_oauth/refresh_grant.py b/metabrainz/new_oauth/refresh_grant.py index 13ce90be..6ada4428 100644 --- a/metabrainz/new_oauth/refresh_grant.py +++ b/metabrainz/new_oauth/refresh_grant.py @@ -19,7 +19,7 @@ def authenticate_user(self, credential): # TODO: Do we need to verify the client_id / client_secret / token associated with the code here? return db.session\ .query(OAuth2User)\ - .filter_by(user_id=credential.user_id)\ + .filter_by(id=credential.user_id)\ .first() def revoke_old_credential(self, credential): diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index 295a5dd3..c9597c4c 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -1,11 +1,13 @@ from authlib.oauth2 import OAuth2Error -from flask import Blueprint, request, render_template, redirect, url_for +from flask import Blueprint, request, render_template, redirect, url_for, jsonify from flask_login import login_required, current_user from metabrainz.decorators import nocache, crossdomain from metabrainz.new_oauth.forms import ApplicationForm from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.scope import get_scopes +from metabrainz.new_oauth.models.token import OAuth2Token +from metabrainz.new_oauth.models.user import OAuth2User from metabrainz.new_oauth.provider import authorization_server from metabrainz.new_oauth.models import db from metabrainz.utils import build_url @@ -56,9 +58,12 @@ def authorize_prompt(): redirect_uri = request.args.get('redirect_uri') if request.method == 'GET': # Client requests access try: - grant = authorization_server.validate_consent_request(end_user=current_user) + grant = authorization_server.get_consent_grant(end_user=current_user) except OAuth2Error as error: - return error.error # FIXME: Add oauth error page + return jsonify({ + "error": error.error, + "description": error.description + }) # FIXME: Add oauth error page return render_template('oauth/prompt.html', client=grant.client, scope=grant.request.scope, cancel_url=build_url(redirect_uri, {"error": "access_denied"}), hide_navbar_links=True, hide_footer=True) @@ -79,5 +84,37 @@ def revoke_token(): return authorization_server.create_endpoint_response("revocation") +@new_oauth_bp.route('/userinfo', methods=['GET']) +def user_info(): + auth_header = request.headers.get("Authorization") + if not auth_header: + return jsonify({"error": "missing auth header"}), 401 + try: + token = auth_header.split(" ")[1] + except (ValueError, KeyError): + return jsonify({"error": "invalid auth header"}), 401 + + token = db.session\ + .query(OAuth2Token)\ + .filter_by(access_token=token)\ + .one_or_none() + + if token is None: + return jsonify({"error": "invalid access token"}), 403 + + if token.is_expired(): + return jsonify({"error": "expired access token"}), 403 + + user = db.session\ + .query(OAuth2User)\ + .filter_by(id=token.user_id)\ + .first() + + return { + "sub": user.name, + "metabrainz_user_id": user.id + } + + def split_by_crlf(s): return [v for v in s.splitlines() if v] From 7cdc3f19fec62e4cdba197804019ad804ed68fd4 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 7 Oct 2022 21:11:36 +0530 Subject: [PATCH 13/65] add introspection endpoint --- metabrainz/new_oauth/introspection.py | 38 +++++++++++++++++++++++++++ metabrainz/new_oauth/provider.py | 4 ++- metabrainz/new_oauth/views.py | 7 ++++- metabrainz/users/views.py | 5 +++- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 metabrainz/new_oauth/introspection.py diff --git a/metabrainz/new_oauth/introspection.py b/metabrainz/new_oauth/introspection.py new file mode 100644 index 00000000..af5ef449 --- /dev/null +++ b/metabrainz/new_oauth/introspection.py @@ -0,0 +1,38 @@ +from authlib.oauth2.rfc7662 import IntrospectionEndpoint + +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.token import OAuth2Token + + +class OAuth2IntrospectionEndpoint(IntrospectionEndpoint): + + def query_token(self, token, token_type_hint): + base_query = db.session.query(OAuth2Token) + if token_type_hint == 'access_token': + token = base_query.filter_by(access_token=token).first() + elif token_type_hint == 'refresh_token': + token = base_query.filter_by(refresh_token=token).first() + else: # without token_type_hint + token = base_query.filter_by(access_token=token).first() + if not token: + token = base_query.filter_by(refresh_token=token).first() + return token + + def introspect_token(self, token): + return { + 'active': True, + 'client_id': token.client.client_id, + 'token_type': token.token_type, + 'username': token.user.name, + 'metabrainz_user_id': token.user_id, + 'scope': token.get_scope(), + 'sub': token.user.name, + 'aud': token.client.client_id, + 'iss': 'https://metabrainz.com/', + 'exp': token.expires_at, + 'iat': token.issued_at, + } + + def check_permission(self, token, client, request): + # TODO: discuss restricting + return True diff --git a/metabrainz/new_oauth/provider.py b/metabrainz/new_oauth/provider.py index a069e694..7c7017df 100644 --- a/metabrainz/new_oauth/provider.py +++ b/metabrainz/new_oauth/provider.py @@ -1,10 +1,10 @@ from authlib.integrations.sqla_oauth2 import ( create_query_client_func, - create_save_token_func, create_revocation_endpoint ) from authlib.integrations.flask_oauth2 import AuthorizationServer +from metabrainz.new_oauth.introspection import OAuth2IntrospectionEndpoint from metabrainz.new_oauth.models import db from metabrainz.new_oauth.models.client import OAuth2Client from metabrainz.new_oauth.models.token import OAuth2Token, save_token @@ -16,3 +16,5 @@ # for the server. Its simple and will also be nice to prefix our tokens # with meb. Rationale: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ authorization_server = AuthorizationServer(query_client=query_client, save_token=save_token) +authorization_server.register_endpoint(revoke_token) +authorization_server.register_endpoint(OAuth2IntrospectionEndpoint) diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index c9597c4c..9b44f34f 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -5,7 +5,6 @@ from metabrainz.decorators import nocache, crossdomain from metabrainz.new_oauth.forms import ApplicationForm from metabrainz.new_oauth.models.client import OAuth2Client -from metabrainz.new_oauth.models.scope import get_scopes from metabrainz.new_oauth.models.token import OAuth2Token from metabrainz.new_oauth.models.user import OAuth2User from metabrainz.new_oauth.provider import authorization_server @@ -86,6 +85,7 @@ def revoke_token(): @new_oauth_bp.route('/userinfo', methods=['GET']) def user_info(): + # TODO: Discuss merging with introspection endpoint auth_header = request.headers.get("Authorization") if not auth_header: return jsonify({"error": "missing auth header"}), 401 @@ -116,5 +116,10 @@ def user_info(): } +@new_oauth_bp.route('/oauth/introspect', methods=['POST']) +def introspect_token(): + return authorization_server.create_endpoint_response("introspection") + + def split_by_crlf(s): return [v for v in s.splitlines() if v] diff --git a/metabrainz/users/views.py b/metabrainz/users/views.py index 5ad78324..2935a797 100644 --- a/metabrainz/users/views.py +++ b/metabrainz/users/views.py @@ -7,6 +7,8 @@ from metabrainz.model.tier import Tier from metabrainz.model.user import User, InactiveUserException from metabrainz.model.token import TokenGenerationLimitException +from metabrainz.new_oauth.models import db +from metabrainz.new_oauth.models.user import OAuth2User from metabrainz.users import musicbrainz_login, login_forbidden from metabrainz.users.forms import CommercialSignUpForm, NonCommercialSignUpForm, UserEditForm, CommercialUserEditForm, \ NonCommercialUserEditForm @@ -308,7 +310,8 @@ def regenerate_token(): @users_bp.route('/login') @login_forbidden def login(): - return render_template('users/mb-login.html') + login_user(db.session.query(OAuth2User).filter_by(name='test1').first()) + return redirect(url_for('index.home')) @users_bp.route('/logout') From 27146a94732348f11315b5770681f731348fe22e Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 7 Oct 2022 21:14:13 +0530 Subject: [PATCH 14/65] fix return --- metabrainz/new_oauth/introspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metabrainz/new_oauth/introspection.py b/metabrainz/new_oauth/introspection.py index af5ef449..6792b428 100644 --- a/metabrainz/new_oauth/introspection.py +++ b/metabrainz/new_oauth/introspection.py @@ -16,7 +16,7 @@ def query_token(self, token, token_type_hint): token = base_query.filter_by(access_token=token).first() if not token: token = base_query.filter_by(refresh_token=token).first() - return token + return token def introspect_token(self, token): return { From 5115adf698d7ec454098477c0ce0f48de9b4dcec Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 7 Oct 2022 22:16:08 +0530 Subject: [PATCH 15/65] fix bugs --- metabrainz/new_oauth/introspection.py | 16 ++++++++-------- metabrainz/new_oauth/models/token.py | 3 +++ metabrainz/new_oauth/views.py | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/metabrainz/new_oauth/introspection.py b/metabrainz/new_oauth/introspection.py index 6792b428..c9f02e7a 100644 --- a/metabrainz/new_oauth/introspection.py +++ b/metabrainz/new_oauth/introspection.py @@ -6,31 +6,31 @@ class OAuth2IntrospectionEndpoint(IntrospectionEndpoint): - def query_token(self, token, token_type_hint): + def query_token(self, token_str, token_type_hint): base_query = db.session.query(OAuth2Token) if token_type_hint == 'access_token': - token = base_query.filter_by(access_token=token).first() + token = base_query.filter_by(access_token=token_str).first() elif token_type_hint == 'refresh_token': - token = base_query.filter_by(refresh_token=token).first() + token = base_query.filter_by(refresh_token=token_str).first() else: # without token_type_hint - token = base_query.filter_by(access_token=token).first() + token = base_query.filter_by(access_token=token_str).first() if not token: - token = base_query.filter_by(refresh_token=token).first() + token = base_query.filter_by(refresh_token=token_str).first() return token def introspect_token(self, token): return { 'active': True, 'client_id': token.client.client_id, - 'token_type': token.token_type, + 'token_type': 'Bearer', 'username': token.user.name, 'metabrainz_user_id': token.user_id, 'scope': token.get_scope(), 'sub': token.user.name, 'aud': token.client.client_id, 'iss': 'https://metabrainz.com/', - 'exp': token.expires_at, - 'iat': token.issued_at, + 'exp': int(token.get_expires_at().timestamp()), + 'iat': int(token.issued_at.timestamp()), } def check_permission(self, token, client, request): diff --git a/metabrainz/new_oauth/models/token.py b/metabrainz/new_oauth/models/token.py index 9ae16e98..d0defa9c 100644 --- a/metabrainz/new_oauth/models/token.py +++ b/metabrainz/new_oauth/models/token.py @@ -51,6 +51,9 @@ def check_client(self, client): def is_expired(self): return datetime.now(tz=timezone.utc) >= self.get_expires_at() + def is_revoked(self): + return self.revoked + def save_token(token_data, request): # TODO: Handle refresh token diff --git a/metabrainz/new_oauth/views.py b/metabrainz/new_oauth/views.py index 9b44f34f..b82d949b 100644 --- a/metabrainz/new_oauth/views.py +++ b/metabrainz/new_oauth/views.py @@ -116,7 +116,7 @@ def user_info(): } -@new_oauth_bp.route('/oauth/introspect', methods=['POST']) +@new_oauth_bp.route('/introspect', methods=['POST']) def introspect_token(): return authorization_server.create_endpoint_response("introspection") From 3ba4519d0826f5d0208e86a903b28eb97b6fc11c Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Tue, 9 May 2023 17:59:40 +0530 Subject: [PATCH 16/65] Create dummy oauth."user" table --- admin/sql/oauth/create_tables.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/admin/sql/oauth/create_tables.sql b/admin/sql/oauth/create_tables.sql index e9a80a7b..78660ba1 100644 --- a/admin/sql/oauth/create_tables.sql +++ b/admin/sql/oauth/create_tables.sql @@ -39,3 +39,9 @@ CREATE TABLE oauth.code ( code_challenge_method TEXT, granted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL ); + + -- TODO: add relevant fields to user model +CREATE TABLE oauth."user" ( + id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL +); + From d44838a1877204ce882c8b3e94d0efe02de3b878 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Wed, 10 May 2023 20:40:05 +0530 Subject: [PATCH 17/65] temp fix for create_tables oauth --- admin/sql/oauth/create_tables.sql | 121 +++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 36 deletions(-) diff --git a/admin/sql/oauth/create_tables.sql b/admin/sql/oauth/create_tables.sql index 78660ba1..1f16e947 100644 --- a/admin/sql/oauth/create_tables.sql +++ b/admin/sql/oauth/create_tables.sql @@ -1,47 +1,96 @@ -CREATE TABLE oauth.client ( - id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, - client_id TEXT NOT NULL, -- PK - client_secret TEXT, -- public clients won't have client_secret so NULLABLE - owner_id INTEGER, -- (maybe FK?), user - client_name TEXT NOT NULL, - description TEXT NOT NULL, - website TEXT NOT NULL, - redirect_uris TEXT[] NOT NULL +CREATE TABLE oauth."user" ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + name TEXT NOT NULL, + email TEXT, + unconfirmed_email TEXT, + website TEXT, + member_since TIMESTAMP WITH TIME ZONE, + email_confirm_date TIMESTAMP WITH TIME ZONE, + last_login_date TIMESTAMP WITH TIME ZONE, + last_updated TIMESTAMP WITH TIME ZONE, + birth_date DATE, + gender INTEGER, + password TEXT NOT NULL, + ha1 TEXT NOT NULL, + deleted BOOLEAN, + PRIMARY KEY (id) ); -CREATE TABLE oauth.scopes ( - id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK - name TEXT NOT NULL, - description TEXT NOT NULL + + +CREATE TABLE oauth.scope ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + name TEXT NOT NULL, + description TEXT NOT NULL, + PRIMARY KEY (id) ); -CREATE TABLE oauth.token ( - id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK - user_id INTEGER NOT NULL, -- FK, user - client_id INTEGER NOT NULL, -- FK, client - access_token TEXT NOT NULL, - refresh_token TEXT, - scopes INTEGER[] NOT NULL, - issued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), - expires_in INTEGER NOT NULL, - revoked BOOLEAN NOT NULL DEFAULT FALSE + + +CREATE TABLE oauth.client ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + client_id TEXT NOT NULL, + client_secret TEXT, + owner_id INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + website TEXT, + redirect_uris TEXT[] NOT NULL, + client_id_issued_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(owner_id) REFERENCES oauth."user" (id) ON DELETE CASCADE ); + CREATE TABLE oauth.code ( - id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL, -- PK - user_id INTEGER NOT NULL, -- FK, user - client_id INTEGER NOT NULL, -- FK, client - code TEXT NOT NULL UNIQUE, - redirect_uri TEXT NOT NULL, - response_type TEXT NOT NULL, - scopes INTEGER[] NOT NULL, - code_challenge TEXT, - code_challenge_method TEXT, - granted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + user_id INTEGER NOT NULL, + client_id INTEGER NOT NULL, + code TEXT NOT NULL, + redirect_uri TEXT NOT NULL, + response_type TEXT NOT NULL, + code_challenge TEXT, + code_challenge_method TEXT, + granted_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(user_id) REFERENCES oauth."user" (id) ON DELETE CASCADE, + FOREIGN KEY(client_id) REFERENCES oauth.client (id) ON DELETE CASCADE, + UNIQUE (code) ); - -- TODO: add relevant fields to user model -CREATE TABLE oauth."user" ( - id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL + +CREATE TABLE oauth.token ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + user_id INTEGER NOT NULL, + client_id INTEGER NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + issued_at TIMESTAMP WITH TIME ZONE, + expires_in INTEGER, + revoked BOOLEAN, + PRIMARY KEY (id), + FOREIGN KEY(user_id) REFERENCES oauth."user" (id) ON DELETE CASCADE, + FOREIGN KEY(client_id) REFERENCES oauth.client (id) ON DELETE CASCADE, + UNIQUE (access_token) +); + + +CREATE INDEX ix_oauth_token_refresh_token ON oauth.token (refresh_token); + +CREATE TABLE oauth.l_token_scope ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + token_id INTEGER NOT NULL, + scope_id INTEGER NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(token_id) REFERENCES oauth.token (id) ON DELETE CASCADE, + FOREIGN KEY(scope_id) REFERENCES oauth.scope (id) ON DELETE CASCADE ); +CREATE TABLE oauth.l_code_scope ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + code_id INTEGER NOT NULL, + scope_id INTEGER NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(code_id) REFERENCES oauth.code (id) ON DELETE CASCADE, + FOREIGN KEY(scope_id) REFERENCES oauth.scope (id) ON DELETE CASCADE +); \ No newline at end of file From 86ace6a74a152cb949e92bcbf7edda9afb3ec5bc Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 11 May 2023 21:27:40 +0530 Subject: [PATCH 18/65] fix before_first_request --- metabrainz/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/metabrainz/__init__.py b/metabrainz/__init__.py index 94003152..99fd80c7 100644 --- a/metabrainz/__init__.py +++ b/metabrainz/__init__.py @@ -132,11 +132,7 @@ def create_app(debug=None, config_path = None): ]) from metabrainz.new_oauth.models import db as new_oauth_db - - @app.before_first_request - def create_tables(): - new_oauth_db.create_all() - + new_oauth_db.create_all() config_oauth(app) # Blueprints From f54acb74635762a05adecc285ec80b7457182766 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 11 May 2023 21:28:58 +0530 Subject: [PATCH 19/65] fix before_first_request -2 --- metabrainz/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/metabrainz/__init__.py b/metabrainz/__init__.py index 99fd80c7..57da184b 100644 --- a/metabrainz/__init__.py +++ b/metabrainz/__init__.py @@ -131,8 +131,6 @@ def create_app(debug=None, config_path = None): LOGO_UPLOAD_SET, ]) - from metabrainz.new_oauth.models import db as new_oauth_db - new_oauth_db.create_all() config_oauth(app) # Blueprints From df56e6747f645b7e2e63fa6fbc3dc789803461aa Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Oct 2022 18:48:03 +0200 Subject: [PATCH 20/65] Create new OAuth login page Initial commit with basic HTML + CSS --- metabrainz/static/css/login.less | 63 +++++++++++++++++ metabrainz/static/css/main.less | 1 + metabrainz/templates/users/mb-login.html | 90 +++++++++++++++++++++--- 3 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 metabrainz/static/css/login.less diff --git a/metabrainz/static/css/login.less b/metabrainz/static/css/login.less new file mode 100644 index 00000000..316f3a60 --- /dev/null +++ b/metabrainz/static/css/login.less @@ -0,0 +1,63 @@ +#login-page { + font-family: 'Sintony'; + font-style: normal; + font-weight: 400; + min-height: 500px; + background: linear-gradient(90deg, #3B9766 0%, #FFA500 100%); + display: flex; + align-items: center; + justify-content: center; + + .form-label{ + font-weight: normal; + } + + .icon-pills { + display: flex; + justify-content: space-evenly; + margin-bottom: 2rem; + } + .icon-pill { + background: #D9D9D9; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); + border-radius: 50%; + text-align: center; + width:50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + img { + width:65%; + } + } + + .login-page-container { + max-width: 400px; + } + .login-card-container { + background: #E7E7E7; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); + border-radius: 3px; + } + .login-card { + h1,h2,h3,h4,h5,h6{ + font-weight: bold; + } + background: #FFFFFF; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); + padding: 1rem; + border-radius: 3px; + } + .login-card-bottom { + display: flex; + align-items: center; + justify-content: space-between; + } + .create-account-card { + padding: 1rem; + font-size: 1.3rem; + line-height: 1.6rem; + color: #808080; + } +} diff --git a/metabrainz/static/css/main.less b/metabrainz/static/css/main.less index 61478331..cd343778 100644 --- a/metabrainz/static/css/main.less +++ b/metabrainz/static/css/main.less @@ -1,5 +1,6 @@ @import "theme/theme.less"; @import "carousel.less"; +@import "login.less"; @icon-font-path:"/static/fonts/"; diff --git a/metabrainz/templates/users/mb-login.html b/metabrainz/templates/users/mb-login.html index 3a976929..6f12a52a 100644 --- a/metabrainz/templates/users/mb-login.html +++ b/metabrainz/templates/users/mb-login.html @@ -1,15 +1,89 @@ {% extends 'base.html' %} -{% block title %}{{ _('Log in') }} - MetaBrainz Foundation{% endblock %} +{% block title %}{{ _('Sign in') }} - MetaBrainz Foundation{% endblock %} {% block content %} -

{{ _('Log in') }}

+
+ +
{% endblock %} From 70738ad0391caec42c9be95074025e048dc93045 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 11 May 2023 18:05:07 +0200 Subject: [PATCH 21/65] Use more generic css class names so that we can use the same styles for the signup (create account) page --- .../static/css/{login.less => auth-page.less} | 15 ++++++++------- metabrainz/static/css/main.less | 2 +- metabrainz/templates/users/mb-login.html | 18 +++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) rename metabrainz/static/css/{login.less => auth-page.less} (87%) diff --git a/metabrainz/static/css/login.less b/metabrainz/static/css/auth-page.less similarity index 87% rename from metabrainz/static/css/login.less rename to metabrainz/static/css/auth-page.less index 316f3a60..c5b7449c 100644 --- a/metabrainz/static/css/login.less +++ b/metabrainz/static/css/auth-page.less @@ -1,9 +1,10 @@ -#login-page { - font-family: 'Sintony'; +#auth-page { + font-style: normal; font-weight: 400; min-height: 500px; background: linear-gradient(90deg, #3B9766 0%, #FFA500 100%); + margin: 0 -1em; display: flex; align-items: center; justify-content: center; @@ -32,15 +33,15 @@ } } - .login-page-container { + .auth-page-container { max-width: 400px; } - .login-card-container { + .auth-card-container { background: #E7E7E7; box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); border-radius: 3px; } - .login-card { + .auth-card { h1,h2,h3,h4,h5,h6{ font-weight: bold; } @@ -49,12 +50,12 @@ padding: 1rem; border-radius: 3px; } - .login-card-bottom { + .auth-card-bottom { display: flex; align-items: center; justify-content: space-between; } - .create-account-card { + .auth-card-footer { padding: 1rem; font-size: 1.3rem; line-height: 1.6rem; diff --git a/metabrainz/static/css/main.less b/metabrainz/static/css/main.less index cd343778..ca3a787f 100644 --- a/metabrainz/static/css/main.less +++ b/metabrainz/static/css/main.less @@ -1,6 +1,6 @@ @import "theme/theme.less"; @import "carousel.less"; -@import "login.less"; +@import "auth-page.less"; @icon-font-path:"/static/fonts/"; diff --git a/metabrainz/templates/users/mb-login.html b/metabrainz/templates/users/mb-login.html index 6f12a52a..2e635b80 100644 --- a/metabrainz/templates/users/mb-login.html +++ b/metabrainz/templates/users/mb-login.html @@ -3,8 +3,8 @@ {% block title %}{{ _('Sign in') }} - MetaBrainz Foundation{% endblock %} {% block content %} -
-