flow for connect to wallet
This commit is contained in:
parent
fc7d7b4549
commit
278377090a
|
@ -110,3 +110,5 @@ class DevicehubConfig(Config):
|
||||||
|
|
||||||
if API_DLT:
|
if API_DLT:
|
||||||
API_DLT = API_DLT.strip("/")
|
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)
|
||||||
|
|
|
@ -227,7 +227,7 @@
|
||||||
{% if oidc %}
|
{% if oidc %}
|
||||||
<br />
|
<br />
|
||||||
<a class="btn btn-primary mt-3" type="button" href="{{ url_for('oidc.login_other_inventory') }}?next={{ path }}">
|
<a class="btn btn-primary mt-3" type="button" href="{{ url_for('oidc.login_other_inventory') }}?next={{ path }}">
|
||||||
User of other inventory
|
Use a wallet
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,6 +60,8 @@ class DidView(View):
|
||||||
tlmp = {
|
tlmp = {
|
||||||
"isOperator": "operator.html",
|
"isOperator": "operator.html",
|
||||||
"isVerifier": "verifier.html",
|
"isVerifier": "verifier.html",
|
||||||
|
"operator": "operator.html",
|
||||||
|
"verifier": "verifier.html",
|
||||||
}
|
}
|
||||||
self.template_name = tlmp.get(rol, self.template_name)
|
self.template_name = tlmp.get(rol, self.template_name)
|
||||||
|
|
||||||
|
|
|
@ -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;")
|
|
@ -4,12 +4,17 @@ from authlib.integrations.sqla_oauth2 import (
|
||||||
OAuth2TokenMixin,
|
OAuth2TokenMixin,
|
||||||
)
|
)
|
||||||
from flask import g
|
from flask import g
|
||||||
|
from werkzeug.security import gen_salt
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def gen_code():
|
||||||
|
return gen_salt(24)
|
||||||
|
|
||||||
|
|
||||||
class MemberFederated(Thing):
|
class MemberFederated(Thing):
|
||||||
__tablename__ = 'member_federated'
|
__tablename__ = 'member_federated'
|
||||||
|
|
||||||
|
@ -74,3 +79,11 @@ class OAuth2Token(Thing, OAuth2TokenMixin):
|
||||||
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
|
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
|
||||||
)
|
)
|
||||||
member = db.relationship('MemberFederated')
|
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)
|
||||||
|
|
|
@ -14,16 +14,22 @@ from flask import (
|
||||||
request,
|
request,
|
||||||
session,
|
session,
|
||||||
url_for,
|
url_for,
|
||||||
|
current_app as app
|
||||||
)
|
)
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from ereuse_devicehub import __version__, messages
|
from ereuse_devicehub import __version__, messages
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.modules.oidc.forms import (
|
from ereuse_devicehub.modules.oidc.forms import (
|
||||||
AuthorizeForm,
|
AuthorizeForm,
|
||||||
CreateClientForm,
|
CreateClientForm,
|
||||||
ListInventoryForm,
|
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 (
|
from ereuse_devicehub.modules.oidc.oauth2 import (
|
||||||
authorization,
|
authorization,
|
||||||
generate_user_info,
|
generate_user_info,
|
||||||
|
@ -124,20 +130,19 @@ class SelectInventoryView(GenericMixin):
|
||||||
title = "Select an Inventory"
|
title = "Select an Inventory"
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
form = ListInventoryForm()
|
host = app.config.get('HOST', '').strip("/")
|
||||||
if form.validate_on_submit():
|
url = "https://ebsi-pcp-wallet-ui.vercel.app/oid4vp?"
|
||||||
return redirect(form.save(), code=302)
|
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', '#')
|
next = request.args.get('next', '#')
|
||||||
context = {
|
session['next_url'] = next
|
||||||
'next': next,
|
|
||||||
'form': form,
|
return redirect(url, code=302)
|
||||||
'title': self.title,
|
|
||||||
'user': g.user,
|
|
||||||
'grant': '',
|
|
||||||
'version': __version__,
|
|
||||||
}
|
|
||||||
return render_template(self.template_name, **context)
|
|
||||||
|
|
||||||
|
|
||||||
class AllowCodeView(GenericMixin):
|
class AllowCodeView(GenericMixin):
|
||||||
|
@ -222,95 +227,91 @@ class AllowCodeOidc4vpView(GenericMixin):
|
||||||
discovery = {}
|
discovery = {}
|
||||||
|
|
||||||
def dispatch_request(self):
|
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")
|
self.vp_token = request.values.get("vp_token")
|
||||||
pv = self.vp_token.split(".")
|
pv = self.vp_token.split(".")
|
||||||
token = json.loads(base64.b64decode(pv[1]).decode())
|
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 = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'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({
|
data = json.dumps({
|
||||||
"type": "VerificationRequest",
|
"type": "VerificationRequest",
|
||||||
"jwtCredential": vcredential
|
"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:
|
if result.status_code != 200:
|
||||||
return
|
return
|
||||||
|
|
||||||
vps = json.loads(result.text)
|
vps = json.loads(result.text)
|
||||||
if not vps.get('verified'):
|
if not vps.get('verified'):
|
||||||
return
|
return
|
||||||
roles = vps['credential']['credentialSubject'].get('role')
|
|
||||||
if not roles:
|
|
||||||
return
|
|
||||||
|
|
||||||
return jsonify({"result": "ok"})
|
return vps['credential']['credentialSubject'].get('role')
|
||||||
# if not self.code or not self.oidc:
|
|
||||||
# return self.redirect()
|
|
||||||
|
|
||||||
# self.member = MemberFederated.query.filter(
|
def get_response_uri(selfi, roles):
|
||||||
# MemberFederated.dlt_id_provider == self.oidc,
|
code = Code2Roles(roles=roles)
|
||||||
# MemberFederated.client_id.isnot(None),
|
db.session.add(code)
|
||||||
# MemberFederated.client_secret.isnot(None),
|
db.session.commit()
|
||||||
# ).first()
|
|
||||||
|
|
||||||
# if not self.member:
|
url = "https://{host}/allow_code_oidc4vp2?code={code}".format(
|
||||||
# return self.redirect()
|
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()
|
class AllowCodeOidc4vp2View(GenericMixin):
|
||||||
# return self.redirect()
|
methods = ['GET', 'POST']
|
||||||
|
|
||||||
def get_discovery(self):
|
def dispatch_request(self):
|
||||||
if self.discovery:
|
self.code = request.args.get('code')
|
||||||
return self.discovery
|
if not self.code:
|
||||||
|
return self.redirect()
|
||||||
|
|
||||||
try:
|
self.get_user_info()
|
||||||
url_well_known = self.member.domain + '.well-known/openid-configuration'
|
|
||||||
self.discovery = requests.get(url_well_known).json()
|
|
||||||
except Exception:
|
|
||||||
self.discovery = {'code': 404}
|
|
||||||
|
|
||||||
return self.discovery
|
return self.redirect()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def redirect(self):
|
def redirect(self):
|
||||||
url = session.get('next_url') or '/login'
|
url = session.get('next_url') or '/login'
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
||||||
def get_user_info(self):
|
def get_user_info(self):
|
||||||
if self.userinfo:
|
code = Code2Roles.query.filter(code=self.code).first()
|
||||||
return self.userinfo
|
|
||||||
if 'access_token' not in self.token:
|
if not code:
|
||||||
return
|
return
|
||||||
|
|
||||||
url = self.member.domain + '/oauth/userinfo'
|
session['rols'] = [(k.strip(), k.strip()) for k in code.roles.split(",")]
|
||||||
url = self.get_discovery().get('userinfo_endpoint', url)
|
db.session.delete(code)
|
||||||
access_token = self.token['access_token']
|
db.session.commit()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
##########
|
##########
|
||||||
|
@ -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('/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', 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_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/token', view_func=IssueTokenView.as_view('oauth_issue_token'))
|
||||||
oidc.add_url_rule(
|
oidc.add_url_rule(
|
||||||
'/oauth/userinfo', view_func=OauthProfileView.as_view('oauth_user_info')
|
'/oauth/userinfo', view_func=OauthProfileView.as_view('oauth_user_info')
|
||||||
|
|
Reference in New Issue