Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for LSAAI v2 #389

Merged
merged 9 commits into from
May 2, 2024
3 changes: 3 additions & 0 deletions docker/lifemonitor.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ COPY --chown=lm:lm lifemonitor /lm/lifemonitor
COPY --chown=lm:lm migrations /lm/migrations
COPY --chown=lm:lm cli /lm/cli

# Ensure read access to source code to unprivileged users
RUN find /lm/lifemonitor/ -type d -exec chmod a+r {} \;

##################################################################
## Node Stage
##################################################################
Expand Down
11 changes: 9 additions & 2 deletions lifemonitor/auth/oauth2/client/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ def authorize(name):
user_info = remote.userinfo(token=token)
return _handle_authorize(remote, token, user_info)
except OAuthError as e:
logger.debug(e)
return e.description, 401
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise exceptions.OAuthAuthorizationException(title="Authorization Error",
detail=e.description)
except Exception as e:
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise exceptions.OAuthAuthorizationException(title="Authorization Error",
detail="Unable to authorize the user")

@blueprint.route('/login/<name>')
@next_route_aware
Expand Down
1 change: 1 addition & 0 deletions lifemonitor/auth/oauth2/client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def to_dict(self):

@staticmethod
def from_dict(data: dict):
assert data, "User data from the OAuth Provider cannot be empty"
profile = OAuthUserProfile()
for k, v, in data.items():
setattr(profile, k, v)
Expand Down
53 changes: 19 additions & 34 deletions lifemonitor/auth/oauth2/client/providers/lsaai.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,41 +21,26 @@
import logging

from flask import current_app

from lifemonitor.exceptions import OAuthAuthorizationException
from lifemonitor.auth.oauth2.client.models import OAuthIdentity

# Config a module level logger
logger = logging.getLogger(__name__)


def normalize_userinfo(client, data):
logger.debug("LSAAI Data: %r", data)
preferred_username = data.get('eduperson_principal_name')[0].replace('@lifescience-ri.eu', '') \
if 'eduperson_principal_namex' in data and len(data['eduperson_principal_name']) > 0 \
else data['name'].replace(' ', '')
params = {
'sub': str(data['sub']),
'name': data['name'],
'email': data.get('email'),
'preferred_username': preferred_username
# 'profile': data['html_url'],
# 'picture': data['avatar_url'],
# 'website': data.get('blog'),
}

# The email can be be None despite the scope being 'user:email'.
# That is because a user can choose to make his/her email private.
# If that is the case we get all the users emails regardless if private or note
# and use the one he/she has marked as `primary`
info = {}
try:
if params.get('email') is None:
resp = client.get('user/emails')
resp.raise_for_status()
data = resp.json()
params["email"] = next(email['email'] for email in data if email['primary'])
except Exception as e:
logger.warning("Unable to get user email. Reason: %r", str(e))
return params
info["sub"] = data['sub']
except KeyError:
raise OAuthAuthorizationException(title="Unable to get user data",
description="the LS ID is required")
for key in ['name', 'email', 'preferred_username']:
info[key] = data.get(key, None)
if info[key] is None:
logger.warning("User %r has no %r", info["sub"], key)

return info


class LsAAI:
Expand All @@ -65,15 +50,15 @@ class LsAAI:
'client_id': current_app.config.get('LSAAI_CLIENT_ID', None),
'client_secret': current_app.config.get('LSAAI_CLIENT_SECRET', None),
'client_name': client_name,
'uri': 'https://proxy.aai.lifescience-ri.eu',
'api_base_url': 'https://proxy.aai.lifescience-ri.eu',
'access_token_url': 'https://proxy.aai.lifescience-ri.eu/OIDC/token',
'authorize_url': 'https://proxy.aai.lifescience-ri.eu/saml2sp/OIDC/authorization',
'client_kwargs': {'scope': 'openid profile email orcid eduperson_principal_name'},
'userinfo_endpoint': 'https://proxy.aai.lifescience-ri.eu/OIDC/userinfo',
'uri': 'https://login.aai.lifescience-ri.eu',
'api_base_url': 'https://login.aai.lifescience-ri.eu',
'access_token_url': 'https://login.aai.lifescience-ri.eu/oidc/token',
'authorize_url': 'https://login.aai.lifescience-ri.eu/oidc/authorize',
'client_kwargs': {'scope': 'openid profile email'},
'userinfo_endpoint': 'https://login.aai.lifescience-ri.eu/oidc/userinfo',
'userinfo_compliance_fix': normalize_userinfo,
'user_profile_html': 'https://profile.aai.lifescience-ri.eu/profile',
'server_metadata_url': 'https://proxy.aai.lifescience-ri.eu/.well-known/openid-configuration'
'server_metadata_url': 'https://login.aai.lifescience-ri.eu/oidc/.well-known/openid-configuration'
}

def __repr__(self) -> str:
Expand Down
18 changes: 11 additions & 7 deletions lifemonitor/auth/templates/auth/identity_not_found.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
{% block body %}

<div class="login-box" style="height: auto;">

{{ macros.render_logo(class="login-logo", style="width: auto") }}

<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">
<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">

<div class="card-body login-card-body">
{% if not identity %}
Expand All @@ -26,7 +26,11 @@
</div>
{{ macros.render_provider_logo(identity.provider) }}
<h5 class="login-box-msg text-bold mt-4" style="font-weight: lighter; font-size: 1.6em;">
{% if identity.user_info.name or identity.user_info.preferred_username %}
Hi, {{ identity.user_info.name or identity.user_info.preferred_username }}!
{% else %}
Hi, there!
{% endif %}
</h5>

<div class="text-center">
Expand All @@ -35,26 +39,26 @@
If you already have an account, we strongly recommend
that you <span class="text-center text-bold">Sign In</span>
with your existing credentials and link your new identity
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>.
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>.
</p>
<p class="text-bold">- OR -</p>
<p class="p-1 pb-4">
Click on <b>Register</b> to create a new account
linked to your <b>{{ identity.provider.name }}</b> identity.
</p>
</div>
</div>
</div>
{% endif %}

<form method="POST" action="{{action}}" >

{{ form.hidden_tag() }}

<div class="text-center mb-3 row">
<div class="text-center mb-3 row">
<div class="col-6">
<a href="{{ url_for("auth.login") }}" class="btn btn-block btn-secondary">
Sign In
</a>
</a>
</div>
<div class="col-6">
<a href="{{ url_for("auth.register_identity") }}" class="btn btn-block btn-primary">
Expand All @@ -69,4 +73,4 @@
</div>
</div>

{% endblock body %}
{% endblock body %}
26 changes: 15 additions & 11 deletions lifemonitor/auth/templates/auth/register.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
{% block body %}

<div class="login-box" style="height: auto;">

{{ macros.render_logo(class="login-logo", style="width: auto") }}

<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">
<div class="card card-primary card-outline shadow-lg p-1 mb-5 bg-white rounded">

<div class="card-body login-card-body">
{% if not identity %}
<h5 class="login-box-msg text-bold m-0">Sign Up</h5>
{% else %}
<div class="text-center">
<div class="small text-muted m-2">
<div>
<div>
Sign Up for <span style="font-style: italic; font-family: Baskerville,Baskerville Old Face,Hoefler Text,Garamond,Times New Roman,serif;">Life</span><span class="small" style="font-size: 75%; margin: 0 -1px 0 1px;">-</span><span style="font-weight: bold; font-family: Gill Sans,Gill Sans MT,Calibri,sans-serif;">Monitor</span>
</div>
<div class="m-n1">
Expand All @@ -26,17 +26,21 @@
</div>
{{ macros.render_provider_logo(identity.provider) }}
<h5 class="login-box-msg text-bold mt-4" style="font-weight: lighter; font-size: 1.6em;">
{% if identity.user_info.name or identity.user_info.preferred_username %}
Hi, {{ identity.user_info.name or identity.user_info.preferred_username }}!
{% else %}
Hi, there!
{% endif %}
</h5>
</div>
</div>
<div class="mt-4 small text-muted" style="font-weight: lighter;">
Choose a username for your LifeMonitor account:
</div>
{% endif %}

<form method="POST" action="{{action}}" >
{% if identity %}
{{ form.identity(value=identity.user_info.sub) | safe }}
{{ form.identity(value=identity.user_info.sub) | safe }}
{% endif %}
{{ macros.render_custom_field(form.username, value=user.username if user else "") }}
{% if not identity %}
Expand All @@ -45,24 +49,24 @@
{% endif %}
{{ form.hidden_tag() }}

<div class="text-center mb-3 row">
<div class="text-center mb-3 row">
<div class="col-6">
<a href="{{ url_for("auth.login") }}" class="btn btn-block btn-secondary">
Back
</a>
</a>
</div>
<div class="col-6">
<button type="submit"
class="btn btn-block btn-primary">
Register
</button>
</button>
</div>
</div>

</form>

{% if not identity %}
<div class="social-auth-links text-center mb-3">
<div class="social-auth-links text-center mb-3">
<p class="text-bold">- OR -</p>
{% for p in providers %}
{% if p.client_name != 'lsaai' %}
Expand Down Expand Up @@ -92,12 +96,12 @@
Rather than creating a new account, we strongly recommend
that you <a href="{{ url_for("auth.login") }}" class="text-center">Sign In</a>
with your existing credentials and link your new identity
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>
<a href="{{ url_for("auth.profile") }}">from the account configuration page</a>
</div>
</p>
{% endif %}
</div>
</div>
</div>

{% endblock body %}
{% endblock body %}
14 changes: 14 additions & 0 deletions lifemonitor/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ def handle_400(e: Exception = None, description: str = None):
)


@blueprint.route("/401")
def handle_401(e: Exception = None, description: str = None):
return __handle_error__(
{
"title": getattr(e, 'title', None) or "Unauthorized",
"code": "401",
"description": description if description
else str(e) if e and logger.isEnabledFor(logging.DEBUG)
else "Bad request",
}
)


@blueprint.route("/404")
def handle_404(e: Exception = None):
resource = request.args.get("resource", None, type=str)
Expand Down Expand Up @@ -187,6 +200,7 @@ def register_api(app):
logger.debug("Registering errors blueprint")
app.register_blueprint(blueprint)
app.register_error_handler(400, handle_400)
app.register_error_handler(401, handle_401)
app.register_error_handler(404, handle_404)
app.register_error_handler(429, handle_429)
app.register_error_handler(500, handle_500)
Expand Down
8 changes: 8 additions & 0 deletions lifemonitor/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ def __init__(self, detail=None,
detail=detail, status=status, **kwargs)


class OAuthAuthorizationException(LifeMonitorException):

def __init__(self, detail=None, title="OAuth Authorization Exception",
type="about:blank", status=401, **kwargs):
super().__init__(title=title,
detail=detail, status=status, **kwargs)


def handle_exception(e: Exception):
"""Return JSON instead of HTML for HTTP errors."""
# start with the correct headers and status code from the error
Expand Down
2 changes: 1 addition & 1 deletion lifemonitor/templates/base.j2
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
<script>
initCookieConsentBanner('{{ domain }}')
</script>
{% endif %}
{% endif %}
</body>


Loading