Skip to content

Commit

Permalink
GH-3: Allow the user to sign out
Browse files Browse the repository at this point in the history
  • Loading branch information
markhobson committed Oct 6, 2023
1 parent 8631d8d commit 914aea3
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 24 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@

The application can also be configured with the following environment variables:

| Name | Value |
|---------------------------------|---------------------------------------------------------------------------------------------|
| FLASK_ENV | Application environment name (`dev` / `test`) |
| FLASK_SECRET_KEY | Flask session [secret key](https://flask.palletsprojects.com/en/2.3.x/quickstart/#sessions) |
| FLASK_BASIC_AUTH_USERNAME | HTTP Basic Auth username |
| FLASK_BASIC_AUTH_PASSWORD | HTTP Basic Auth password |
| FLASK_GOVUK_CLIENT_ID | OIDC client id |
| FLASK_GOVUK_CLIENT_SECRET | OIDC client secret |
| FLASK_GOVUK_SERVER_METADATA_URL | OIDC discovery endpoint |
| FLASK_GOVUK_TOKEN_ENDPOINT | OIDC token endpoint |
| FLASK_GOVUK_PROFILE_URL | OIDC profile URL |
| Name | Value |
|----------------------------------|---------------------------------------------------------------------------------------------|
| FLASK_ENV | Application environment name (`dev` / `test`) |
| FLASK_SECRET_KEY | Flask session [secret key](https://flask.palletsprojects.com/en/2.3.x/quickstart/#sessions) |
| FLASK_BASIC_AUTH_USERNAME | HTTP Basic Auth username |
| FLASK_BASIC_AUTH_PASSWORD | HTTP Basic Auth password |
| FLASK_GOVUK_CLIENT_ID | OIDC client id |
| FLASK_GOVUK_CLIENT_SECRET | OIDC client secret |
| FLASK_GOVUK_SERVER_METADATA_URL | OIDC discovery endpoint |
| FLASK_GOVUK_TOKEN_ENDPOINT | OIDC token endpoint |
| FLASK_GOVUK_PROFILE_URL | OIDC profile URL |
| FLASK_GOVUK_END_SESSION_ENDPOINT | OIDC end session endpoint |

## Running locally

Expand Down
14 changes: 14 additions & 0 deletions schemes/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from functools import wraps
from typing import Callable, ParamSpec, TypeVar
from urllib.parse import urlencode, urlparse

from authlib.integrations.flask_client import OAuth
from flask import Blueprint, Response, current_app, redirect, session, url_for
Expand All @@ -17,6 +18,19 @@ def callback() -> BaseResponse:
return redirect(url_for("home.index"))


@bp.route("/logout")
def logout() -> BaseResponse:
id_token = session["id_token"]
del session["user"]
del session["id_token"]

end_session_endpoint = current_app.config["GOVUK_END_SESSION_ENDPOINT"]
post_logout_redirect_uri = url_for("start.index", _external=True)
logout_query = urlencode({"id_token_hint": id_token, "post_logout_redirect_uri": post_logout_redirect_uri})
logout_url = urlparse(end_session_endpoint)._replace(query=logout_query).geturl()
return redirect(logout_url)


T = TypeVar("T")
P = ParamSpec("P")

Expand Down
1 change: 1 addition & 0 deletions schemes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Config:
GOVUK_SERVER_METADATA_URL = "https://oidc.integration.account.gov.uk/.well-known/openid-configuration"
GOVUK_TOKEN_ENDPOINT = "https://oidc.integration.account.gov.uk/token"
GOVUK_PROFILE_URL = "https://home.integration.account.gov.uk/"
GOVUK_END_SESSION_ENDPOINT = "https://oidc.integration.account.gov.uk/logout"


class DevConfig(Config):
Expand Down
14 changes: 6 additions & 8 deletions schemes/templates/govuk_one_login_service_header/macro.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

{%- set oneLoginLink = params.oneLoginLink if params.oneLoginLink else "https://home.account.gov.uk/" -%}
{%- set homepageLink = params.homepageLink if params.homepageLink else "https://www.gov.uk/" -%}
{%- set signOutLink = params.signOutLink if params.signOutLink else "https://your-service-sign-out-url-goes-here.gov.uk/" -%}
{%- macro littlePersonIcon(modifier="default") -%}
{%- set class = "focus" if modifier == "focus" else "default" %}
{%- set backgroundColour = "black" if modifier == "focus" else "white" %}
Expand Down Expand Up @@ -84,14 +85,11 @@
{{ littlePersonIcon("focus") }}
</a>
</li>
{# --- Start change ------------------------------------------------------------------------------------------------ #}
{# <li class="one-login-header__nav__list-item">#}
{# <!-- REPLACE SIGN OUT URL PLACEHOLDER WITH SIGN OUT PAGE FOR YOUR SERVICE -->#}
{# <a class="one-login-header__nav__link" href="https://your-service-sign-out-url-goes-here.gov.uk">#}
{# Sign out#}
{# </a>#}
{# </li>#}
{# --- End change -------------------------------------------------------------------------------------------------- #}
<li class="one-login-header__nav__list-item">
<a class="one-login-header__nav__link" href={{ signOutLink }}>
Sign out
</a>
</li>
</ul>
</nav>
</div>
Expand Down
7 changes: 6 additions & 1 deletion schemes/templates/service_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
{% endblock %}

{% block header %}
{{ govukOneLoginServiceHeader({"serviceName": "Schemes", "homepageLink": url_for("start.index"), "oneLoginLink": oneLoginLink}) }}
{{ govukOneLoginServiceHeader({
"serviceName": "Schemes",
"homepageLink": url_for("start.index"),
"oneLoginLink": oneLoginLink,
"signOutLink": url_for("auth.logout")
}) }}
{% endblock %}

{% block bodyEnd %}
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def app_fixture(oidc_server_app: OidcServerFlask) -> Flask:
"GOVUK_CLIENT_SECRET": private_key.decode(),
"GOVUK_SERVER_METADATA_URL": oidc_server_app.url_for("openid_configuration", _external=True),
"GOVUK_TOKEN_ENDPOINT": oidc_server_app.url_for("token", _external=True),
"GOVUK_END_SESSION_ENDPOINT": oidc_server_app.url_for("logout", _external=True),
}
)

Expand Down
14 changes: 11 additions & 3 deletions tests/e2e/oidc_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from authlib.jose.rfc7517.key_set import KeySet
from authlib.jose.rfc7518.rsa_key import RSAKey
from authlib.oauth2.rfc6749.requests import OAuth2Request
from flask import Flask, Response, jsonify, url_for
from flask import Flask, Response, jsonify, redirect, request, url_for
from werkzeug import Response as BaseResponse

from tests.e2e.oidc_server.clients import ClientRepository, StubClient
from tests.e2e.oidc_server.grants import (
Expand Down Expand Up @@ -77,10 +78,10 @@ def _init_grant(self, grant: StubAuthorizationCodeGrant) -> None:
grant.authorization_codes = self._authorization_codes
grant.users = self._users

def _save_token(self, token: dict[str, str], request: OAuth2Request) -> None:
def _save_token(self, token: dict[str, str], oauth2_request: OAuth2Request) -> None:
self._tokens.add(
token["access_token"],
StubToken(client_id=request.client.client_id, user_id=request.user.id, scope=token["scope"]),
StubToken(client_id=oauth2_request.client.client_id, user_id=oauth2_request.user.id, scope=token["scope"]),
)


Expand Down Expand Up @@ -129,6 +130,13 @@ def userinfo() -> Response:
def jwks() -> Response:
return jsonify(KeySet([key]).as_dict())

@app.route("/logout")
def logout() -> BaseResponse:
post_logout_redirect_uri = request.args.get("post_logout_redirect_uri")
assert post_logout_redirect_uri is not None

return redirect(post_logout_redirect_uri)

# register after token endpoint has been defined
authorization_server.register_client_auth_method(
PrivateKeyJwtClientAssertion.CLIENT_AUTH_METHOD,
Expand Down
13 changes: 12 additions & 1 deletion tests/e2e/pages.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from flask import Flask
from playwright.sync_api import Page
from playwright.sync_api import Locator, Page


class StartPage:
Expand Down Expand Up @@ -34,6 +34,7 @@ class HomePage:
def __init__(self, app: Flask, page: Page):
self._app = app
self._page = page
self.header = ServiceHeaderComponent(app, page.get_by_role("banner"))

def open(self) -> HomePage:
self._page.goto(f"{_get_base_url(self._app)}/home")
Expand All @@ -47,6 +48,16 @@ def visible(self) -> bool:
return self._page.get_by_role("heading", name="Home").is_visible()


class ServiceHeaderComponent:
def __init__(self, app: Flask, component: Locator):
self._app = app
self._component = component

def sign_out(self) -> StartPage:
self._component.get_by_role("link", name="Sign out").click()
return StartPage(self._app, self._component.page)


class LoginPage:
def __init__(self, app: Flask, page: Page):
self._app = app
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ def test_home_when_authenticated(self, app: Flask, page: Page) -> None:

assert home_page.visible()

@pytest.mark.usefixtures("live_server", "oidc_server")
def test_header_sign_out(self, app: Flask, page: Page) -> None:
home_page = HomePage(app, page).open()

start_page = home_page.header.sign_out()

assert start_page.visible()


class TestUnauthenticated:
@pytest.mark.usefixtures("live_server", "oidc_server")
Expand Down
1 change: 1 addition & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def config_fixture() -> Mapping[str, Any]:
"GOVUK_SERVER_METADATA_URL": "test",
"GOVUK_TOKEN_ENDPOINT": "test",
"GOVUK_PROFILE_URL": "test",
"GOVUK_END_SESSION_ENDPOINT": "test",
}


Expand Down

0 comments on commit 914aea3

Please sign in to comment.