diff --git a/consent_provider/app.py b/consent_provider/app.py index 65b29035eb2114ba8cff65695a3b09f6265903c6..0e17b7bb39634b711655f154ee978e84ec0bf856 100644 --- a/consent_provider/app.py +++ b/consent_provider/app.py @@ -1,91 +1,42 @@ -from flask import abort, Flask, redirect, render_template, request +from flask import abort, Flask, redirect, request from flask.views import View from os import urandom, environ from hydra_client import HydraAdmin -from flask_wtf import FlaskForm -from wtforms import SubmitField, HiddenField -from flask_wtf.csrf import CSRFProtect -from wtforms.validators import DataRequired - +from db import User, BackendConnectionError HYDRA_ADMIN_URL = environ['HYDRA_ADMIN_URL'] - -class ConsentForm(FlaskForm): - accept = SubmitField("accept") - challenge = HiddenField("challenge") - - -class ConsentView(View): - - methods = "GET", "POST" - - def render_form(self, form, **context): - return render_template("consent.html", form=form, **context) - - def dispatch_request(self): - hydra = HydraAdmin(HYDRA_ADMIN_URL) - form = ConsentForm() - - challenge = request.args.get("consent_challenge") or form.challenge.data - if not challenge: - abort(400) - - consent_request = hydra.consent_request(challenge) - - session = { - "access_token": {}, - "id_token": { - "sub": "248289761010", - "name": "Example User", - "given_name": "Example", - "family_name": "User", - "preferred_username": "example", - "email": "example@oas.example.com", - "picture": "", - }, - } - - if request.method == "GET": - return self.get(form, consent_request, session) - elif request.method == "POST": - return self.post(form, consent_request, session) - abort(405) - - def get(self, form, consent_request, session): - if consent_request.skip: - redirect_to = consent_request.accept( - grant_scope=consent_request.requested_scope, - grant_access_token_audience=consent_request.requested_access_token_audience, - session=session, - ) - return redirect(redirect_to) - else: - form.challenge.data = consent_request.challenge - return self.render_form( - form, user=consent_request.subject, client=consent_request.client - ) - - def post(self, form, consent_request, session): - if form.validate(): - if form.accept.data: - redirect_to = consent_request.accept( - grant_scope=consent_request.requested_scope, - grant_access_token_audience=consent_request.requested_access_token_audience, - session=session, - remember=False - ) - else: - redirect_to = consent_request.reject(error="user_decline") - return redirect(redirect_to) - else: - # TODO: show error message - pass - return self.render_form(form) - - app = Flask(__name__) -app.secret_key = urandom(16) -csrf = CSRFProtect(app) -app.add_url_rule("/consent", view_func=ConsentView.as_view("consent")) +@app.route('/', methods=['GET']) +def home(): + hydra = HydraAdmin(HYDRA_ADMIN_URL) + challenge = request.args.get("consent_challenge") + if not challenge: + abort(403) + consent_request = hydra.consent_request(challenge) + app_name = consent_request.client["client_name"] + username = consent_request.subject + try: + user = User(username) + except BackendConnectionError as error: + # TODO: replace with propper logging via logger + print("Retrieving user object from GraphQL server failed") + print(error) + return redirect(consent_request.reject( + "Permission denied", + error_description="Login request was denied due to an internal server error")) + access_granted = user.has_app_permission(app_name) + if access_granted: + session = user.get_oauth_session() + return redirect(consent_request.accept( + grant_scope=consent_request.requested_scope, + grant_access_token_audience=consent_request.requested_access_token_audience, + session=session, + )) + return redirect(consent_request.reject( + "Permission denied", + error_description="Login request was denied due to missing application permission")) + +if __name__ == '__main__': + app.run() diff --git a/consent_provider/db.py b/consent_provider/db.py new file mode 100644 index 0000000000000000000000000000000000000000..510833530e76e8b7f88aff875c219ae9fd09ff1d --- /dev/null +++ b/consent_provider/db.py @@ -0,0 +1,76 @@ +from os import environ +from hydra_client import HydraAdmin +from graphqlclient import GraphQLClient +import urllib +import json + +GRAPHQL_URL = environ['GRAPHQL_URL'] +GRAPHQL_CLIENT = GraphQLClient(GRAPHQL_URL) + + +class User(): + def __init__(self, username): + self.username = username + try: + self._load_remote_user_info() + except urllib.error.HTTPError as error: + raise BackendConnectionError( + error.code, + error.headers, + ("Error during retrieval of userdata - " + error.reason)) + + def _load_remote_user_info(self): + querystring = '''{{ + getUser(username: "{0}"){{ + email, + applications{{ + edges{{ + node{{ + name + }} + }} + }} + }}}}'''.format(self.username) + result = json.loads(GRAPHQL_CLIENT.execute(querystring)) + data = result["data"]["getUser"] + self.applications = list(map(lambda x: x["node"]["name"], + data["applications"]["edges"])) + self.email = data["email"] + + def has_app_permission(self, appname): + return appname in self.applications + + def get_oauth_session(self): + """Create openID Connect token + + Use the userdata stored in the user object to create an OpenID Connect token. + The token will be passed to Hydra, which will store it and serve it to all OpenID Connect + Clients, that successfully query the /userinfo endpoint. Every field in the "id_token" + dictionary can be accessed through standard scopes and claims. + See https://openid.net/specs/openid-connect-core-1_0.html#Claims + + Returns: + OpenID Connect token of type dict + """ + return { + "access_token": {}, + "id_token": { + "name": self.username, + "preferred_username": self.username, + "email" : self.email, + "picture": ""} + } + + +class BackendConnectionError(Exception): + """Raised when requests to the backend server fail + + Attributes: + code -- http response code + headers -- http response headers + reason -- reson for the error + """ + def __init__(self, code, headers, reason): + self.code = code + self.headers = headers + self.reason = reason diff --git a/consent_provider/requirements.txt b/consent_provider/requirements.txt index 128d32e76b068d0d305215a9955361a7a7fc9e31..00c803983822201a1630f938960184bf1fa0d5c7 100644 --- a/consent_provider/requirements.txt +++ b/consent_provider/requirements.txt @@ -1,3 +1,3 @@ -Flask==1.1.1 -flask-wtf==0.14.2 -hydra-client==0.4.0 +Flask +hydra-client +graphqlclient diff --git a/login_provider/Dockerfile b/login_provider/Dockerfile index c671dd715c1d64a7ee2bf29f35b1881cffdbec30..cf821f60f279d4a14c866c3aff193b2b2cbeb8ea 100644 --- a/login_provider/Dockerfile +++ b/login_provider/Dockerfile @@ -12,7 +12,7 @@ EXPOSE 5000 ENV FLASK_ENV production ENV FLASK_RUN_HOST 0.0.0.0 ENV FLASK_RUN_PORT 5000 -ENV HYDRA_ADMIN_URL http://localhost:444 +ENV HYDRA_ADMIN_URL http://localhost:4445 ENV GRAPHQL_URL http://localhost:5002/graphql CMD [ "flask", "run" ] diff --git a/login_provider/requirements.txt b/login_provider/requirements.txt index 72bf67b2748a4195946d5c65b55d2d79cc9c341e..082ed2567ec47d8a6dc7138ed1700975db7ccadc 100644 --- a/login_provider/requirements.txt +++ b/login_provider/requirements.txt @@ -1,6 +1,7 @@ -Flask==1.1.1 -flask-wtf==0.14.2 -hydra-client==0.4.0 -Flask-Security==3.0.0 -Flask-SQLAlchemy==2.4.0 -bcrypt==3.1.7 +Flask +flask-wtf +hydra-client +Flask-Security +Flask-SQLAlchemy +bcrypt +graphqlclient diff --git a/login_provider/test/behave/features/login.feature b/login_provider/test/behave/features/login.feature index f1279e92a6d2a34bbea0d4982038a3cf2f1b52b8..ca1ff06439cb4ebeb85646f99c8d9d0e03183b4a 100644 --- a/login_provider/test/behave/features/login.feature +++ b/login_provider/test/behave/features/login.feature @@ -25,6 +25,16 @@ Scenario: Logout and redirect to login screen Then I wait on element "input#username" for 1000ms to exist And I expect that element "input#password" does exist +Scenario: Login with an invalid user + Given the element "input#username" is visible + And the element "input#password" is visible + And the element "input#submit" is visible + When I add "invalid_username" to the inputfield "input#username" + And I enter the "password" in the inputfield "input#password" + And I click on the button "input#submit" + Then I wait on element "input#logout" for 1000ms to not exist + And I expect that element "input#password" does exist + Scenario: Login with an invalid password Given the element "input#username" is visible And the element "input#password" is visible