about summary refs log tree commit diff
path: root/gn3/auth
diff options
context:
space:
mode:
Diffstat (limited to 'gn3/auth')
-rw-r--r--gn3/auth/__init__.py1
-rw-r--r--gn3/auth/authentication/__init__.py24
-rw-r--r--gn3/auth/authentication/exceptions.py4
-rw-r--r--gn3/auth/authentication/oauth2/__init__.py0
-rw-r--r--gn3/auth/authentication/oauth2/endpoints/__init__.py0
-rw-r--r--gn3/auth/authentication/oauth2/endpoints/introspection.py48
-rw-r--r--gn3/auth/authentication/oauth2/endpoints/revocation.py22
-rw-r--r--gn3/auth/authentication/oauth2/endpoints/utilities.py31
-rw-r--r--gn3/auth/authentication/oauth2/grants/__init__.py0
-rw-r--r--gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py85
-rw-r--r--gn3/auth/authentication/oauth2/grants/password_grant.py22
-rw-r--r--gn3/auth/authentication/oauth2/models/__init__.py0
-rw-r--r--gn3/auth/authentication/oauth2/models/authorization_code.py93
-rw-r--r--gn3/auth/authentication/oauth2/server.py72
-rw-r--r--gn3/auth/authentication/oauth2/views.py104
-rw-r--r--gn3/auth/authorisation/checks.py3
-rw-r--r--gn3/auth/authorisation/data/views.py4
-rw-r--r--gn3/auth/authorisation/groups/models.py2
-rw-r--r--gn3/auth/authorisation/groups/views.py5
-rw-r--r--gn3/auth/authorisation/oauth2/oauth2client.py (renamed from gn3/auth/authentication/oauth2/models/oauth2client.py)2
-rw-r--r--gn3/auth/authorisation/oauth2/oauth2token.py (renamed from gn3/auth/authentication/oauth2/models/oauth2token.py)2
-rw-r--r--gn3/auth/authorisation/oauth2/resource_server.py (renamed from gn3/auth/authentication/oauth2/resource_server.py)2
-rw-r--r--gn3/auth/authorisation/privileges.py2
-rw-r--r--gn3/auth/authorisation/resources/checks.py2
-rw-r--r--gn3/auth/authorisation/resources/models.py2
-rw-r--r--gn3/auth/authorisation/resources/views.py4
-rw-r--r--gn3/auth/authorisation/roles/models.py2
-rw-r--r--gn3/auth/authorisation/roles/views.py3
-rw-r--r--gn3/auth/authorisation/users/__init__.py12
-rw-r--r--gn3/auth/authorisation/users/admin/__init__.py2
-rw-r--r--gn3/auth/authorisation/users/admin/ui.py27
-rw-r--r--gn3/auth/authorisation/users/admin/views.py230
-rw-r--r--gn3/auth/authorisation/users/base.py (renamed from gn3/auth/authentication/users.py)0
-rw-r--r--gn3/auth/authorisation/users/collections/views.py4
-rw-r--r--gn3/auth/authorisation/users/masquerade/__init__.py1
-rw-r--r--gn3/auth/authorisation/users/masquerade/models.py67
-rw-r--r--gn3/auth/authorisation/users/masquerade/views.py48
-rw-r--r--gn3/auth/authorisation/users/models.py2
-rw-r--r--gn3/auth/authorisation/users/views.py9
-rw-r--r--gn3/auth/views.py5
40 files changed, 34 insertions, 914 deletions
diff --git a/gn3/auth/__init__.py b/gn3/auth/__init__.py
index a28498d..cd65e9b 100644
--- a/gn3/auth/__init__.py
+++ b/gn3/auth/__init__.py
@@ -1,5 +1,4 @@
 """Top-Level `Auth` module"""
 from . import authorisation
-from . import authentication
 
 from .views import oauth2
diff --git a/gn3/auth/authentication/__init__.py b/gn3/auth/authentication/__init__.py
deleted file mode 100644
index 42ceacb..0000000
--- a/gn3/auth/authentication/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Handle authentication requests"""
-
-import bcrypt
-
-def credentials_in_database(cursor, email: str, password: str) -> bool:
-    """Check whether credentials are in the database."""
-    if len(email.strip()) == 0 or len(password.strip()) == 0:
-        return False
-
-    cursor.execute(
-        ("SELECT "
-         "users.email, user_credentials.password "
-         "FROM users LEFT JOIN user_credentials "
-         "ON users.user_id = user_credentials.user_id "
-         "WHERE users.email = :email"),
-        {"email": email})
-    results = cursor.fetchall()
-    if len(results) == 0:
-        return False
-
-    assert len(results) == 1, "Expected one row."
-    row = results[0]
-    return (email == row[0] and
-            bcrypt.checkpw(password.encode("utf-8"), row[1]))
diff --git a/gn3/auth/authentication/exceptions.py b/gn3/auth/authentication/exceptions.py
deleted file mode 100644
index c31e691..0000000
--- a/gn3/auth/authentication/exceptions.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""Exceptions for authentication"""
-
-class AuthenticationError(Exception):
-    """Base exception class for `gn3.auth.authentication` package."""
diff --git a/gn3/auth/authentication/oauth2/__init__.py b/gn3/auth/authentication/oauth2/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/gn3/auth/authentication/oauth2/__init__.py
+++ /dev/null
diff --git a/gn3/auth/authentication/oauth2/endpoints/__init__.py b/gn3/auth/authentication/oauth2/endpoints/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/gn3/auth/authentication/oauth2/endpoints/__init__.py
+++ /dev/null
diff --git a/gn3/auth/authentication/oauth2/endpoints/introspection.py b/gn3/auth/authentication/oauth2/endpoints/introspection.py
deleted file mode 100644
index cfe2998..0000000
--- a/gn3/auth/authentication/oauth2/endpoints/introspection.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Handle introspection of tokens."""
-import datetime
-from urllib.parse import urlparse
-
-from flask import request as flask_request
-from authlib.oauth2.rfc7662 import (
-    IntrospectionEndpoint as _IntrospectionEndpoint)
-
-from gn3.auth.authentication.oauth2.models.oauth2token import OAuth2Token
-
-from .utilities import query_token as _query_token
-
-def get_token_user_sub(token: OAuth2Token) -> str:# pylint: disable=[unused-argument]
-    """
-    Return the token's subject as defined in
-    https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
-    """
-    ## For now a dummy return to prevent issues.
-    return "sub"
-
-class IntrospectionEndpoint(_IntrospectionEndpoint):
-    """Introspect token."""
-    def query_token(self, token_string: str, token_type_hint: str):
-        """Query the token."""
-        return _query_token(self, token_string, token_type_hint)
-
-    def introspect_token(self, token: OAuth2Token) -> dict:
-        """Return the introspection information."""
-        url = urlparse(flask_request.url)
-        return {
-            "active": True,
-            "scope": token.get_scope(),
-            "client_id": token.client.client_id,
-            "username": token.user.name,
-            "token_type": token.token_type,
-            "exp": int(token.expires_at.timestamp()),
-            "iat": int(token.issued_at.timestamp()),
-            "nbf": int(
-                (token.issued_at - datetime.timedelta(seconds=120)).timestamp()),
-            # "sub": get_token_user_sub(token),
-            "aud": token.client.client_id,
-            "iss": f"{url.scheme}://{url.netloc}",
-            "jti": token.token_id
-        }
-
-    def check_permission(self, token, client, request):
-        """Check that the client has permission to introspect token."""
-        return client.client_type == "internal"
diff --git a/gn3/auth/authentication/oauth2/endpoints/revocation.py b/gn3/auth/authentication/oauth2/endpoints/revocation.py
deleted file mode 100644
index b8517b6..0000000
--- a/gn3/auth/authentication/oauth2/endpoints/revocation.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Handle token revocation."""
-
-from flask import current_app
-from authlib.oauth2.rfc7009 import RevocationEndpoint as _RevocationEndpoint
-
-from gn3.auth import db
-from gn3.auth.authentication.oauth2.models.oauth2token import (
-    save_token, OAuth2Token, revoke_token)
-
-from .utilities import query_token as _query_token
-
-class RevocationEndpoint(_RevocationEndpoint):
-    """Revoke the tokens"""
-    ENDPOINT_NAME = "revoke"
-    def query_token(self, token_string: str, token_type_hint: str):
-        """Query the token."""
-        return _query_token(self, token_string, token_type_hint)
-
-    def revoke_token(self, token: OAuth2Token, request):
-        """Revoke token `token`."""
-        with db.connection(current_app.config["AUTH_DB"]) as conn:
-            save_token(conn, revoke_token(token))
diff --git a/gn3/auth/authentication/oauth2/endpoints/utilities.py b/gn3/auth/authentication/oauth2/endpoints/utilities.py
deleted file mode 100644
index e13784e..0000000
--- a/gn3/auth/authentication/oauth2/endpoints/utilities.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""endpoint utilities"""
-from typing import Any, Optional
-
-from flask import current_app
-from pymonad.maybe import Nothing
-
-from gn3.auth import db
-from gn3.auth.authentication.oauth2.models.oauth2token import (
-    OAuth2Token, token_by_access_token, token_by_refresh_token)
-
-def query_token(# pylint: disable=[unused-argument]
-        endpoint_object: Any, token_str: str, token_type_hint) -> Optional[
-            OAuth2Token]:
-    """Retrieve the token from the database."""
-    def __identity__(val):
-        return val
-    token = Nothing
-    with db.connection(current_app.config["AUTH_DB"]) as conn:
-        if token_type_hint == "access_token":
-            token = token_by_access_token(conn, token_str)
-        if token_type_hint == "access_token":
-            token = token_by_refresh_token(conn, token_str)
-
-        return token.maybe(
-            token_by_access_token(conn, token_str).maybe(
-                token_by_refresh_token(conn, token_str).maybe(
-                    None, __identity__),
-                __identity__),
-            __identity__)
-
-    return None
diff --git a/gn3/auth/authentication/oauth2/grants/__init__.py b/gn3/auth/authentication/oauth2/grants/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/gn3/auth/authentication/oauth2/grants/__init__.py
+++ /dev/null
diff --git a/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py b/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py
deleted file mode 100644
index fb8d436..0000000
--- a/gn3/auth/authentication/oauth2/grants/authorisation_code_grant.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""Classes and function for Authorisation Code flow."""
-import uuid
-import string
-import random
-from typing import Optional
-from datetime import datetime
-
-from flask import current_app as app
-from authlib.oauth2.rfc6749 import grants
-from authlib.oauth2.rfc7636 import create_s256_code_challenge
-
-from gn3.auth import db
-from gn3.auth.db_utils import with_db_connection
-from gn3.auth.authentication.users import User
-
-from ..models.oauth2client import OAuth2Client
-from ..models.authorization_code import (
-    AuthorisationCode, authorisation_code, save_authorisation_code)
-
-class AuthorisationCodeGrant(grants.AuthorizationCodeGrant):
-    """Implement the 'Authorisation Code' grant."""
-    TOKEN_ENDPOINT_AUTH_METHODS: list[str] = [
-        "client_secret_basic", "client_secret_post"]
-    AUTHORIZATION_CODE_LENGTH: int = 48
-    TOKEN_ENDPOINT_HTTP_METHODS = ['POST']
-    GRANT_TYPE = "authorization_code"
-    RESPONSE_TYPES = {'code'}
-
-    def save_authorization_code(self, code, request):
-        """Persist the authorisation code to database."""
-        client = request.client
-        nonce = "".join(random.sample(string.ascii_letters + string.digits,
-                                      k=self.AUTHORIZATION_CODE_LENGTH))
-        return __save_authorization_code__(AuthorisationCode(
-            uuid.uuid4(), code, client, request.redirect_uri, request.scope,
-            nonce, int(datetime.now().timestamp()),
-            create_s256_code_challenge(app.config["SECRET_KEY"]),
-            "S256", request.user))
-
-    def query_authorization_code(self, code, client):
-        """Retrieve the code from the database."""
-        return __query_authorization_code__(code, client)
-
-    def delete_authorization_code(self, authorization_code):
-        """Delete the authorisation code."""
-        with db.connection(app.config["AUTH_DB"]) as conn:
-            with db.cursor(conn) as cursor:
-                cursor.execute(
-                    "DELETE FROM authorisation_code WHERE code_id=?",
-                    (str(authorization_code.code_id),))
-
-    def authenticate_user(self, authorization_code) -> Optional[User]:
-        """Authenticate the user who own the authorisation code."""
-        query = (
-            "SELECT users.* FROM authorisation_code LEFT JOIN users "
-            "ON authorisation_code.user_id=users.user_id "
-            "WHERE authorisation_code.code=?")
-        with db.connection(app.config["AUTH_DB"]) as conn:
-            with db.cursor(conn) as cursor:
-                cursor.execute(query, (str(authorization_code.code),))
-                res = cursor.fetchone()
-                if res:
-                    return User(
-                        uuid.UUID(res["user_id"]), res["email"], res["name"])
-
-        return None
-
-def __query_authorization_code__(
-        code: str, client: OAuth2Client) -> AuthorisationCode:
-    """A helper function that creates a new database connection.
-
-    This is found to be necessary since the `AuthorizationCodeGrant` class(es)
-    do not have a way to pass the database connection."""
-    def __auth_code__(conn) -> str:
-        the_code = authorisation_code(conn, code, client)
-        return the_code.maybe(None, lambda cde: cde) # type: ignore[misc, arg-type, return-value]
-
-    return with_db_connection(__auth_code__)
-
-def __save_authorization_code__(code: AuthorisationCode) -> AuthorisationCode:
-    """A helper function that creates a new database connection.
-
-    This is found to be necessary since the `AuthorizationCodeGrant` class(es)
-    do not have a way to pass the database connection."""
-    return with_db_connection(lambda conn: save_authorisation_code(conn, code))
diff --git a/gn3/auth/authentication/oauth2/grants/password_grant.py b/gn3/auth/authentication/oauth2/grants/password_grant.py
deleted file mode 100644
index 3233877..0000000
--- a/gn3/auth/authentication/oauth2/grants/password_grant.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Allows users to authenticate directly."""
-
-from flask import current_app as app
-from authlib.oauth2.rfc6749 import grants
-
-from gn3.auth import db
-from gn3.auth.authentication.users import valid_login, user_by_email
-
-from gn3.auth.authorisation.errors import NotFoundError
-
-class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
-    """Implement the 'Password' grant."""
-    TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"]
-
-    def authenticate_user(self, username, password):
-        "Authenticate the user with their username and password."
-        with db.connection(app.config["AUTH_DB"]) as conn:
-            try:
-                user = user_by_email(conn, username)
-                return user if valid_login(conn, user, password) else None
-            except NotFoundError as _nfe:
-                return None
diff --git a/gn3/auth/authentication/oauth2/models/__init__.py b/gn3/auth/authentication/oauth2/models/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/gn3/auth/authentication/oauth2/models/__init__.py
+++ /dev/null
diff --git a/gn3/auth/authentication/oauth2/models/authorization_code.py b/gn3/auth/authentication/oauth2/models/authorization_code.py
deleted file mode 100644
index f282814..0000000
--- a/gn3/auth/authentication/oauth2/models/authorization_code.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""Model and functions for handling the Authorisation Code"""
-from uuid import UUID
-from datetime import datetime
-from typing import NamedTuple
-
-from pymonad.maybe import Just, Maybe, Nothing
-
-from gn3.auth import db
-
-from .oauth2client import OAuth2Client
-
-from ...users import User, user_by_id
-
-__5_MINUTES__ = 300 # in seconds
-
-class AuthorisationCode(NamedTuple):
-    """
-    The AuthorisationCode model for the auth(entic|oris)ation system.
-    """
-    # Instance variables
-    code_id: UUID
-    code: str
-    client: OAuth2Client
-    redirect_uri: str
-    scope: str
-    nonce: str
-    auth_time: int
-    code_challenge: str
-    code_challenge_method: str
-    user: User
-
-    @property
-    def response_type(self) -> str:
-        """
-        For authorisation code flow, the response_type type MUST always be
-        'code'.
-        """
-        return "code"
-
-    def is_expired(self):
-        """Check whether the code is expired."""
-        return self.auth_time + __5_MINUTES__ < datetime.now().timestamp()
-
-    def get_redirect_uri(self):
-        """Get the redirect URI"""
-        return self.redirect_uri
-
-    def get_scope(self):
-        """Return the assigned scope for this AuthorisationCode."""
-        return self.scope
-
-    def get_nonce(self):
-        """Get the one-time use token."""
-        return self.nonce
-
-def authorisation_code(conn: db.DbConnection ,
-                       code: str,
-                       client: OAuth2Client) -> Maybe[AuthorisationCode]:
-    """
-    Retrieve the authorisation code object that corresponds to `code` and the
-    given OAuth2 client.
-    """
-    with db.cursor(conn) as cursor:
-        query = ("SELECT * FROM authorisation_code "
-                 "WHERE code=:code AND client_id=:client_id")
-        cursor.execute(
-            query, {"code": code, "client_id": str(client.client_id)})
-        result = cursor.fetchone()
-        if result:
-            return Just(AuthorisationCode(
-                UUID(result["code_id"]), result["code"], client,
-                result["redirect_uri"], result["scope"], result["nonce"],
-                int(result["auth_time"]), result["code_challenge"],
-                result["code_challenge_method"],
-                user_by_id(conn, UUID(result["user_id"]))))
-        return Nothing
-
-def save_authorisation_code(conn: db.DbConnection,
-                            auth_code: AuthorisationCode) -> AuthorisationCode:
-    """Persist the `auth_code` into the database."""
-    with db.cursor(conn) as cursor:
-        cursor.execute(
-            "INSERT INTO authorisation_code VALUES("
-            ":code_id, :code, :client_id, :redirect_uri, :scope, :nonce, "
-            ":auth_time, :code_challenge, :code_challenge_method, :user_id"
-            ")",
-            {
-                **auth_code._asdict(),
-                "code_id": str(auth_code.code_id),
-                "client_id": str(auth_code.client.client_id),
-                "user_id": str(auth_code.user.user_id)
-            })
-        return auth_code
diff --git a/gn3/auth/authentication/oauth2/server.py b/gn3/auth/authentication/oauth2/server.py
deleted file mode 100644
index 7d7113a..0000000
--- a/gn3/auth/authentication/oauth2/server.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Initialise the OAuth2 Server"""
-import uuid
-import datetime
-from typing import Callable
-
-from flask import Flask, current_app
-from authlib.oauth2.rfc6749.errors import InvalidClientError
-from authlib.integrations.flask_oauth2 import AuthorizationServer
-# from authlib.oauth2.rfc7636 import CodeChallenge
-
-from gn3.auth import db
-
-from .models.oauth2client import client
-from .models.oauth2token import OAuth2Token, save_token
-
-from .grants.password_grant import PasswordGrant
-from .grants.authorisation_code_grant import AuthorisationCodeGrant
-
-from .endpoints.revocation import RevocationEndpoint
-from .endpoints.introspection import IntrospectionEndpoint
-
-def create_query_client_func() -> Callable:
-    """Create the function that loads the client."""
-    def __query_client__(client_id: uuid.UUID):
-        # use current_app rather than passing the db_uri to avoid issues
-        # when config changes, e.g. while testing.
-        with db.connection(current_app.config["AUTH_DB"]) as conn:
-            the_client = client(conn, client_id).maybe(
-                None, lambda clt: clt) # type: ignore[misc]
-            if bool(the_client):
-                return the_client
-            raise InvalidClientError(
-                "No client found for the given CLIENT_ID and CLIENT_SECRET.")
-
-    return __query_client__
-
-def create_save_token_func(token_model: type) -> Callable:
-    """Create the function that saves the token."""
-    def __save_token__(token, request):
-        with db.connection(current_app.config["AUTH_DB"]) as conn:
-            save_token(
-                conn, token_model(
-                    token_id=uuid.uuid4(), client=request.client,
-                    user=request.user,
-                    **{
-                        "refresh_token": None, "revoked": False,
-                        "issued_at": datetime.datetime.now(),
-                        **token
-                    }))
-
-    return __save_token__
-
-def setup_oauth2_server(app: Flask) -> None:
-    """Set's up the oauth2 server for the flask application."""
-    server = AuthorizationServer()
-    server.register_grant(PasswordGrant)
-
-    # Figure out a common `code_verifier` for GN2 and GN3 and set
-    # server.register_grant(AuthorisationCodeGrant, [CodeChallenge(required=False)])
-    # below
-    server.register_grant(AuthorisationCodeGrant)
-
-    # register endpoints
-    server.register_endpoint(RevocationEndpoint)
-    server.register_endpoint(IntrospectionEndpoint)
-
-    # init server
-    server.init_app(
-        app,
-        query_client=create_query_client_func(),
-        save_token=create_save_token_func(OAuth2Token))
-    app.config["OAUTH2_SERVER"] = server
diff --git a/gn3/auth/authentication/oauth2/views.py b/gn3/auth/authentication/oauth2/views.py
deleted file mode 100644
index 2bd3865..0000000
--- a/gn3/auth/authentication/oauth2/views.py
+++ /dev/null
@@ -1,104 +0,0 @@
-"""Endpoints for the oauth2 server"""
-import uuid
-import traceback
-
-from authlib.oauth2.rfc6749.errors import InvalidClientError
-from email_validator import validate_email, EmailNotValidError
-from flask import (
-    flash,
-    request,
-    url_for,
-    redirect,
-    Response,
-    Blueprint,
-    render_template,
-    current_app as app)
-
-from gn3.auth import db
-from gn3.auth.db_utils import with_db_connection
-from gn3.auth.authorisation.errors import ForbiddenAccess
-
-from .resource_server import require_oauth
-from .endpoints.revocation import RevocationEndpoint
-from .endpoints.introspection import IntrospectionEndpoint
-
-from ..users import valid_login, NotFoundError, user_by_email
-
-auth = Blueprint("auth", __name__)
-
-@auth.route("/delete-client/<uuid:client_id>", methods=["GET", "POST"])
-def delete_client(client_id: uuid.UUID):
-    """Delete an OAuth2 client."""
-    return f"WOULD DELETE OAUTH2 CLIENT {client_id}."
-
-@auth.route("/authorise", methods=["GET", "POST"])
-def authorise():
-    """Authorise a user"""
-    try:
-        server = app.config["OAUTH2_SERVER"]
-        client_id = uuid.UUID(request.args.get(
-            "client_id",
-            request.form.get("client_id", str(uuid.uuid4()))))
-        client = server.query_client(client_id)
-        if not bool(client):
-            flash("Invalid OAuth2 client.", "alert-error")
-        if request.method == "GET":
-            client = server.query_client(request.args.get("client_id"))
-            return render_template(
-                "oauth2/authorise-user.html",
-                client=client,
-                scope=client.scope,
-                response_type="code")
-
-        form = request.form
-        def __authorise__(conn: db.DbConnection) -> Response:
-            email_passwd_msg = "Email or password is invalid!"
-            redirect_response = redirect(url_for("oauth2.auth.authorise",
-                                                 client_id=client_id))
-            try:
-                email = validate_email(
-                    form.get("user:email"), check_deliverability=False)
-                user = user_by_email(conn, email["email"])
-                if valid_login(conn, user, form.get("user:password", "")):
-                    return server.create_authorization_response(request=request, grant_user=user)
-                flash(email_passwd_msg, "alert-error")
-                return redirect_response # type: ignore[return-value]
-            except EmailNotValidError as _enve:
-                app.logger.debug(traceback.format_exc())
-                flash(email_passwd_msg, "alert-error")
-                return redirect_response # type: ignore[return-value]
-            except NotFoundError as _nfe:
-                app.logger.debug(traceback.format_exc())
-                flash(email_passwd_msg, "alert-error")
-                return redirect_response # type: ignore[return-value]
-
-        return with_db_connection(__authorise__)
-    except InvalidClientError as ice:
-        return render_template(
-            "oauth2/oauth2_error.html", error=ice), ice.status_code
-
-@auth.route("/token", methods=["POST"])
-def token():
-    """Retrieve the authorisation token."""
-    server = app.config["OAUTH2_SERVER"]
-    return server.create_token_response()
-
-@auth.route("/revoke", methods=["POST"])
-def revoke_token():
-    """Revoke the token."""
-    return app.config["OAUTH2_SERVER"].create_endpoint_response(
-        RevocationEndpoint.ENDPOINT_NAME)
-
-@auth.route("/introspect", methods=["POST"])
-@require_oauth("introspect")
-def introspect_token() -> Response:
-    """Provide introspection information for the token."""
-    # This is dangerous to provide publicly
-    authorised_clients = app.config.get(
-        "OAUTH2_CLIENTS_WITH_INTROSPECTION_PRIVILEGE", [])
-    with require_oauth.acquire("introspect") as the_token:
-        if the_token.client.client_id in authorised_clients:
-            return app.config["OAUTH2_SERVER"].create_endpoint_response(
-                IntrospectionEndpoint.ENDPOINT_NAME)
-
-    raise ForbiddenAccess("You cannot access this endpoint")
diff --git a/gn3/auth/authorisation/checks.py b/gn3/auth/authorisation/checks.py
index 1c87c02..17daca4 100644
--- a/gn3/auth/authorisation/checks.py
+++ b/gn3/auth/authorisation/checks.py
@@ -5,12 +5,11 @@ from typing import Callable
 from flask import request, current_app as app
 
 from gn3.auth import db
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
 
 from . import privileges as auth_privs
 from .errors import InvalidData, AuthorisationError
 
-from ..authentication.oauth2.resource_server import require_oauth
-
 def __system_privileges_in_roles__(conn, user):
     """
     This really is a hack since groups are not treated as resources at the
diff --git a/gn3/auth/authorisation/data/views.py b/gn3/auth/authorisation/data/views.py
index 8adf862..81811dd 100644
--- a/gn3/auth/authorisation/data/views.py
+++ b/gn3/auth/authorisation/data/views.py
@@ -29,9 +29,9 @@ from gn3.auth.authorisation.resources.checks import authorised_for
 from gn3.auth.authorisation.resources.models import (
     user_resources, public_resources, attach_resources_data)
 
-from gn3.auth.authentication.users import User
-from gn3.auth.authentication.oauth2.resource_server import require_oauth
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
 
+from gn3.auth.authorisation.users import User
 from gn3.auth.authorisation.data.phenotypes import link_phenotype_data
 from gn3.auth.authorisation.data.mrna import link_mrna_data, ungrouped_mrna_data
 from gn3.auth.authorisation.data.genotypes import (
diff --git a/gn3/auth/authorisation/groups/models.py b/gn3/auth/authorisation/groups/models.py
index 5a3ae50..7212a78 100644
--- a/gn3/auth/authorisation/groups/models.py
+++ b/gn3/auth/authorisation/groups/models.py
@@ -9,7 +9,7 @@ from pymonad.maybe import Just, Maybe, Nothing
 
 from gn3.auth import db
 from gn3.auth.dictify import dictify
-from gn3.auth.authentication.users import User, user_by_id
+from gn3.auth.authorisation.users import User, user_by_id
 
 from ..checks import authorised_p
 from ..privileges import Privilege
diff --git a/gn3/auth/authorisation/groups/views.py b/gn3/auth/authorisation/groups/views.py
index 628df36..a849a73 100644
--- a/gn3/auth/authorisation/groups/views.py
+++ b/gn3/auth/authorisation/groups/views.py
@@ -12,6 +12,8 @@ from gn3 import db_utils as gn3db
 
 from gn3.auth.dictify import dictify
 from gn3.auth.db_utils import with_db_connection
+from gn3.auth.authorisation.users import User
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
 
 from .data import link_data_to_group
 from .models import (
@@ -28,9 +30,6 @@ from ..checks import authorised_p
 from ..privileges import Privilege, privileges_by_ids
 from ..errors import InvalidData, NotFoundError, AuthorisationError
 
-from ...authentication.users import User
-from ...authentication.oauth2.resource_server import require_oauth
-
 groups = Blueprint("groups", __name__)
 
 @groups.route("/list", methods=["GET"])
diff --git a/gn3/auth/authentication/oauth2/models/oauth2client.py b/gn3/auth/authorisation/oauth2/oauth2client.py
index 2a307e3..dc54a41 100644
--- a/gn3/auth/authentication/oauth2/models/oauth2client.py
+++ b/gn3/auth/authorisation/oauth2/oauth2client.py
@@ -7,9 +7,9 @@ from typing import Sequence, Optional, NamedTuple
 from pymonad.maybe import Just, Maybe, Nothing
 
 from gn3.auth import db
-from gn3.auth.authentication.users import User, users, user_by_id, same_password
 
 from gn3.auth.authorisation.errors import NotFoundError
+from gn3.auth.authorisation.users import User, users, user_by_id, same_password
 
 class OAuth2Client(NamedTuple):
     """
diff --git a/gn3/auth/authentication/oauth2/models/oauth2token.py b/gn3/auth/authorisation/oauth2/oauth2token.py
index bfe4aaf..bb19039 100644
--- a/gn3/auth/authentication/oauth2/models/oauth2token.py
+++ b/gn3/auth/authorisation/oauth2/oauth2token.py
@@ -6,9 +6,9 @@ from typing import NamedTuple, Optional
 from pymonad.maybe import Just, Maybe, Nothing
 
 from gn3.auth import db
-from gn3.auth.authentication.users import User, user_by_id
 
 from gn3.auth.authorisation.errors import NotFoundError
+from gn3.auth.authorisation.users import User, user_by_id
 
 from .oauth2client import client, OAuth2Client
 
diff --git a/gn3/auth/authentication/oauth2/resource_server.py b/gn3/auth/authorisation/oauth2/resource_server.py
index 223e811..e806dc5 100644
--- a/gn3/auth/authentication/oauth2/resource_server.py
+++ b/gn3/auth/authorisation/oauth2/resource_server.py
@@ -5,7 +5,7 @@ from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator
 from authlib.integrations.flask_oauth2 import ResourceProtector
 
 from gn3.auth import db
-from gn3.auth.authentication.oauth2.models.oauth2token import token_by_access_token
+from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token
 
 class BearerTokenValidator(_BearerTokenValidator):
     """Extends `authlib.oauth2.rfc6750.BearerTokenValidator`"""
diff --git a/gn3/auth/authorisation/privileges.py b/gn3/auth/authorisation/privileges.py
index dbb4129..7907d76 100644
--- a/gn3/auth/authorisation/privileges.py
+++ b/gn3/auth/authorisation/privileges.py
@@ -2,7 +2,7 @@
 from typing import Any, Iterable, NamedTuple
 
 from gn3.auth import db
-from gn3.auth.authentication.users import User
+from gn3.auth.authorisation.users import User
 
 class Privilege(NamedTuple):
     """Class representing a privilege: creates immutable objects."""
diff --git a/gn3/auth/authorisation/resources/checks.py b/gn3/auth/authorisation/resources/checks.py
index fafde76..1f5a0f9 100644
--- a/gn3/auth/authorisation/resources/checks.py
+++ b/gn3/auth/authorisation/resources/checks.py
@@ -4,7 +4,7 @@ from functools import reduce
 from typing import Sequence
 
 from gn3.auth import db
-from gn3.auth.authentication.users import User
+from gn3.auth.authorisation.users import User
 
 def __organise_privileges_by_resource_id__(rows):
     def __organise__(privs, row):
diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py
index b301a93..cf7769e 100644
--- a/gn3/auth/authorisation/resources/models.py
+++ b/gn3/auth/authorisation/resources/models.py
@@ -7,7 +7,7 @@ from typing import Any, Dict, Sequence, Optional, NamedTuple
 
 from gn3.auth import db
 from gn3.auth.dictify import dictify
-from gn3.auth.authentication.users import User
+from gn3.auth.authorisation.users import User
 from gn3.auth.db_utils import with_db_connection
 
 from .checks import authorised_for
diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py
index 3b2bbeb..bda67cd 100644
--- a/gn3/auth/authorisation/resources/views.py
+++ b/gn3/auth/authorisation/resources/views.py
@@ -7,6 +7,8 @@ from functools import reduce
 from flask import request, jsonify, Response, Blueprint, current_app as app
 
 from gn3.auth.db_utils import with_db_connection
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
+from gn3.auth.authorisation.users import User, user_by_id, user_by_email
 
 from .checks import authorised_for
 from .models import (
@@ -21,8 +23,6 @@ from ..groups.models import Group, GroupRole, group_role_by_id
 
 from ... import db
 from ...dictify import dictify
-from ...authentication.oauth2.resource_server import require_oauth
-from ...authentication.users import User, user_by_id, user_by_email
 
 resources = Blueprint("resources", __name__)
 
diff --git a/gn3/auth/authorisation/roles/models.py b/gn3/auth/authorisation/roles/models.py
index 97e11af..890d33b 100644
--- a/gn3/auth/authorisation/roles/models.py
+++ b/gn3/auth/authorisation/roles/models.py
@@ -7,7 +7,7 @@ from pymonad.either import Left, Right, Either
 
 from gn3.auth import db
 from gn3.auth.dictify import dictify
-from gn3.auth.authentication.users import User
+from gn3.auth.authorisation.users import User
 from gn3.auth.authorisation.errors import AuthorisationError
 
 from ..checks import authorised_p
diff --git a/gn3/auth/authorisation/roles/views.py b/gn3/auth/authorisation/roles/views.py
index 3670aab..d00e596 100644
--- a/gn3/auth/authorisation/roles/views.py
+++ b/gn3/auth/authorisation/roles/views.py
@@ -5,11 +5,10 @@ from flask import jsonify, Response, Blueprint, current_app
 
 from gn3.auth import db
 from gn3.auth.dictify import dictify
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
 
 from .models import user_role
 
-from ...authentication.oauth2.resource_server import require_oauth
-
 roles = Blueprint("roles", __name__)
 
 @roles.route("/view/<uuid:role_id>", methods=["GET"])
diff --git a/gn3/auth/authorisation/users/__init__.py b/gn3/auth/authorisation/users/__init__.py
index e69de29..5f0c89c 100644
--- a/gn3/auth/authorisation/users/__init__.py
+++ b/gn3/auth/authorisation/users/__init__.py
@@ -0,0 +1,12 @@
+"""Initialise the users' package."""
+from .base import (
+    User,
+    users,
+    save_user,
+    user_by_id,
+    # valid_login,
+    user_by_email,
+    hash_password, # only used in tests... maybe make gn-auth a GN3 dependency
+    same_password,
+    set_user_password
+)
diff --git a/gn3/auth/authorisation/users/admin/__init__.py b/gn3/auth/authorisation/users/admin/__init__.py
deleted file mode 100644
index 8aa0743..0000000
--- a/gn3/auth/authorisation/users/admin/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""The admin module"""
-from .views import admin
diff --git a/gn3/auth/authorisation/users/admin/ui.py b/gn3/auth/authorisation/users/admin/ui.py
deleted file mode 100644
index 242c7a6..0000000
--- a/gn3/auth/authorisation/users/admin/ui.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""UI utilities for the auth system."""
-from functools import wraps
-from flask import flash, url_for, redirect
-
-from gn3.auth.authentication.users import User
-from gn3.auth.db_utils import with_db_connection
-from gn3.auth.authorisation.roles.models import user_roles
-
-from gn3.session import logged_in, session_user, clear_session_info
-
-def is_admin(func):
-    """Verify user is a system admin."""
-    @wraps(func)
-    @logged_in
-    def __admin__(*args, **kwargs):
-        admin_roles = [
-            role for role in with_db_connection(
-                lambda conn: user_roles(
-                    conn, User(**session_user())))
-            if role.role_name == "system-administrator"]
-        if len(admin_roles) > 0:
-            return func(*args, **kwargs)
-        flash("Expected a system administrator.", "alert-danger")
-        flash("You have been logged out of the system.", "alert-info")
-        clear_session_info()
-        return redirect(url_for("oauth2.admin.login"))
-    return __admin__
diff --git a/gn3/auth/authorisation/users/admin/views.py b/gn3/auth/authorisation/users/admin/views.py
deleted file mode 100644
index c9f1887..0000000
--- a/gn3/auth/authorisation/users/admin/views.py
+++ /dev/null
@@ -1,230 +0,0 @@
-"""UI for admin stuff"""
-import uuid
-import json
-import random
-import string
-from functools import partial
-from datetime import datetime, timezone, timedelta
-
-from email_validator import validate_email, EmailNotValidError
-from flask import (
-    flash,
-    request,
-    url_for,
-    redirect,
-    Blueprint,
-    current_app,
-    render_template)
-
-
-from gn3 import session
-from gn3.auth import db
-from gn3.auth.db_utils import with_db_connection
-
-from gn3.auth.authentication.oauth2.models.oauth2client import (
-    save_client,
-    OAuth2Client,
-    oauth2_clients,
-    client as oauth2_client,
-    delete_client as _delete_client)
-from gn3.auth.authentication.users import (
-    User,
-    user_by_id,
-    valid_login,
-    user_by_email,
-    hash_password)
-
-from .ui import is_admin
-
-admin = Blueprint("admin", __name__)
-
-@admin.before_request
-def update_expires():
-    """Update session expiration."""
-    if session.session_info() and not session.update_expiry():
-        flash("Session has expired. Logging out...", "alert-warning")
-        session.clear_session_info()
-        return redirect(url_for("oauth2.admin.login"))
-    return None
-
-@admin.route("/dashboard", methods=["GET"])
-@is_admin
-def dashboard():
-    """Admin dashboard."""
-    return render_template("admin/dashboard.html")
-
-@admin.route("/login", methods=["GET", "POST"])
-def login():
-    """Log in to GN3 directly without OAuth2 client."""
-    if request.method == "GET":
-        return render_template(
-            "admin/login.html",
-            next_uri=request.args.get("next", "oauth2.admin.dashboard"))
-
-    form = request.form
-    next_uri = form.get("next_uri", "oauth2.admin.dashboard")
-    error_message = "Invalid email or password provided."
-    login_page = redirect(url_for("oauth2.admin.login", next=next_uri))
-    try:
-        email = validate_email(form.get("email", "").strip(),
-                               check_deliverability=False)
-        password = form.get("password")
-        with db.connection(current_app.config["AUTH_DB"]) as conn:
-            user = user_by_email(conn, email["email"])
-            if valid_login(conn, user, password):
-                session.update_session_info(
-                    user=user._asdict(),
-                    expires=(
-                        datetime.now(tz=timezone.utc) + timedelta(minutes=10)))
-                return redirect(url_for(next_uri))
-            flash(error_message, "alert-danger")
-            return login_page
-    except EmailNotValidError as _enve:
-        flash(error_message, "alert-danger")
-        return login_page
-
-@admin.route("/logout", methods=["GET"])
-def logout():
-    """Log out the admin."""
-    if not session.session_info():
-        flash("Not logged in.", "alert-info")
-        return redirect(url_for("oauth2.admin.login"))
-    session.clear_session_info()
-    flash("Logged out", "alert-success")
-    return redirect(url_for("oauth2.admin.login"))
-
-def random_string(length: int = 64) -> str:
-    """Generate a random string."""
-    return "".join(
-        random.choice(string.ascii_letters + string.digits + string.punctuation)
-        for _idx in range(0, length))
-
-def __response_types__(grant_types: tuple[str, ...]) -> tuple[str, ...]:
-    """Compute response types from grant types."""
-    resps = {
-        "password": ("token",),
-        "authorization_code": ("token", "code"),
-        "refresh_token": ("token",)
-    }
-    return tuple(set(
-        resp_typ for types_list
-        in (types for grant, types in resps.items() if grant in grant_types)
-        for resp_typ in types_list))
-
-@admin.route("/register-client", methods=["GET", "POST"])
-@is_admin
-def register_client():
-    """Register an OAuth2 client."""
-    def __list_users__(conn):
-        with db.cursor(conn) as cursor:
-            cursor.execute("SELECT * FROM users")
-            return tuple(
-                User(uuid.UUID(row["user_id"]), row["email"], row["name"])
-                for row in cursor.fetchall())
-    if request.method == "GET":
-        return render_template(
-            "admin/register-client.html",
-            scope=current_app.config["OAUTH2_SCOPE"],
-            users=with_db_connection(__list_users__),
-            current_user=session.session_user())
-
-    form = request.form
-    raw_client_secret = random_string()
-    default_redirect_uri = form["redirect_uri"]
-    grant_types = form.getlist("grants[]")
-    client = OAuth2Client(
-        client_id = uuid.uuid4(),
-        client_secret = hash_password(raw_client_secret),
-        client_id_issued_at = datetime.now(tz=timezone.utc),
-        client_secret_expires_at = datetime.fromtimestamp(0),
-        client_metadata = {
-            "client_name": "GN2 Dev Server",
-            "token_endpoint_auth_method": [
-                "client_secret_post", "client_secret_basic"],
-            "client_type": "confidential",
-            "grant_types": ["password", "authorization_code", "refresh_token"],
-            "default_redirect_uri": default_redirect_uri,
-            "redirect_uris": [default_redirect_uri] + form.get("other_redirect_uri", "").split(),
-            "response_type": __response_types__(tuple(grant_types)),
-            "scope": form.getlist("scope[]")
-        },
-        user = with_db_connection(partial(
-            user_by_id, user_id=uuid.UUID(form["user"])))
-    )
-    client = with_db_connection(partial(save_client, the_client=client))
-    return render_template(
-        "admin/registered-client.html",
-        client=client,
-        client_secret = raw_client_secret)
-
-def __parse_client__(sqlite3_row) -> dict:
-    """Parse the client details into python datatypes."""
-    return {
-        **dict(sqlite3_row),
-        "client_metadata": json.loads(sqlite3_row["client_metadata"])
-    }
-
-@admin.route("/list-client", methods=["GET"])
-@is_admin
-def list_clients():
-    """List all registered OAuth2 clients."""
-    return render_template(
-        "admin/list-oauth2-clients.html",
-        clients=with_db_connection(oauth2_clients))
-
-@admin.route("/view-client/<uuid:client_id>", methods=["GET"])
-@is_admin
-def view_client(client_id: uuid.UUID):
-    """View details of OAuth2 client with given `client_id`."""
-    return render_template(
-        "admin/view-oauth2-client.html",
-        client=with_db_connection(partial(oauth2_client, client_id=client_id)),
-        scope=current_app.config["OAUTH2_SCOPE"])
-
-@admin.route("/edit-client", methods=["POST"])
-@is_admin
-def edit_client():
-    """Edit the details of the given client."""
-    form = request.form
-    the_client = with_db_connection(partial(
-        oauth2_client, client_id=uuid.UUID(form["client_id"])))
-    if the_client.is_nothing():
-        flash("No such client.", "alert-danger")
-        return redirect(url_for("oauth2.admin.list_clients"))
-    the_client = the_client.value
-    client_metadata = {
-        **the_client.client_metadata,
-        "default_redirect_uri": form["default_redirect_uri"],
-        "redirect_uris": list(set(
-            [form["default_redirect_uri"]] +
-            form["other_redirect_uris"].split("\r\n"))),
-        "grants": form.getlist("grants[]"),
-        "scope": form.getlist("scope[]")
-    }
-    with_db_connection(partial(save_client, the_client=OAuth2Client(
-        the_client.client_id,
-        the_client.client_secret,
-        the_client.client_id_issued_at,
-        the_client.client_secret_expires_at,
-        client_metadata,
-        the_client.user)))
-    flash("Client updated.", "alert-success")
-    return redirect(url_for("oauth2.admin.view_client",
-                            client_id=the_client.client_id))
-
-@admin.route("/delete-client", methods=["POST"])
-@is_admin
-def delete_client():
-    """Delete the details of the client."""
-    form = request.form
-    the_client = with_db_connection(partial(
-        oauth2_client, client_id=uuid.UUID(form["client_id"])))
-    if the_client.is_nothing():
-        flash("No such client.", "alert-danger")
-        return redirect(url_for("oauth2.admin.list_clients"))
-    the_client = the_client.value
-    with_db_connection(partial(_delete_client, client=the_client))
-    flash((f"Client '{the_client.client_metadata.client_name}' was deleted "
-           "successfully."),
-          "alert-success")
-    return redirect(url_for("oauth2.admin.list_clients"))
diff --git a/gn3/auth/authentication/users.py b/gn3/auth/authorisation/users/base.py
index 0e72ed2..0e72ed2 100644
--- a/gn3/auth/authentication/users.py
+++ b/gn3/auth/authorisation/users/base.py
diff --git a/gn3/auth/authorisation/users/collections/views.py b/gn3/auth/authorisation/users/collections/views.py
index 1fa25a3..775e8bc 100644
--- a/gn3/auth/authorisation/users/collections/views.py
+++ b/gn3/auth/authorisation/users/collections/views.py
@@ -9,8 +9,8 @@ from gn3.auth.db_utils import with_db_connection
 from gn3.auth.authorisation.checks import require_json
 from gn3.auth.authorisation.errors import NotFoundError
 
-from gn3.auth.authentication.users import User, user_by_id
-from gn3.auth.authentication.oauth2.resource_server import require_oauth
+from gn3.auth.authorisation.users import User, user_by_id
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
 
 from .models import (
     add_traits,
diff --git a/gn3/auth/authorisation/users/masquerade/__init__.py b/gn3/auth/authorisation/users/masquerade/__init__.py
deleted file mode 100644
index 69d64f0..0000000
--- a/gn3/auth/authorisation/users/masquerade/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Package to deal with masquerading."""
diff --git a/gn3/auth/authorisation/users/masquerade/models.py b/gn3/auth/authorisation/users/masquerade/models.py
deleted file mode 100644
index 9f24b6b..0000000
--- a/gn3/auth/authorisation/users/masquerade/models.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""Functions for handling masquerade."""
-from uuid import uuid4
-from functools import wraps
-from datetime import datetime
-
-from flask import current_app as app
-
-from gn3.auth import db
-
-from gn3.auth.authorisation.errors import ForbiddenAccess
-from gn3.auth.authorisation.roles.models import user_roles
-
-from gn3.auth.authentication.users import User
-from gn3.auth.authentication.oauth2.models.oauth2token import (
-    OAuth2Token, save_token)
-
-__FIVE_HOURS__ = (60 * 60 * 5)
-
-def can_masquerade(func):
-    """Security decorator."""
-    @wraps(func)
-    def __checker__(*args, **kwargs):
-        if len(args) == 3:
-            conn, token, _masq_user = args
-        elif len(args) == 2:
-            conn, token = args
-        elif len(args) == 1:
-            conn = args[0]
-            token = kwargs["original_token"]
-        else:
-            conn = kwargs["conn"]
-            token = kwargs["original_token"]
-
-        masq_privs = [priv for role in user_roles(conn, token.user)
-                      for priv in role.privileges
-                      if priv.privilege_id == "system:user:masquerade"]
-        if len(masq_privs) == 0:
-            raise ForbiddenAccess(
-                "You do not have the ability to masquerade as another user.")
-        return func(*args, **kwargs)
-    return __checker__
-
-@can_masquerade
-def masquerade_as(
-        conn: db.DbConnection,
-        original_token: OAuth2Token,
-        masqueradee: User) -> OAuth2Token:
-    """Get a token that enables `masquerader` to act as `masqueradee`."""
-    token_details = app.config["OAUTH2_SERVER"].generate_token(
-        client=original_token.client,
-        grant_type="authorization_code",
-        user=masqueradee,
-        expires_in=__FIVE_HOURS__,
-        include_refresh_token=True)
-    new_token = OAuth2Token(
-        token_id=uuid4(),
-        client=original_token.client,
-        token_type=token_details["token_type"],
-        access_token=token_details["access_token"],
-        refresh_token=token_details.get("refresh_token"),
-        scope=original_token.scope,
-        revoked=False,
-        issued_at=datetime.now(),
-        expires_in=token_details["expires_in"],
-        user=masqueradee)
-    save_token(conn, new_token)
-    return new_token
diff --git a/gn3/auth/authorisation/users/masquerade/views.py b/gn3/auth/authorisation/users/masquerade/views.py
deleted file mode 100644
index 43286a1..0000000
--- a/gn3/auth/authorisation/users/masquerade/views.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Endpoints for user masquerade"""
-from uuid import UUID
-from functools import partial
-
-from flask import request, jsonify, Response, Blueprint
-
-from gn3.auth.db_utils import with_db_connection
-from gn3.auth.authorisation.errors import InvalidData
-from gn3.auth.authorisation.checks import require_json
-
-from gn3.auth.authentication.users import user_by_id
-from gn3.auth.authentication.oauth2.resource_server import require_oauth
-
-from .models import masquerade_as
-
-masq = Blueprint("masquerade", __name__)
-
-@masq.route("/", methods=["POST"])
-@require_oauth("profile user masquerade")
-@require_json
-def masquerade() -> Response:
-    """Masquerade as a particular user."""
-    with require_oauth.acquire("profile user masquerade") as token:
-        masqueradee_id = UUID(request.json["masquerade_as"])#type: ignore[index]
-        if masqueradee_id == token.user.user_id:
-            raise InvalidData("You are not allowed to masquerade as yourself.")
-
-        masq_user = with_db_connection(partial(
-            user_by_id, user_id=masqueradee_id))
-        def __masq__(conn):
-            new_token = masquerade_as(conn, original_token=token, masqueradee=masq_user)
-            return new_token
-        def __dump_token__(tok):
-            return {
-                key: value for key, value in (tok._asdict().items())
-                if key in ("access_token", "refresh_token", "expires_in",
-                           "token_type")
-            }
-        return jsonify({
-            "original": {
-                "user": token.user._asdict(),
-                "token": __dump_token__(token)
-            },
-            "masquerade_as": {
-                "user": masq_user._asdict(),
-                "token": __dump_token__(with_db_connection(__masq__))
-            }
-        })
diff --git a/gn3/auth/authorisation/users/models.py b/gn3/auth/authorisation/users/models.py
index 89c1d22..0157154 100644
--- a/gn3/auth/authorisation/users/models.py
+++ b/gn3/auth/authorisation/users/models.py
@@ -7,7 +7,7 @@ from gn3.auth.authorisation.roles.models import Role
 from gn3.auth.authorisation.checks import authorised_p
 from gn3.auth.authorisation.privileges import Privilege
 
-from gn3.auth.authentication.users import User
+from .base import User
 
 @authorised_p(
     ("system:user:list",),
diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py
index 826e222..f75b51e 100644
--- a/gn3/auth/authorisation/users/views.py
+++ b/gn3/auth/authorisation/users/views.py
@@ -10,9 +10,11 @@ from flask import request, jsonify, Response, Blueprint, current_app
 from gn3.auth import db
 from gn3.auth.dictify import dictify
 from gn3.auth.db_utils import with_db_connection
+from gn3.auth.authorisation.oauth2.resource_server import require_oauth
+from gn3.auth.authorisation.users import User, save_user, set_user_password
+from gn3.auth.authorisation.oauth2.oauth2token import token_by_access_token
 
 from .models import list_users
-from .masquerade.views import masq
 from .collections.views import collections
 
 from ..groups.models import user_group as _user_group
@@ -21,12 +23,7 @@ from ..roles.models import assign_default_roles, user_roles as _user_roles
 from ..errors import (
     NotFoundError, UsernameError, PasswordError, UserRegistrationError)
 
-from ...authentication.oauth2.resource_server import require_oauth
-from ...authentication.users import User, save_user, set_user_password
-from ...authentication.oauth2.models.oauth2token import token_by_access_token
-
 users = Blueprint("users", __name__)
-users.register_blueprint(masq, url_prefix="/masquerade")
 users.register_blueprint(collections, url_prefix="/collections")
 
 @users.route("/", methods=["GET"])
diff --git a/gn3/auth/views.py b/gn3/auth/views.py
index 56eace7..da64049 100644
--- a/gn3/auth/views.py
+++ b/gn3/auth/views.py
@@ -1,21 +1,16 @@
 """The Auth(oris|entic)ation routes/views"""
 from flask import Blueprint
 
-from .authentication.oauth2.views import auth
-
 from .authorisation.data.views import data
 from .authorisation.users.views import users
-from .authorisation.users.admin import admin
 from .authorisation.roles.views import roles
 from .authorisation.groups.views import groups
 from .authorisation.resources.views import resources
 
 oauth2 = Blueprint("oauth2", __name__)
 
-oauth2.register_blueprint(auth, url_prefix="/")
 oauth2.register_blueprint(data, url_prefix="/data")
 oauth2.register_blueprint(users, url_prefix="/user")
 oauth2.register_blueprint(roles, url_prefix="/role")
-oauth2.register_blueprint(admin, url_prefix="/admin")
 oauth2.register_blueprint(groups, url_prefix="/group")
 oauth2.register_blueprint(resources, url_prefix="/resource")