diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 2f23a14a..2ffca3b3 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -110,3 +110,5 @@ class DevicehubConfig(Config): if API_DLT: API_DLT = API_DLT.strip("/") + WALLET_INX_EBSI_PLUGIN_TOKEN = config('WALLET_INX_EBSI_PLUGIN_TOKEN', None) + WALLET_INX_EBSI_PLUGIN_URL = config('WALLET_INX_EBSI_PLUGIN_URL', None) diff --git a/ereuse_devicehub/modules/did/templates/anonymous.html b/ereuse_devicehub/modules/did/templates/anonymous.html index d0c03229..33e42fb8 100644 --- a/ereuse_devicehub/modules/did/templates/anonymous.html +++ b/ereuse_devicehub/modules/did/templates/anonymous.html @@ -227,7 +227,7 @@ {% if oidc %}
- User of other inventory + Use a wallet {% endif %} diff --git a/ereuse_devicehub/modules/did/views.py b/ereuse_devicehub/modules/did/views.py index 1999eb1b..63aea371 100644 --- a/ereuse_devicehub/modules/did/views.py +++ b/ereuse_devicehub/modules/did/views.py @@ -60,6 +60,8 @@ class DidView(View): tlmp = { "isOperator": "operator.html", "isVerifier": "verifier.html", + "operator": "operator.html", + "verifier": "verifier.html", } self.template_name = tlmp.get(rol, self.template_name) diff --git a/ereuse_devicehub/modules/oidc/migrations/versions/96092022dadb_code2roles.py b/ereuse_devicehub/modules/oidc/migrations/versions/96092022dadb_code2roles.py new file mode 100644 index 00000000..5f05b771 --- /dev/null +++ b/ereuse_devicehub/modules/oidc/migrations/versions/96092022dadb_code2roles.py @@ -0,0 +1,53 @@ +"""code2roles + +Revision ID: 96092022dadb +Revises: abba37ff5c80 +Create Date: 2023-12-12 18:45:45.324285 + +""" +import citext +import sqlalchemy as sa +from alembic import context, op +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '96092022dadb' +down_revision = 'abba37ff5c80' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + op.create_table( + 'code_roles', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column( + 'updated', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column( + 'created', + sa.TIMESTAMP(timezone=True), + server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, + ), + sa.Column('code', citext.CIText(), nullable=False), + sa.Column('roles', citext.CIText(), nullable=False), + schema=f'{get_inv()}', + ) + op.execute(f"CREATE SEQUENCE {get_inv()}.code_roles_seq;") + + +def downgrade(): + op.drop_table('code_roles', schema=f'{get_inv()}') + op.execute(f"DROP SEQUENCE {get_inv()}.code_roles_seq;") diff --git a/ereuse_devicehub/modules/oidc/models.py b/ereuse_devicehub/modules/oidc/models.py index 790e03a9..d5737c00 100644 --- a/ereuse_devicehub/modules/oidc/models.py +++ b/ereuse_devicehub/modules/oidc/models.py @@ -4,12 +4,17 @@ from authlib.integrations.sqla_oauth2 import ( OAuth2TokenMixin, ) from flask import g +from werkzeug.security import gen_salt from ereuse_devicehub.db import db from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +def gen_code(): + return gen_salt(24) + + class MemberFederated(Thing): __tablename__ = 'member_federated' @@ -74,3 +79,11 @@ class OAuth2Token(Thing, OAuth2TokenMixin): db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'), ) member = db.relationship('MemberFederated') + + +class Code2Roles(Thing): + __tablename__ = 'code_roles' + + id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.String(40), default=gen_code, nullable=False) + roles = db.Column(db.String(40), unique=False, nullable=False) diff --git a/ereuse_devicehub/modules/oidc/views.py b/ereuse_devicehub/modules/oidc/views.py index 36f23c4b..26778761 100644 --- a/ereuse_devicehub/modules/oidc/views.py +++ b/ereuse_devicehub/modules/oidc/views.py @@ -14,16 +14,22 @@ from flask import ( request, session, url_for, + current_app as app ) from flask_login import login_required from ereuse_devicehub import __version__, messages +from ereuse_devicehub.db import db from ereuse_devicehub.modules.oidc.forms import ( AuthorizeForm, CreateClientForm, ListInventoryForm, ) -from ereuse_devicehub.modules.oidc.models import MemberFederated, OAuth2Client +from ereuse_devicehub.modules.oidc.models import ( + MemberFederated, + OAuth2Client, + Code2Roles +) from ereuse_devicehub.modules.oidc.oauth2 import ( authorization, generate_user_info, @@ -124,20 +130,19 @@ class SelectInventoryView(GenericMixin): title = "Select an Inventory" def dispatch_request(self): - form = ListInventoryForm() - if form.validate_on_submit(): - return redirect(form.save(), code=302) + host = app.config.get('HOST', '').strip("/") + url = "https://ebsi-pcp-wallet-ui.vercel.app/oid4vp?" + url += f"client_id=https://{host}&" + url += "presentation_definition_uri=https%3A%2F%2Fiotaledger.github.io" + url += "%2Febsi-stardust-components%2Fpublic%2Fpresentation-definition-ex1.json&" + url += f"response_uri=https://{host}/allow_code_oidc4vp" + url += "&state=1700822573400&response_type=vp_token&response_mode=direct_post" + url += "&nonce=DybC3A%3D%3D" next = request.args.get('next', '#') - context = { - 'next': next, - 'form': form, - 'title': self.title, - 'user': g.user, - 'grant': '', - 'version': __version__, - } - return render_template(self.template_name, **context) + session['next_url'] = next + + return redirect(url, code=302) class AllowCodeView(GenericMixin): @@ -222,95 +227,91 @@ class AllowCodeOidc4vpView(GenericMixin): discovery = {} def dispatch_request(self): + vcredential = self.get_credential() + if not vcredential: + return jsonify({"error": "No there are credentials"}) + + roles = self.verify(vcredential) + if not roles: + return jsonify({"error": "No there are roles"}) + + uri = self.get_response_uri(roles) + + return jsonify({"redirect_uri": uri}) + + def get_credential(self): self.vp_token = request.values.get("vp_token") pv = self.vp_token.split(".") token = json.loads(base64.b64decode(pv[1]).decode()) + return token.get('vp', {}).get("verifiableCredential") + + def verify(self, vcredential): + WALLET_INX_EBSI_PLUGIN_TOKEN = app.config.get( + 'WALLET_INX_EBSI_PLUGIN_TOKEN' + ) + WALLET_INX_EBSI_PLUGIN_URL = app.config.get( + 'WALLET_INX_EBSI_PLUGIN_URL' + ) headers = { 'Content-Type': 'application/json', - 'Authorization': f'Bearer WALLET_INX_EBSI_PLUGIN_TOKEN' + 'Authorization': f'Bearer {WALLET_INX_EBSI_PLUGIN_TOKEN}' } - vcredential = token.get('vp', {}).get("verifiableCredential") - if not vcredential: - return - data = json.dumps({ "type": "VerificationRequest", "jwtCredential": vcredential }) - result = requests.post(WALLET_INX_EBSI_PLUGIN_URL, headers=headers, data=data) + result = requests.post( + WALLET_INX_EBSI_PLUGIN_URL, + headers=headers, + data=data + ) + if result.status_code != 200: return vps = json.loads(result.text) if not vps.get('verified'): return - roles = vps['credential']['credentialSubject'].get('role') - if not roles: - return - return jsonify({"result": "ok"}) - # if not self.code or not self.oidc: - # return self.redirect() + return vps['credential']['credentialSubject'].get('role') - # self.member = MemberFederated.query.filter( - # MemberFederated.dlt_id_provider == self.oidc, - # MemberFederated.client_id.isnot(None), - # MemberFederated.client_secret.isnot(None), - # ).first() + def get_response_uri(selfi, roles): + code = Code2Roles(roles=roles) + db.session.add(code) + db.session.commit() - # if not self.member: - # return self.redirect() + url = "https://{host}/allow_code_oidc4vp2?code={code}".format( + host=app.config.get('HOST'), + code=code.code + ) + return url - # self.get_token() - # if 'error' in self.token: - # messages.error(self.token.get('error', '')) - # return self.redirect() - # self.get_user_info() - # return self.redirect() +class AllowCodeOidc4vp2View(GenericMixin): + methods = ['GET', 'POST'] - def get_discovery(self): - if self.discovery: - return self.discovery + def dispatch_request(self): + self.code = request.args.get('code') + if not self.code: + return self.redirect() - try: - url_well_known = self.member.domain + '.well-known/openid-configuration' - self.discovery = requests.get(url_well_known).json() - except Exception: - self.discovery = {'code': 404} + self.get_user_info() - return self.discovery - - def get_token(self): - data = {'grant_type': 'authorization_code', 'code': self.code} - url = self.member.domain + '/oauth/token' - url = self.get_discovery().get('token_endpoint', url) - - auth = (self.member.client_id, self.member.client_secret) - msg = requests.post(url, data=data, auth=auth) - self.token = json.loads(msg.text) + return self.redirect() def redirect(self): url = session.get('next_url') or '/login' return redirect(url) def get_user_info(self): - if self.userinfo: - return self.userinfo - if 'access_token' not in self.token: + code = Code2Roles.query.filter(code=self.code).first() + + if not code: return - url = self.member.domain + '/oauth/userinfo' - url = self.get_discovery().get('userinfo_endpoint', url) - access_token = self.token['access_token'] - token_type = self.token.get('token_type', 'Bearer') - headers = {"Authorization": f"{token_type} {access_token}"} - - msg = requests.get(url, headers=headers) - self.userinfo = json.loads(msg.text) - rols = self.userinfo.get('rols', []) - session['rols'] = [(k, k) for k in rols] - return self.userinfo + session['rols'] = [(k.strip(), k.strip()) for k in code.roles.split(",")] + db.session.delete(code) + db.session.commit() ########## @@ -320,6 +321,7 @@ oidc.add_url_rule('/create_client', view_func=CreateClientView.as_view('create_c oidc.add_url_rule('/oauth/authorize', view_func=AuthorizeView.as_view('autorize_oidc')) oidc.add_url_rule('/allow_code', view_func=AllowCodeView.as_view('allow_code')) oidc.add_url_rule('/allow_code_oidc4vp', view_func=AllowCodeOidc4vpView.as_view('allow_code_oidc4vp')) +oidc.add_url_rule('/allow_code_oidc4vp2', view_func=AllowCodeOidc4vp2View.as_view('allow_code_oidc4vp2')) oidc.add_url_rule('/oauth/token', view_func=IssueTokenView.as_view('oauth_issue_token')) oidc.add_url_rule( '/oauth/userinfo', view_func=OauthProfileView.as_view('oauth_user_info')