about summary refs log tree commit diff
path: root/gn2/wqflask/oauth2
diff options
context:
space:
mode:
Diffstat (limited to 'gn2/wqflask/oauth2')
-rw-r--r--gn2/wqflask/oauth2/checks.py8
-rw-r--r--gn2/wqflask/oauth2/client.py157
-rw-r--r--gn2/wqflask/oauth2/data.py8
-rw-r--r--gn2/wqflask/oauth2/groups.py32
-rw-r--r--gn2/wqflask/oauth2/jwks.py86
-rw-r--r--gn2/wqflask/oauth2/request_utils.py2
-rw-r--r--gn2/wqflask/oauth2/resources.py240
-rw-r--r--gn2/wqflask/oauth2/roles.py76
-rw-r--r--gn2/wqflask/oauth2/session.py25
-rw-r--r--gn2/wqflask/oauth2/toplevel.py35
-rw-r--r--gn2/wqflask/oauth2/ui.py19
-rw-r--r--gn2/wqflask/oauth2/users.py28
12 files changed, 509 insertions, 207 deletions
diff --git a/gn2/wqflask/oauth2/checks.py b/gn2/wqflask/oauth2/checks.py
index 7f33348e..4a5a117f 100644
--- a/gn2/wqflask/oauth2/checks.py
+++ b/gn2/wqflask/oauth2/checks.py
@@ -2,12 +2,10 @@
 from functools import wraps
 from urllib.parse import urljoin
 
+from flask import flash, request, redirect
 from authlib.integrations.requests_client import OAuth2Session
-from flask import (
-    flash, request, redirect, session as flask_session)
 
 from . import session
-from .session import clear_session_info
 from .client import (
     oauth2_get,
     oauth2_client,
@@ -24,8 +22,6 @@ def require_oauth2(func):
 
         def __clear_session__(_no_token):
             session.clear_session_info()
-            flask_session.pop("oauth2_token", None)
-            flask_session.pop("user_details", None)
             flash("You need to be logged in.", "alert-warning")
             return redirect("/")
 
@@ -36,7 +32,7 @@ def require_oauth2(func):
             if not user_details.get("error", False):
                 return func(*args, **kwargs)
 
-            return clear_session_info(token)
+            return __clear_session__(token)
 
         return session.user_token().either(__clear_session__, __with_token__)
 
diff --git a/gn2/wqflask/oauth2/client.py b/gn2/wqflask/oauth2/client.py
index 75e438cc..a7d20f6b 100644
--- a/gn2/wqflask/oauth2/client.py
+++ b/gn2/wqflask/oauth2/client.py
@@ -1,12 +1,17 @@
 """Common oauth2 client utilities."""
 import json
+import time
+import random
 import requests
 from typing import Optional
 from urllib.parse import urljoin
+from datetime import datetime, timedelta
 
 from flask import current_app as app
 from pymonad.either import Left, Right, Either
-from authlib.jose import jwt
+from authlib.common.urls import url_decode
+from authlib.jose.errors import BadSignatureError
+from authlib.jose import KeySet, JsonWebKey, JsonWebToken
 from authlib.integrations.requests_client import OAuth2Session
 
 from gn2.wqflask.oauth2 import session
@@ -31,11 +36,76 @@ def oauth2_clientsecret():
 def user_logged_in():
     """Check whether the user has logged in."""
     suser = session.session_info()["user"]
-    if suser["logged_in"]:
-        if session.expired():
-            session.clear_session_info()
-            return False
-        return suser["token"].is_right()
+    return suser["logged_in"] and suser["token"].is_right()
+
+
+def __make_token_validator__(keys: KeySet):
+    """Make a token validator function."""
+    def __validator__(token: dict):
+        for key in keys.keys:
+            try:
+                # Fixes CVE-2016-10555. See
+                # https://docs.authlib.org/en/latest/jose/jwt.html
+                jwt = JsonWebToken(["RS256"])
+                jwt.decode(token["access_token"], key)
+                return Right(token)
+            except BadSignatureError:
+                pass
+
+        return Left("INVALID-TOKEN")
+
+    return __validator__
+
+
+def auth_server_jwks() -> Optional[KeySet]:
+    """Fetch the auth-server JSON Web Keys information."""
+    _jwks = session.session_info().get("auth_server_jwks")
+    if bool(_jwks):
+        return {
+            "last-updated": _jwks["last-updated"],
+            "jwks": KeySet([
+                JsonWebKey.import_key(key) for key in _jwks.get(
+                    "auth_server_jwks", {}).get(
+                        "jwks", {"keys": []})["keys"]])}
+
+
+def __validate_token__(keys):
+    """Validate that the token is really from the auth server."""
+    def __index__(_sess):
+        return _sess
+    return session.user_token().then(__make_token_validator__(keys)).then(
+        session.set_user_token).either(__index__, __index__)
+
+
+def __update_auth_server_jwks__():
+    """Updates the JWKs every 2 hours or so."""
+    jwks = auth_server_jwks()
+    if bool(jwks):
+        last_updated = jwks.get("last-updated")
+        now = datetime.now().timestamp()
+        if bool(last_updated) and (now - last_updated) < timedelta(hours=2).seconds:
+            return __validate_token__(jwks["jwks"])
+
+    jwksuri = urljoin(authserver_uri(), "auth/public-jwks")
+    jwks = KeySet([
+        JsonWebKey.import_key(key)
+        for key in requests.get(jwksuri).json()["jwks"]])
+    return __validate_token__(jwks)
+
+
+def is_token_expired(token):
+    """Check whether the token has expired."""
+    __update_auth_server_jwks__()
+    jwks = auth_server_jwks()
+    if bool(jwks):
+        for jwk in jwks["jwks"].keys:
+            try:
+                jwt = JsonWebToken(["RS256"]).decode(
+                    token["access_token"], key=jwk)
+                return datetime.now().timestamp() > jwt["exp"]
+            except BadSignatureError as _bse:
+                pass
+
     return False
 
 
@@ -43,20 +113,56 @@ def oauth2_client():
     def __update_token__(token, refresh_token=None, access_token=None):
         """Update the token when refreshed."""
         session.set_user_token(token)
+        return token
+
+    def __delay__():
+        """Do a tiny delay."""
+        time.sleep(random.choice(tuple(i/1000.0 for i in range(0,100))))
+
+    def __refresh_token__(token):
+        """Synchronise token refresh."""
+        if is_token_expired(token):
+            __delay__()
+            if session.is_token_refreshing():
+                while session.is_token_refreshing():
+                    __delay__()
+
+                _token = session.user_token().either(None, lambda _tok: _tok)
+                return _token
+
+            session.toggle_token_refreshing()
+            _client = __client__(token)
+            _client.get(urljoin(authserver_uri(), "auth/user/"))
+            session.toggle_token_refreshing()
+            return _client.token
+
+        return token
+
+    def __json_auth__(client, method, uri, headers, body):
+        return (
+            uri,
+            {**headers, "Content-Type": "application/json"},
+            json.dumps({
+                **dict(url_decode(body)),
+                "client_id": oauth2_clientid(),
+                "client_secret": oauth2_clientsecret()
+            }))
 
     def __client__(token) -> OAuth2Session:
-        _jwt = jwt.decode(token["access_token"],
-                          app.config["AUTH_SERVER_SSL_PUBLIC_KEY"])
         client = OAuth2Session(
             oauth2_clientid(),
             oauth2_clientsecret(),
             scope=SCOPE,
-            token_endpoint=urljoin(authserver_uri(), "/auth/token"),
+            token_endpoint=urljoin(authserver_uri(), "auth/token"),
             token_endpoint_auth_method="client_secret_post",
             token=token,
             update_token=__update_token__)
+        client.register_client_auth_method(
+            ("client_secret_post", __json_auth__))
         return client
-    return session.user_token().either(
+
+    __update_auth_server_jwks__()
+    return session.user_token().then(__refresh_token__).either(
         lambda _notok: __client__(None),
         lambda token: __client__(token))
 
@@ -70,12 +176,18 @@ def __no_token__(_err) -> Left:
     resp.status_code = 400
     return Left(resp)
 
-def oauth2_get(uri_path: str, data: dict = {},
-               jsonify_p: bool = False, **kwargs) -> Either:
+def oauth2_get(
+        uri_path: str,
+        data: dict = {},
+        jsonify_p: bool = False,
+        headers: dict = {"Content-Type": "application/json"},
+        **kwargs
+) -> Either:
     def __get__(token) -> Either:
         resp = oauth2_client().get(
             urljoin(authserver_uri(), uri_path),
             data=data,
+            headers=headers,
             **kwargs)
         if resp.status_code == 200:
             if jsonify_p:
@@ -87,11 +199,18 @@ def oauth2_get(uri_path: str, data: dict = {},
     return session.user_token().either(__no_token__, __get__)
 
 def oauth2_post(
-        uri_path: str, data: Optional[dict] = None, json: Optional[dict] = None,
-        **kwargs) -> Either:
+        uri_path: str,
+        data: Optional[dict] = None,
+        json: Optional[dict] = None,
+        headers: dict = {"Content-Type": "application/json"},
+        **kwargs
+) -> Either:
     def __post__(token) -> Either:
         resp = oauth2_client().post(
-            urljoin(authserver_uri(), uri_path), data=data, json=json,
+            urljoin(authserver_uri(), uri_path),
+            data=data,
+            json=json,
+            headers=headers,
             **kwargs)
         if resp.status_code == 200:
             return Right(resp.json())
@@ -100,10 +219,14 @@ def oauth2_post(
 
     return session.user_token().either(__no_token__, __post__)
 
-def no_token_get(uri_path: str, **kwargs) -> Either:
+def no_token_get(
+        uri_path: str,
+        headers: dict = {"Content-Type": "application/json"},
+        **kwargs
+) -> Either:
     uri = urljoin(authserver_uri(), uri_path)
     try:
-        resp = requests.get(uri, **kwargs)
+        resp = requests.get(uri, headers=headers, **kwargs)
         if resp.status_code == 200:
             return Right(resp.json())
         return Left(resp)
diff --git a/gn2/wqflask/oauth2/data.py b/gn2/wqflask/oauth2/data.py
index 29d68be0..16e5f60c 100644
--- a/gn2/wqflask/oauth2/data.py
+++ b/gn2/wqflask/oauth2/data.py
@@ -69,8 +69,10 @@ def __search_phenotypes__(query, template, **kwargs):
             template, traits=[], per_page=per_page, query=query,
             selected_traits=selected_traits, search_results=search_results,
             search_endpoint=urljoin(
-                authserver_uri(), "auth/data/search"),
-            gn_server_url = authserver_uri(),
+                request.host_url, "oauth2/data/phenotype/search"),
+            auth_server_url=authserver_uri(),
+            pheno_results_template=urljoin(
+                authserver_uri(), "auth/data/search/phenotype/<jobid>"),
             results_endpoint=urljoin(
                 authserver_uri(),
                 f"auth/data/search/phenotype/{job_id}"),
@@ -122,6 +124,7 @@ def json_search_mrna() -> Response:
 @data.route("/phenotype/search", methods=["POST"])
 def json_search_phenotypes() -> Response:
     """Search for phenotypes."""
+    from gn2.utility.tools import GN_SERVER_URL
     form = request.json
     def __handle_error__(err):
         error = process_error(err)
@@ -136,6 +139,7 @@ def json_search_phenotypes() -> Response:
             "per_page": int(form.get("per_page", 50)),
             "page": int(form.get("page", 1)),
             "auth_server_uri": authserver_uri(),
+            "gn3_server_uri": GN_SERVER_URL,
             "selected_traits": form.get("selected_traits", [])
         }).either(__handle_error__, jsonify)
 
diff --git a/gn2/wqflask/oauth2/groups.py b/gn2/wqflask/oauth2/groups.py
index 3bc4bcb2..b3f1f54d 100644
--- a/gn2/wqflask/oauth2/groups.py
+++ b/gn2/wqflask/oauth2/groups.py
@@ -44,7 +44,7 @@ def create_group():
     def __setup_group__(response):
         session["user_details"]["group"] = response
 
-    resp = oauth2_post("auth/group/create", data=dict(request.form))
+    resp = oauth2_post("auth/group/create", json=dict(request.form))
     return resp.either(
         handle_error("oauth2.group.join_or_create"),
         handle_success(
@@ -116,7 +116,7 @@ def accept_join_request():
         return redirect(url_for("oauth2.group.list_join_requests"))
     return oauth2_post(
         "auth/group/requests/join/accept",
-        data=request.form).either(
+        json=dict(request.form)).either(
             handle_error("oauth2.group.list_join_requests"),
             __success__)
 
@@ -132,34 +132,10 @@ def reject_join_request():
         return redirect(url_for("oauth2.group.list_join_requests"))
     return oauth2_post(
         "auth/group/requests/join/reject",
-        data=request.form).either(
+        json=dict(request.form)).either(
             handle_error("oauth2.group.list_join_requests"),
             __success__)
 
-@groups.route("/role/<uuid:group_role_id>", methods=["GET"])
-@require_oauth2
-def group_role(group_role_id: uuid.UUID):
-    """View the details of a particular role."""
-    def __render_error__(**kwargs):
-        return render_ui("oauth2/view-group-role.html", **kwargs)
-
-    def __gprivs_success__(role, group_privileges):
-        return render_ui(
-            "oauth2/view-group-role.html", group_role=role,
-            group_privileges=tuple(
-                priv for priv in group_privileges
-                if priv not in role["role"]["privileges"]))
-
-    def __role_success__(role):
-        return oauth2_get("auth/group/privileges").either(
-            lambda err: __render_error__(
-                group_role=group_role,
-                group_privileges_error=process_error(err)),
-            lambda privileges: __gprivs_success__(role, privileges))
-
-    return oauth2_get(f"auth/group/role/{group_role_id}").either(
-        lambda err: __render_error__(group_role_error=process_error(err)),
-        __role_success__)
 
 def add_delete_privilege_to_role(
         group_role_id: uuid.UUID, direction: str) -> Response:
@@ -187,7 +163,7 @@ def add_delete_privilege_to_role(
         }
         return oauth2_post(
             uris[direction],
-            data={
+            json={
                 "group_role_id": group_role_id,
                 "privilege_id": privilege_id
             }).either(__error__, __success__)
diff --git a/gn2/wqflask/oauth2/jwks.py b/gn2/wqflask/oauth2/jwks.py
new file mode 100644
index 00000000..efd04997
--- /dev/null
+++ b/gn2/wqflask/oauth2/jwks.py
@@ -0,0 +1,86 @@
+"""Utilities dealing with JSON Web Keys (JWK)"""
+import os
+from pathlib import Path
+from typing import Any, Union
+from datetime import datetime, timedelta
+
+from flask import Flask
+from authlib.jose import JsonWebKey
+from pymonad.either import Left, Right, Either
+
+def jwks_directory(app: Flask, configname: str) -> Path:
+    """Compute the directory where the JWKs are stored."""
+    appsecretsdir = Path(app.config[configname]).parent
+    if appsecretsdir.exists() and appsecretsdir.is_dir():
+        jwksdir = Path(appsecretsdir, "jwks/")
+        if not jwksdir.exists():
+            jwksdir.mkdir()
+        return jwksdir
+    raise ValueError(
+        "The `appsecretsdir` value should be a directory that actually exists.")
+
+
+def generate_and_save_private_key(
+        storagedir: Path,
+        kty: str = "RSA",
+        crv_or_size: Union[str, int] = 2048,
+        options: tuple[tuple[str, Any]] = (("iat", datetime.now().timestamp()),)
+) -> JsonWebKey:
+    """Generate a private key and save to `storagedir`."""
+    privatejwk = JsonWebKey.generate_key(
+        kty, crv_or_size, dict(options), is_private=True)
+    keyname = f"{privatejwk.thumbprint()}.private.pem"
+    with open(Path(storagedir, keyname), "wb") as pemfile:
+        pemfile.write(privatejwk.as_pem(is_private=True))
+
+    return privatejwk
+
+
+def pem_to_jwk(filepath: Path) -> JsonWebKey:
+    """Parse a PEM file into a JWK object."""
+    with open(filepath, "rb") as pemfile:
+        return JsonWebKey.import_key(pemfile.read())
+
+
+def __sorted_jwks_paths__(storagedir: Path) -> tuple[tuple[float, Path], ...]:
+    """A sorted list of the JWK file paths with their creation timestamps."""
+    return tuple(sorted(((os.stat(keypath).st_ctime, keypath)
+                         for keypath in (Path(storagedir, keyfile)
+                                         for keyfile in os.listdir(storagedir)
+                                         if keyfile.endswith(".pem"))),
+                        key=lambda tpl: tpl[0]))
+
+
+def list_jwks(storagedir: Path) -> tuple[JsonWebKey, ...]:
+    """
+    List all the JWKs in a particular directory in the order they were created.
+    """
+    return tuple(pem_to_jwk(keypath) for ctime,keypath in
+                 __sorted_jwks_paths__(storagedir))
+
+
+def newest_jwk(storagedir: Path) -> Either:
+    """
+    Return an Either monad with the newest JWK or a message if none exists.
+    """
+    existingkeys = __sorted_jwks_paths__(storagedir)
+    if len(existingkeys) > 0:
+        return Right(pem_to_jwk(existingkeys[-1][1]))
+    return Left("No JWKs exist")
+
+
+def newest_jwk_with_rotation(jwksdir: Path, keyage: int) -> JsonWebKey:
+    """
+    Retrieve the latests JWK, creating a new one if older than `keyage` days.
+    """
+    def newer_than_days(jwkey):
+        filestat = os.stat(Path(
+            jwksdir, f"{jwkey.as_dict()['kid']}.private.pem"))
+        oldesttimeallowed = (datetime.now() - timedelta(days=keyage))
+        if filestat.st_ctime < (oldesttimeallowed.timestamp()):
+            return Left("JWK is too old!")
+        return jwkey
+
+    return newest_jwk(jwksdir).then(newer_than_days).either(
+        lambda _errmsg: generate_and_save_private_key(jwksdir),
+        lambda key: key)
diff --git a/gn2/wqflask/oauth2/request_utils.py b/gn2/wqflask/oauth2/request_utils.py
index 1cdc465f..456aba2b 100644
--- a/gn2/wqflask/oauth2/request_utils.py
+++ b/gn2/wqflask/oauth2/request_utils.py
@@ -36,7 +36,7 @@ def process_error(error: Response,
         try:
             err = error.json()
             msg = err.get(
-                "error", err.get("error_description", f"{error.reason}"))
+                "error_message", err.get("error_description", f"{error.reason}"))
         except simplejson.errors.JSONDecodeError as _jde:
             msg = message
         return {
diff --git a/gn2/wqflask/oauth2/resources.py b/gn2/wqflask/oauth2/resources.py
index 32efbd2a..7ea7fe38 100644
--- a/gn2/wqflask/oauth2/resources.py
+++ b/gn2/wqflask/oauth2/resources.py
@@ -1,17 +1,25 @@
-import uuid
+from uuid import UUID
 
 from flask import (
     flash, request, url_for, redirect, Response, Blueprint)
 
 from . import client
-from .ui import render_ui
+from . import session
+from .ui import render_ui as _render_ui
 from .checks import require_oauth2
 from .client import oauth2_get, oauth2_post
-from .request_utils import (
-    flash_error, flash_success, request_error, process_error)
+from .request_utils import (flash_error,
+                            flash_success,
+                            request_error,
+                            process_error,
+                            with_flash_error,
+                            with_flash_success)
 
 resources = Blueprint("resource", __name__)
 
+def render_ui(template, **kwargs):
+    return _render_ui(template, uipages="resources", **kwargs)
+
 @resources.route("/", methods=["GET"])
 @require_oauth2
 def user_resources():
@@ -51,7 +59,7 @@ def create_resource():
         flash("Resource created successfully", "alert-success")
         return redirect(url_for("oauth2.resource.user_resources"))
     return oauth2_post(
-        "auth/resource/create", data=request.form).either(
+        "auth/resource/create", json=dict(request.form)).either(
             __perr__, __psuc__)
 
 def __compute_page__(submit, current_page):
@@ -59,47 +67,48 @@ def __compute_page__(submit, current_page):
         return current_page + 1
     return (current_page - 1) or 1
 
-@resources.route("/view/<uuid:resource_id>", methods=["GET"])
+@resources.route("/<uuid:resource_id>/view", methods=["GET"])
 @require_oauth2
-def view_resource(resource_id: uuid.UUID):
+def view_resource(resource_id: UUID):
     """View the given resource."""
     page = __compute_page__(request.args.get("submit"),
                             int(request.args.get("page", "1"), base=10))
     count_per_page = int(request.args.get("count_per_page", "100"), base=10)
     def __users_success__(
-            resource, unlinked_data, users_n_roles, this_user, group_roles,
+            resource, unlinked_data, users_n_roles, this_user, resource_roles,
             users):
         return render_ui(
             "oauth2/view-resource.html", resource=resource,
             unlinked_data=unlinked_data, users_n_roles=users_n_roles,
-            this_user=this_user, group_roles=group_roles, users=users,
+            this_user=this_user, resource_roles=resource_roles, users=users,
             page=page, count_per_page=count_per_page)
 
-    def __group_roles_success__(
-            resource, unlinked_data, users_n_roles, this_user, group_roles):
+    def __resource_roles_success__(
+            resource, unlinked_data, users_n_roles, this_user, resource_roles):
         return oauth2_get("auth/user/list").either(
             lambda err: render_ui(
                 "oauth2/view-resource.html", resource=resource,
                 unlinked_data=unlinked_data, users_n_roles=users_n_roles,
-                this_user=this_user, group_roles=group_roles,
+                this_user=this_user, resource_roles=resource_roles,
                 users_error=process_error(err), count_per_page=count_per_page),
             lambda users: __users_success__(
-                resource, unlinked_data, users_n_roles, this_user, group_roles,
+                resource, unlinked_data, users_n_roles, this_user, resource_roles,
                 users))
 
     def __this_user_success__(resource, unlinked_data, users_n_roles, this_user):
-        return oauth2_get("auth/group/roles").either(
+        return oauth2_get(f"auth/resource/{resource_id}/roles").either(
             lambda err: render_ui(
-                "oauth2/view-resources.html", resource=resource,
+                "oauth2/view-resource.html", resource=resource,
                 unlinked_data=unlinked_data, users_n_roles=users_n_roles,
-                this_user=this_user, group_roles_error=process_error(err)),
-            lambda groles: __group_roles_success__(
-                resource, unlinked_data, users_n_roles, this_user, groles))
+                this_user=this_user, resource_roles_error=process_error(err),
+                count_per_page=count_per_page),
+            lambda rroles: __resource_roles_success__(
+                resource, unlinked_data, users_n_roles, this_user, rroles))
 
     def __users_n_roles_success__(resource, unlinked_data, users_n_roles):
         return oauth2_get("auth/user/").either(
             lambda err: render_ui(
-                "oauth2/view-resources.html",
+                "oauth2/view-resource.html",
                 this_user_error=process_error(err)),
             lambda usr_dets: __this_user_success__(
                 resource, unlinked_data, users_n_roles, usr_dets))
@@ -120,8 +129,10 @@ def view_resource(resource_id: uuid.UUID):
         dataset_type = resource["resource_category"]["resource_category_key"]
         return oauth2_get(f"auth/group/{dataset_type}/unlinked-data").either(
             lambda err: render_ui(
-                "oauth2/view-resource.html", resource=resource,
-                unlinked_error=process_error(err)),
+                "oauth2/view-resource.html",
+                resource=resource,
+                unlinked_error=process_error(err),
+                count_per_page=count_per_page),
             lambda unlinked: __unlinked_success__(resource, unlinked))
 
     def __fetch_resource_data__(resource):
@@ -164,7 +175,7 @@ def link_data_to_resource():
             flash(f"Data linked to resource successfully", "alert-success")
             return redirect(url_for(
                 "oauth2.resource.view_resource", resource_id=resource_id))
-        return oauth2_post("auth/resource/data/link", data=dict(form)).either(
+        return oauth2_post("auth/resource/data/link", json=dict(form)).either(
             __error__,
             __success__)
     except AssertionError as aserr:
@@ -193,7 +204,7 @@ def unlink_data_from_resource():
             return redirect(url_for(
                 "oauth2.resource.view_resource", resource_id=resource_id))
         return oauth2_post(
-            "auth/resource/data/unlink", data=dict(form)).either(
+            "auth/resource/data/unlink", json=dict(form)).either(
             __error__, __success__)
     except AssertionError as aserr:
         flash(aserr.args[0], "alert-danger")
@@ -202,12 +213,12 @@ def unlink_data_from_resource():
 
 @resources.route("<uuid:resource_id>/user/assign", methods=["POST"])
 @require_oauth2
-def assign_role(resource_id: uuid.UUID) -> Response:
+def assign_role(resource_id: UUID) -> Response:
     form = request.form
-    group_role_id = form.get("group_role_id", "")
+    role_id = form.get("role_id", "")
     user_email = form.get("user_email", "")
     try:
-        assert bool(group_role_id), "The role must be provided."
+        assert bool(role_id), "The role must be provided."
         assert bool(user_email), "The user email must be provided."
 
         def __assign_error__(error):
@@ -223,22 +234,22 @@ def assign_role(resource_id: uuid.UUID) -> Response:
 
         return oauth2_post(
             f"auth/resource/{resource_id}/user/assign",
-            data={
-                "group_role_id": group_role_id,
+            json={
+                "role_id": role_id,
                 "user_email": user_email
             }).either(__assign_error__, __assign_success__)
     except AssertionError as aserr:
         flash(aserr.args[0], "alert-danger")
-        return redirect(url_for("oauth2.resources.view_resource", resource_id=resource_id))
+        return redirect(url_for("oauth2.resource.view_resource", resource_id=resource_id))
 
 @resources.route("<uuid:resource_id>/user/unassign", methods=["POST"])
 @require_oauth2
-def unassign_role(resource_id: uuid.UUID) -> Response:
+def unassign_role(resource_id: UUID) -> Response:
     form = request.form
-    group_role_id = form.get("group_role_id", "")
+    role_id = form.get("role_id", "")
     user_id = form.get("user_id", "")
     try:
-        assert bool(group_role_id), "The role must be provided."
+        assert bool(role_id), "The role must be provided."
         assert bool(user_id), "The user id must be provided."
 
         def __unassign_error__(error):
@@ -254,17 +265,17 @@ def unassign_role(resource_id: uuid.UUID) -> Response:
 
         return oauth2_post(
             f"auth/resource/{resource_id}/user/unassign",
-            data={
-                "group_role_id": group_role_id,
+            json={
+                "role_id": role_id,
                 "user_id": user_id
             }).either(__unassign_error__, __unassign_success__)
     except AssertionError as aserr:
         flash(aserr.args[0], "alert-danger")
-        return redirect(url_for("oauth2.resources.view_resource", resource_id=resource_id))
+        return redirect(url_for("oauth2.resource.view_resource", resource_id=resource_id))
 
 @resources.route("/toggle/<uuid:resource_id>", methods=["POST"])
 @require_oauth2
-def toggle_public(resource_id: uuid.UUID):
+def toggle_public(resource_id: UUID):
     """Toggle the given resource's public status."""
     def __handle_error__(err):
         flash_error(process_error(err))
@@ -277,18 +288,169 @@ def toggle_public(resource_id: uuid.UUID):
             "oauth2.resource.view_resource", resource_id=resource_id))
 
     return oauth2_post(
-        f"auth/resource/{resource_id}/toggle-public", data={}).either(
+        f"auth/resource/{resource_id}/toggle-public").either(
             lambda err: __handle_error__(err),
             lambda suc: __handle_success__(suc))
 
 @resources.route("/edit/<uuid:resource_id>", methods=["GET"])
 @require_oauth2
-def edit_resource(resource_id: uuid.UUID):
+def edit_resource(resource_id: UUID):
     """Edit the given resource."""
     return "WOULD Edit THE GIVEN RESOURCE'S DETAILS"
 
 @resources.route("/delete/<uuid:resource_id>", methods=["GET"])
 @require_oauth2
-def delete_resource(resource_id: uuid.UUID):
+def delete_resource(resource_id: UUID):
     """Delete the given resource."""
     return "WOULD DELETE THE GIVEN RESOURCE"
+
+@resources.route("/<uuid:resource_id>/roles/<uuid:role_id>", methods=["GET"])
+@require_oauth2
+def view_resource_role(resource_id: UUID, role_id: UUID):
+    """View resource role page."""
+    def __render_template__(**kwargs):
+        return render_ui("oauth2/view-resource-role.html", **kwargs)
+
+    def __fetch_users__(resource, role, unassigned_privileges):
+        return oauth2_get(
+            f"auth/resource/{resource_id}/role/{role_id}/users").either(
+            lambda error: __render_template__(
+                resource=resource,
+                role=role,
+                unassigned_privileges=unassigned_privileges,
+                user_error=process_error(error)),
+            lambda users: __render_template__(
+                resource=resource,
+                role=role,
+                unassigned_privileges=unassigned_privileges,
+                users=users))
+
+    def __fetch_all_roles__(resource, role):
+        return oauth2_get(f"auth/resource/{resource_id}/roles").either(
+            lambda error: __render_template__(
+                all_roles_error=process_error(error)),
+            lambda all_roles: __fetch_users__(
+                resource=resource,
+                role=role,
+                unassigned_privileges=[
+                    priv for role in all_roles
+                    for priv in role["privileges"]
+                    if priv not in role["privileges"]
+                ]))
+
+    def __fetch_resource_role__(resource):
+        return oauth2_get(
+        f"auth/resource/{resource_id}/role/{role_id}").either(
+            lambda error: __render_template__(
+                resource=resource,
+                role_id=role_id,
+                role_error=process_error(error)),
+            lambda role: __fetch_all_roles__(resource, role))
+
+    return oauth2_get(
+        f"auth/resource/view/{resource_id}").either(
+            lambda error: __render_template__(
+                resource_error=process_error(error)),
+            lambda resource: __fetch_resource_role__(resource=resource))
+
+@resources.route("/<uuid:resource_id>/roles/<uuid:role_id>/unassign-privilege",
+                 methods=["GET", "POST"])
+@require_oauth2
+def unassign_privilege_from_resource_role(resource_id: UUID, role_id: UUID):
+    """Remove a privilege from a resource role."""
+    form = request.form
+    returnto = redirect(url_for("oauth2.resource.view_resource_role",
+                                resource_id=resource_id,
+                                role_id=role_id))
+    privilege_id = (request.args.get("privilege_id")
+                    or form.get("privilege_id"))
+    if not privilege_id:
+        flash("You need to specify a privilege to unassign.", "alert-danger")
+        return returnto
+
+    if request.method=="POST" and form.get("confirm") == "Unassign":
+        return oauth2_post(
+            f"auth/resource/{resource_id}/role/{role_id}/unassign-privilege",
+            json={
+                "privilege_id": form["privilege_id"]
+            }).either(with_flash_error(returnto), with_flash_success(returnto))
+
+    if form.get("confirm") == "Cancel":
+        flash("Cancelled the operation to unassign the privilege.",
+              "alert-info")
+        return returnto
+
+    def __fetch_privilege__(resource, role):
+        return oauth2_get(
+            f"auth/privileges/{privilege_id}/view").either(
+                with_flash_error(returnto),
+                lambda privilege: render_ui(
+                    "oauth2/confirm-resource-role-unassign-privilege.html",
+                    resource=resource,
+                    role=role,
+                    privilege=privilege))
+
+    def __fetch_resource_role__(resource):
+        return oauth2_get(
+            f"auth/resource/{resource_id}/role/{role_id}").either(
+                with_flash_error(returnto),
+                lambda role: __fetch_privilege__(resource, role))
+
+    return oauth2_get(
+        f"auth/resource/view/{resource_id}").either(
+            with_flash_error(returnto),
+            __fetch_resource_role__)
+
+
+@resources.route("/<uuid:resource_id>/roles/create-role",
+                 methods=["GET", "POST"])
+@require_oauth2
+def create_resource_role(resource_id: UUID):
+    """Create new role for the resource."""
+    def __render__(**kwargs):
+        return render_ui("oauth2/create-role.html", **kwargs)
+
+    def __fetch_resource_roles__(resource):
+        user = session.session_info()["user"]
+        return oauth2_get(
+            f"auth/resource/{resource_id}/users/{user['user_id']}"
+            "/roles").either(
+                lambda error: {
+                    "resource": resource,
+                    "resource_role_error": process_error(error)
+                },
+                lambda roles: {"resource": resource, "roles": roles})
+
+    if request.method == "GET":
+        return oauth2_get(f"auth/resource/view/{resource_id}").map(
+            __fetch_resource_roles__).either(
+            lambda error: __render__(resource_error=error),
+            lambda kwargs: __render__(**kwargs))
+
+    formdata = request.form
+    privileges = formdata.getlist("privileges[]")
+    if not bool(privileges):
+        flash(
+            "You must provide at least one privilege for creation of the new "
+            "role.",
+            "alert-danger")
+        return redirect(url_for("oauth2.resource.create_resource_role",
+                                resource_id=resource_id))
+
+    def __handle_error__(error):
+        flash_error(process_error(error))
+        return redirect(url_for(
+            "oauth2.resource.create_resource_role", resource_id=resource_id))
+
+    def __handle_success__(success):
+        flash("Role successfully created.", "alert-success")
+        return redirect(url_for(
+            "oauth2.resource.view_resource", resource_id=resource_id))
+
+    return oauth2_post(
+        f"auth/resource/{resource_id}/roles/create",
+        json={
+            "role_name": formdata["role_name"],
+            "privileges": privileges
+        }).either(
+            __handle_error__, __handle_success__)
diff --git a/gn2/wqflask/oauth2/roles.py b/gn2/wqflask/oauth2/roles.py
index 2fe35f9b..2a21670e 100644
--- a/gn2/wqflask/oauth2/roles.py
+++ b/gn2/wqflask/oauth2/roles.py
@@ -10,31 +10,6 @@ from .request_utils import request_error, process_error
 
 roles = Blueprint("role", __name__)
 
-@roles.route("/user", methods=["GET"])
-@require_oauth2
-def user_roles():
-    def  __grerror__(roles, user_privileges, error):
-        return render_ui(
-            "oauth2/list_roles.html", roles=roles,
-            user_privileges=user_privileges,
-            group_roles_error=process_error(error))
-
-    def  __grsuccess__(roles, user_privileges, group_roles):
-        return render_ui(
-            "oauth2/list_roles.html", roles=roles,
-            user_privileges=user_privileges, group_roles=group_roles)
-
-    def __role_success__(roles):
-        uprivs = tuple(
-            privilege["privilege_id"] for role in roles
-            for privilege in role["privileges"])
-        return oauth2_get("auth/group/roles").either(
-            lambda err: __grerror__(roles, uprivs, err),
-            lambda groles: __grsuccess__(roles, uprivs, groles))
-
-    return oauth2_get("auth/system/roles").either(
-        request_error, __role_success__)
-
 @roles.route("/role/<uuid:role_id>", methods=["GET"])
 @require_oauth2
 def role(role_id: uuid.UUID):
@@ -46,54 +21,3 @@ def role(role_id: uuid.UUID):
     return oauth2_get(f"auth/role/view/{role_id}").either(
         request_error, __success__)
 
-@roles.route("/create", methods=["GET", "POST"])
-@require_oauth2
-def create_role():
-    """Create a new role."""
-    def __roles_error__(error):
-        return render_ui(
-            "oauth2/create-role.html", roles_error=process_error(error))
-
-    def __gprivs_error__(roles, error):
-        return render_ui(
-            "oauth2/create-role.html", roles=roles,
-            group_privileges_error=process_error(error))
-
-    def __success__(roles, gprivs):
-        uprivs = tuple(
-            privilege["privilege_id"] for role in roles
-            for privilege in role["privileges"])
-        return render_ui(
-            "oauth2/create-role.html", roles=roles, user_privileges=uprivs,
-            group_privileges=gprivs,
-            prev_role_name=request.args.get("role_name"))
-
-    def __fetch_gprivs__(roles):
-        return oauth2_get("auth/group/privileges").either(
-            lambda err: __gprivs_error__(roles, err),
-            lambda gprivs: __success__(roles, gprivs))
-
-    if request.method == "GET":
-        return oauth2_get("auth/user/roles").either(
-            __roles_error__, __fetch_gprivs__)
-
-    form = request.form
-    role_name = form.get("role_name")
-    privileges = form.getlist("privileges[]")
-    if len(privileges) == 0:
-        flash("You must assign at least one privilege to the role",
-              "alert-danger")
-        return redirect(url_for(
-            "oauth2.role.create_role", role_name=role_name))
-    def __create_error__(error):
-        err = process_error(error)
-        flash(f"{err['error']}: {err['error_description']}",
-              "alert-danger")
-        return redirect(url_for("oauth2.role.create_role"))
-    def __create_success__(*args):
-        flash("Role created successfully.", "alert-success")
-        return redirect(url_for("oauth2.role.user_roles"))
-    return oauth2_post(
-        "auth/group/role/create",data={
-            "role_name": role_name, "privileges[]": privileges}).either(
-        __create_error__,__create_success__)
diff --git a/gn2/wqflask/oauth2/session.py b/gn2/wqflask/oauth2/session.py
index 2ef534e2..b91534b0 100644
--- a/gn2/wqflask/oauth2/session.py
+++ b/gn2/wqflask/oauth2/session.py
@@ -22,6 +22,8 @@ class SessionInfo(TypedDict):
     user_agent: str
     ip_addr: str
     masquerade: Optional[UserDetails]
+    refreshing_token: bool
+    auth_server_jwks: Optional[dict[str, Any]]
 
 __SESSION_KEY__ = "GN::2::session_info" # Do not use this outside this module!!
 
@@ -61,16 +63,10 @@ def session_info() -> SessionInfo:
             "user_agent": request.headers.get("User-Agent"),
             "ip_addr": request.environ.get("HTTP_X_FORWARDED_FOR",
                                            request.remote_addr),
-            "masquerading": None
+            "masquerading": None,
+            "token_refreshing": False
         }))
 
-def expired():
-    the_session = session_info()
-    def __expired__(token):
-        return datetime.now() > datetime.fromtimestamp(token["expires_at"])
-    return the_session["user"]["token"].either(
-        lambda left: False,
-        __expired__)
 
 def set_user_token(token: str) -> SessionInfo:
     """Set the user's token."""
@@ -109,3 +105,16 @@ def unset_masquerading():
         "user": the_session["masquerading"],
         "masquerading": None
     })
+
+
+def toggle_token_refreshing():
+    """Toggle the state of the token_refreshing variable."""
+    _session = session_info()
+    return save_session_info({
+        **_session,
+        "token_refreshing": not _session.get("token_refreshing", False)})
+
+
+def is_token_refreshing():
+    """Returns whether the token is being refreshed or not."""
+    return session_info().get("token_refreshing", False)
diff --git a/gn2/wqflask/oauth2/toplevel.py b/gn2/wqflask/oauth2/toplevel.py
index 23965cc1..24d60311 100644
--- a/gn2/wqflask/oauth2/toplevel.py
+++ b/gn2/wqflask/oauth2/toplevel.py
@@ -3,11 +3,17 @@ import uuid
 import datetime
 from urllib.parse import urljoin, urlparse, urlunparse
 
-from authlib.jose import jwt
-from flask import (
-    flash, request, Blueprint, url_for, redirect, render_template,
-    current_app as app)
+from authlib.jose import jwt, KeySet
+from flask import (flash,
+                   request,
+                   url_for,
+                   jsonify,
+                   redirect,
+                   Blueprint,
+                   render_template,
+                   current_app as app)
 
+from . import jwks
 from . import session
 from .checks import require_oauth2
 from .request_utils import user_details, process_error
@@ -29,7 +35,9 @@ def authorisation_code():
     code = request.args.get("code", "")
     if bool(code):
         base_url = urlparse(request.base_url, scheme=request.scheme)
-        jwtkey = app.config["SSL_PRIVATE_KEY"]
+        jwtkey = jwks.newest_jwk_with_rotation(
+            jwks.jwks_directory(app, "GN2_SECRETS"),
+            int(app.config["JWKS_ROTATION_AGE_DAYS"]))
         issued = datetime.datetime.now()
         request_data = {
             "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
@@ -47,7 +55,7 @@ def authorisation_code():
                     "iss": str(oauth2_clientid()),
                     "sub": request.args["user_id"],
                     "aud": urljoin(authserver_uri(), "auth/token"),
-                    "exp": (issued + datetime.timedelta(minutes=5)),
+                    "exp": (issued + datetime.timedelta(minutes=5)).timestamp(),
                     "nbf": int(issued.timestamp()),
                     "iat": int(issued.timestamp()),
                     "jti": str(uuid.uuid4())},
@@ -75,8 +83,17 @@ def authorisation_code():
             })
             return redirect("/")
 
-        return no_token_post(
-            "auth/token", data=request_data).either(
-                lambda err: __error__(process_error(err)), __success__)
+        return no_token_post("auth/token", json=request_data).either(
+            lambda err: __error__(process_error(err)), __success__)
     flash("AuthorisationError: No code was provided.", "alert-danger")
     return redirect("/")
+
+
+@toplevel.route("/public-jwks", methods=["GET"])
+def public_jwks():
+    """Provide endpoint that returns the public keys."""
+    return jsonify({
+        "documentation": "The keys are listed in order of creation.",
+        "jwks": KeySet(jwks.list_jwks(
+            jwks.jwks_directory(app, "GN2_SECRETS"))).as_dict().get("keys")
+    })
diff --git a/gn2/wqflask/oauth2/ui.py b/gn2/wqflask/oauth2/ui.py
index cf2e9af7..04095420 100644
--- a/gn2/wqflask/oauth2/ui.py
+++ b/gn2/wqflask/oauth2/ui.py
@@ -1,22 +1,17 @@
 """UI utilities"""
-from flask import session, render_template
+from flask import render_template
 
 from .client import oauth2_get
-from .client import user_logged_in
-from .request_utils import process_error
 
 def render_ui(templatepath: str, **kwargs):
     """Handle repetitive UI rendering stuff."""
-    roles = kwargs.get("roles", tuple()) # Get roles if already provided
-    if user_logged_in() and not bool(roles): # If not, try fetching them
-        roles_results = oauth2_get("auth/system/roles").either(
-            lambda err: {"roles_error": process_error(err)},
-            lambda roles: {"roles": roles})
-        kwargs = {**kwargs, **roles_results}
+    roles = kwargs.get("roles", tuple()) # Get roles
+    if not roles:
+        roles = oauth2_get("auth/system/roles").either(
+                lambda _err: roles, lambda auth_roles: auth_roles)
     user_privileges = tuple(
-        privilege["privilege_id"] for role in roles
-        for privilege in role["privileges"])
+        privilege["privilege_id"] for role in roles for privilege in role["privileges"])
     kwargs = {
-        **kwargs, "roles": roles, "user_privileges": user_privileges
+            **kwargs, "roles": roles, "user_privileges": user_privileges,
     }
     return render_template(templatepath, **kwargs)
diff --git a/gn2/wqflask/oauth2/users.py b/gn2/wqflask/oauth2/users.py
index 8a935170..7d9186ab 100644
--- a/gn2/wqflask/oauth2/users.py
+++ b/gn2/wqflask/oauth2/users.py
@@ -1,6 +1,6 @@
 import requests
 from uuid import UUID
-from urllib.parse import urljoin
+from urllib.parse import urljoin, urlparse
 
 from authlib.integrations.base_client.errors import OAuthError
 from flask import (
@@ -11,10 +11,16 @@ from . import client
 from . import session
 from .ui import render_ui
 from .checks import require_oauth2
-from .client import (oauth2_get, oauth2_post, oauth2_client,
-                     authserver_uri, user_logged_in)
-from .request_utils import (
-    user_details, request_error, process_error, with_flash_error)
+from .client import (oauth2_get,
+                     oauth2_post,
+                     oauth2_client,
+                     authserver_uri,
+                     user_logged_in)
+from .request_utils import (user_details,
+                            request_error,
+                            process_error,
+                            with_flash_error,
+                            authserver_authorise_uri)
 
 users = Blueprint("user", __name__)
 
@@ -61,7 +67,7 @@ def request_add_to_group() -> Response:
         return redirect(url_for("oauth2.user.user_profile"))
 
     return oauth2_post(f"auth/group/requests/join/{group_id}",
-                       data=form).either(__error__, __success__)
+                       json=form).either(__error__, __success__)
 
 
 @users.route("/logout", methods=["GET", "POST"])
@@ -84,7 +90,8 @@ def logout():
             f"{the_session['masquerading']['name']} "
             f"({the_session['masquerading']['email']})",
             "alert-success")
-        return redirect("/")
+
+    return redirect("/")
 
 @users.route("/register", methods=["GET", "POST"])
 def register_user():
@@ -101,11 +108,14 @@ def register_user():
     form = request.form
     response = requests.post(
         urljoin(authserver_uri(), "auth/user/register"),
-        data = {
+        json = {
             "user_name": form.get("user_name"),
             "email": form.get("email_address"),
             "password": form.get("password"),
-            "confirm_password": form.get("confirm_password")})
+            "confirm_password": form.get("confirm_password"),
+            **dict(
+                item.split("=") for item in
+                urlparse(authserver_authorise_uri()).query.split("&"))})
     results = response.json()
     if "error" in results:
         error_messages = tuple(