-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement e-kirjasto authentication flow for admins
- Loading branch information
Showing
11 changed files
with
342 additions
and
10 deletions.
There are no files selected for viewing
38 changes: 38 additions & 0 deletions
38
alembic/versions/20240222_9966f6f95674_create_admincredentials_table.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""Create admincredentials table | ||
Revision ID: 9966f6f95674 | ||
Revises: 993729d4bf97 | ||
Create Date: 2024-02-22 02:36:07.130941+00:00 | ||
""" | ||
import sqlalchemy as sa | ||
|
||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "9966f6f95674" | ||
down_revision = "993729d4bf97" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade() -> None: | ||
op.create_table( | ||
"admincredentials", | ||
sa.Column("id", sa.Integer(), nullable=False), | ||
sa.Column("external_id", sa.Unicode(), nullable=False), | ||
sa.Column("admin_id", sa.Integer(), nullable=False), | ||
sa.ForeignKeyConstraint(["admin_id"], ["admins.id"], ondelete="CASCADE"), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
op.create_index( | ||
op.f("ix_admincredentials_admin_id"), | ||
"admincredentials", | ||
["admin_id"], | ||
unique=False, | ||
) | ||
|
||
|
||
def downgrade() -> None: | ||
op.drop_index(op.f("ix_admincredentials_admin_id"), table_name="admincredentials") | ||
op.drop_table("admincredentials") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import logging | ||
|
||
import requests | ||
from flask import url_for | ||
from pydantic import BaseModel | ||
|
||
from api.admin.admin_authentication_provider import AdminAuthenticationProvider | ||
from api.admin.template_styles import button_style, input_style, label_style | ||
from api.admin.templates import ekirjasto_sign_in_template | ||
from api.circulation_exceptions import RemoteInitiatedServerError | ||
from api.problem_details import ( | ||
EKIRJASTO_REMOTE_AUTHENTICATION_FAILED, | ||
INVALID_EKIRJASTO_TOKEN, | ||
) | ||
from core.util.problem_detail import ProblemDetail | ||
|
||
|
||
class EkirjastoUserInfo(BaseModel): | ||
exp: int | ||
family_name: str = "" | ||
given_name: str = "" | ||
role: str | ||
sub: str | ||
sid: str | ||
municipality: str | ||
verified: bool = False | ||
passkeys: list[dict] = [] | ||
|
||
|
||
class EkirjastoAdminAuthenticationProvider(AdminAuthenticationProvider): | ||
NAME = "Ekirjasto Auth" | ||
|
||
SIGN_IN_TEMPLATE = ekirjasto_sign_in_template.format( | ||
label=label_style, input=input_style, button=button_style | ||
) | ||
|
||
# TODO: make configurable | ||
_ekirjasto_api_url = "https://e-kirjasto.loikka.dev" | ||
|
||
def sign_in_template(self, redirect): | ||
redirect_uri = url_for( | ||
"ekirjasto_auth_finish", redirect_uri=redirect, _external=True | ||
) | ||
# TODO: make configurable | ||
state = ":T0008" # test state for "orgadmin" role | ||
ekirjasto_auth_url = ( | ||
f"{self._ekirjasto_api_url}/v1/auth/tunnistus/start" | ||
f"?locale=fi" | ||
f"&state={state}" | ||
f"&redirect_uri={redirect_uri}" | ||
) | ||
return self.SIGN_IN_TEMPLATE % dict( | ||
ekirjasto_auth_url=ekirjasto_auth_url, | ||
) | ||
|
||
def active_credentials(self, admin): | ||
# TODO: Not sure where this is used, need to figure it out. | ||
return True | ||
|
||
def ekirjasto_authenticate( | ||
self, ekirjasto_token | ||
) -> EkirjastoUserInfo | ProblemDetail: | ||
return self._get_user_info(ekirjasto_token) | ||
|
||
def _get_user_info(self, ekirjasto_token: str) -> EkirjastoUserInfo | ProblemDetail: | ||
userinfo_url = self._ekirjasto_api_url + "/v1/auth/userinfo" | ||
try: | ||
response = requests.get( | ||
userinfo_url, headers={"Authorization": f"Bearer {ekirjasto_token}"} | ||
) | ||
except requests.exceptions.ConnectionError as e: | ||
raise RemoteInitiatedServerError(str(e), self.__class__.__name__) | ||
|
||
if response.status_code == 401: | ||
return INVALID_EKIRJASTO_TOKEN | ||
elif response.status_code != 200: | ||
logging.error( | ||
"Got unexpected response code %d, content=%s", | ||
response.status_code, | ||
(response.content or b"No content").decode("utf-8", errors="replace"), | ||
) | ||
return EKIRJASTO_REMOTE_AUTHENTICATION_FAILED | ||
else: | ||
try: | ||
return EkirjastoUserInfo(**response.json()) | ||
except requests.exceptions.JSONDecodeError as e: | ||
raise RemoteInitiatedServerError(str(e), self.__class__.__name__) | ||
|
||
def try_revoke_ekirjasto_session(self, ekirjasto_token: str) -> None: | ||
revoke_url = self._ekirjasto_api_url + "/v1/auth/revoke" | ||
|
||
try: | ||
response = requests.post( | ||
revoke_url, headers={"Authorization": f"Bearer {ekirjasto_token}"} | ||
) | ||
except requests.exceptions.ConnectionError as e: | ||
logging.exception( | ||
"Failed to revoke ekirjasto session due to connection error." | ||
) | ||
# Ignore connection error, we tried our best | ||
|
||
# Response codes in 4xx range mean that session is already expired, thus ok. | ||
# For 5xx range, we want to log the response | ||
if 500 <= response.status_code < 600: | ||
# Decode response content assuming UTF-8, replace with the correct encoding if necessary | ||
logging.error( | ||
"Failed to revoke ekirjasto session due to server error, status=%s, content=%s", | ||
response.status_code, | ||
(response.content or b"No content").decode("utf-8", errors="replace"), | ||
) | ||
# Ignore the error response, we tried our best |
Oops, something went wrong.