Skip to content

Commit

Permalink
Merge pull request #450 from SUNET/ft-new_auth
Browse files Browse the repository at this point in the history
Security zone backend
  • Loading branch information
helylle authored Jun 17, 2024
2 parents c84ed2b + 1f0043c commit d124178
Show file tree
Hide file tree
Showing 74 changed files with 2,363 additions and 1,691 deletions.
111 changes: 104 additions & 7 deletions src/eduid/common/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

from datetime import timedelta
from enum import Enum
from enum import Enum, unique
from pathlib import Path
from re import Pattern
from typing import IO, Annotated, Any, Iterable, Mapping, Optional, Sequence, TypeVar, Union
Expand Down Expand Up @@ -340,7 +340,6 @@ class ErrorsConfigMixin(BaseModel):


class Pysaml2SPConfigMixin(BaseModel):
frontend_action_finish_url: dict[str, str] = Field(default={})

# Authn algorithms
authn_sign_alg: str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
Expand All @@ -350,7 +349,106 @@ class Pysaml2SPConfigMixin(BaseModel):
safe_relay_domain: str = "eduid.se"


class ProofingConfigMixin(BaseModel):
@unique
class FrontendAction(Enum):
ADD_SECURITY_KEY_AUTHN = "addSecurityKeyAuthn"
CHANGE_PW_AUTHN = "changepwAuthn"
CHANGE_SECURITY_SETTINGS_AUTHN = "changeSecuritySettingsAuthn"
LOGIN = "login"
LOGIN_MFA_AUTHN = "loginMfaAuthn"
OLD_LOGIN = "oldLogin"
REMOVE_IDENTITY = "removeIdentity"
REMOVE_SECURITY_KEY_AUTHN = "removeSecurityKeyAuthn"
RESET_PW_MFA_AUTHN = "resetpwMfaAuthn"
TERMINATE_ACCOUNT_AUTHN = "terminateAccountAuthn"
VERIFY_CREDENTIAL = "verifyCredential"
VERIFY_IDENTITY = "verifyIdentity"


class AuthnParameters(BaseModel):
force_authn: bool = False # a new authentication was required
force_mfa: bool = False # require MFA even if the user has no token (use Freja or other)
high_security: bool = False # opportunistic MFA, request it if the user has a token
same_user: bool = True # the same user was required to log in, such as when entering the security center
max_age: timedelta = timedelta(minutes=5) # the maximum age of the authentication
allow_login_auth: bool = False # allow login authentication as substitute action
finish_url: str # str as we want to use unformatted parts as {app_name} and {authn_id}


class FrontendActionMixin(BaseModel):
# TODO: maybe we should add a meta action shared by the frontend actions that needs the same level of
# security so that we could allow an action "of the same level" to be used for another action
# if the current way means to many logins for the users we can explore it.
frontend_action_authn_parameters: dict[FrontendAction, AuthnParameters] = Field(
default={
FrontendAction.ADD_SECURITY_KEY_AUTHN: AuthnParameters(
high_security=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.CHANGE_PW_AUTHN: AuthnParameters(
force_authn=True,
high_security=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.CHANGE_SECURITY_SETTINGS_AUTHN: AuthnParameters(
force_authn=True,
high_security=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.LOGIN: AuthnParameters(
same_user=False,
finish_url="https://eduid.se/login/ext-return/{app_name}/{authn_id}",
),
FrontendAction.LOGIN_MFA_AUTHN: AuthnParameters(
force_authn=True,
allow_login_auth=True,
finish_url="https://eduid.se/login/ext-return/{app_name}/{authn_id}",
),
FrontendAction.OLD_LOGIN: AuthnParameters(
same_user=False,
finish_url="https://eduid.se/profile/",
),
FrontendAction.REMOVE_SECURITY_KEY_AUTHN: AuthnParameters(
force_mfa=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.RESET_PW_MFA_AUTHN: AuthnParameters(
force_authn=True,
allow_login_auth=True,
finish_url="https://eduid.se/login/ext-return/{app_name}/{authn_id}",
),
FrontendAction.VERIFY_IDENTITY: AuthnParameters(
force_authn=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.TERMINATE_ACCOUNT_AUTHN: AuthnParameters(
force_authn=True,
high_security=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.VERIFY_CREDENTIAL: AuthnParameters(
force_authn=True,
force_mfa=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
FrontendAction.REMOVE_IDENTITY: AuthnParameters(
force_authn=True,
high_security=True,
allow_login_auth=True,
finish_url="https://eduid.se/profile/ext-return/{app_name}/{authn_id}",
),
}
)


class ProofingConfigMixin(FrontendActionMixin):
# sweden connect
trust_framework: TrustFramework = TrustFramework.SWECONN
required_loa: list[str] = Field(default=["loa3"])
Expand All @@ -377,9 +475,6 @@ class ProofingConfigMixin(BaseModel):
security_key_proofing_version: str = Field(default="2023v2")
security_key_foreign_eid_proofing_version: str = Field(default="2022v1")

frontend_action_finish_url: dict[str, str] = Field(default={})
fallback_redirect_url: str = "https://dashboard.eduid.se"


class EduIDBaseAppConfig(RootConfig, LoggingConfigMixin, StatsConfigMixin, RedisConfigMixin):
available_languages: Mapping[str, str] = Field(default={"en": "English", "sv": "Svenska"})
Expand All @@ -392,9 +487,11 @@ class EduIDBaseAppConfig(RootConfig, LoggingConfigMixin, StatsConfigMixin, Redis
# The list is a list of regex that are matched against the path of the
# requested URL ex. ^/test$.
no_authn_urls: list[str] = Field(default=["^/status/healthy$", "^/status/sanity-check$"])
# Feature opt-in for new-style authn responses, requires new frontend code.
enable_authn_json_response: bool = False
status_cache_seconds: int = 10
# All AuthnBaseApps need this to redirect not-logged-in requests to the authn service
token_service_url: str
authn_service_url: str


ReasonableDomainName = Annotated[str, Field(min_length=len("x.se")), AfterValidator(lambda v: v.lower())]
Expand Down
8 changes: 7 additions & 1 deletion src/eduid/common/misc/encoders.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from datetime import datetime
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Union

from bson import ObjectId
from pydantic import TypeAdapter
from saml2.saml import NameID


Expand All @@ -12,7 +14,11 @@ class EduidJSONEncoder(json.JSONEncoder):
def default(self, o: Any) -> Union[str, Any]:
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, timedelta):
return TypeAdapter(timedelta).dump_python(o, mode="json")
if isinstance(o, (ObjectId, NameID)):
return str(o)
if isinstance(o, Enum):
return o.value

return super().default(o)
16 changes: 16 additions & 0 deletions src/eduid/common/models/saml2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-

from enum import unique, Enum

__author__ = "lundberg"


@unique
class EduidAuthnContextClass(str, Enum):
DIGG_LOA2 = "http://id.elegnamnden.se/loa/1.0/loa2"
REFEDS_MFA = "https://refeds.org/profile/mfa"
REFEDS_SFA = "https://refeds.org/profile/sfa"
FIDO_U2F = "https://www.swamid.se/specs/id-fido-u2f-ce-transports"
EDUID_MFA = "https://eduid.se/specs/mfa"
PASSWORD_PT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
NOT_IMPLEMENTED = "not implemented"
4 changes: 4 additions & 0 deletions src/eduid/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ def parse_weak_version(version: ObjectId | str) -> ObjectId | str:
if isinstance(version, ObjectId):
return version
return version.lstrip('W/"').rstrip('"')


def uuid4_str() -> str:
return str(uuid4())
2 changes: 1 addition & 1 deletion src/eduid/userdb/idp/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Any, Optional

from eduid.userdb import User
from eduid.webapp.idp.assurance_data import EduidAuthnContextClass
from eduid.common.models.saml2 import EduidAuthnContextClass

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion src/eduid/userdb/tests/test_idp_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from eduid.common.testing_base import normalised_data
from eduid.userdb.fixtures.users import UserFixtures
from eduid.userdb.idp.user import SUPPORTED_SAML_ATTRIBUTES, IdPUser, SAMLAttributeSettings
from eduid.webapp.idp.assurance_data import EduidAuthnContextClass
from eduid.common.models.saml2 import EduidAuthnContextClass

__author__ = "lundberg"

Expand Down
7 changes: 0 additions & 7 deletions src/eduid/webapp/authn/acs_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ def login_action(args: ACSArgs) -> ACSResult:
"""
Upon successful login in the IdP, store login info in the session
and redirect back to the app that asked for authn.
:param session_info: the SAML session info
:param user: the authenticated user
:param authndata: data about this particular authentication event
"""
current_app.logger.info(f"User {args.user} logging in.")
if not args.user:
Expand Down Expand Up @@ -70,9 +66,6 @@ def _reauthn(reason: str, args: ACSArgs) -> ACSResult:
"""
Upon successful reauthn in the IdP, update the session and redirect back to the app that asked for reauthn.
:param session_info: the SAML session info
:param user: the authenticated user
:param authndata: data about this particular authentication event
"""
current_app.logger.info(f"Re-authenticating user {args.user} for {reason}.")
current_app.logger.debug(f"Data about this authentication: {args.authn_req}")
Expand Down
34 changes: 7 additions & 27 deletions src/eduid/webapp/authn/helpers.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,13 @@
import logging
from typing import Optional
from enum import unique

from eduid.common.misc.timeutil import utc_now
from eduid.userdb.credentials import Credential
from eduid.webapp.common.authn.acs_enums import AuthnAcsAction
from eduid.webapp.common.session import session
from eduid.webapp.common.session.namespaces import SP_AuthnRequest
from eduid.webapp.common.api.messages import TranslatableMsg

logger = logging.getLogger(__name__)


def credential_used_to_authenticate(credential: Credential, max_age: int) -> bool:
@unique
class AuthnMsg(TranslatableMsg):
"""
Check if a particular credential was used to authenticate (using the eduID IdP and authn).
Messages sent to the front end with information on the results of the
attempted operations on the back end.
"""
logger.debug(f"Checking if credential {credential} has been used in the last {max_age} seconds")

login = session.authn.sp.get_authn_for_action(AuthnAcsAction.login)
reauthn = session.authn.sp.get_authn_for_action(AuthnAcsAction.reauthn)

if _credential_recently_used(credential, login, max_age) or _credential_recently_used(credential, reauthn, max_age):
return True
return False


def _credential_recently_used(credential: Credential, action: Optional[SP_AuthnRequest], max_age: int) -> bool:
if action and credential.key in action.credentials_used:
if action.authn_instant is not None:
age = (utc_now() - action.authn_instant).total_seconds()
if 0 < age < max_age:
return True
return False
frontend_action_not_supported = "authn.frontend_action_not_supported"
37 changes: 37 additions & 0 deletions src/eduid/webapp/authn/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from marshmallow import fields

from eduid.webapp.common.api.schemas.base import EduidSchema, FluxStandardAction
from eduid.webapp.common.api.schemas.csrf import CSRFRequestMixin, CSRFResponseMixin

__author__ = "lundberg"


class AuthnCommonRequestSchema(EduidSchema, CSRFRequestMixin):
"""A verify request for either an identity or a credential proofing."""

frontend_action = fields.String(required=True)
frontend_state = fields.String(required=False)
method = fields.String(required=False)


class AuthnCommonResponseSchema(FluxStandardAction):
class AuthnCommonResponsePayload(EduidSchema, CSRFResponseMixin):
location = fields.String(required=False)

payload = fields.Nested(AuthnCommonResponsePayload)


class AuthnStatusRequestSchema(EduidSchema, CSRFRequestMixin):
authn_id = fields.String(required=False)


class AuthnStatusResponseSchema(EduidSchema, CSRFResponseMixin):
class StatusResponsePayload(EduidSchema, CSRFResponseMixin):
authn_id = fields.String(required=False)
frontend_action = fields.String(required=True)
frontend_state = fields.String(required=False)
method = fields.String(required=True)
error = fields.Boolean(required=False)
status = fields.String(required=False)

payload = fields.Nested(StatusResponsePayload)
24 changes: 6 additions & 18 deletions src/eduid/webapp/authn/settings/common.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
from typing import Mapping
from eduid.common.config.base import EduIDBaseAppConfig, ErrorsConfigMixin, FrontendActionMixin, Pysaml2SPConfigMixin
from eduid.common.models.generic import HttpUrlStr

from pydantic import Field

from eduid.common.config.base import EduIDBaseAppConfig, ErrorsConfigMixin, Pysaml2SPConfigMixin


class AuthnConfig(EduIDBaseAppConfig, ErrorsConfigMixin, Pysaml2SPConfigMixin):
class AuthnConfig(EduIDBaseAppConfig, ErrorsConfigMixin, Pysaml2SPConfigMixin, FrontendActionMixin):
"""
Configuration for the authn app
"""

app_name: str = "authn"
server_name: str = "authn"
required_loa: Mapping[str, str] = Field(
default={
"personal": "http://www.swamid.se/policy/assurance/al1",
"helpdesk": "http://www.swamid.se/policy/assurance/al2",
"admin": "http://www.swamid.se/policy/assurance/al3",
}
)
available_loa: str = "http://www.swamid.se/policy/assurance/al1"
signup_authn_success_redirect_url: str = "https://dashboard.eduid.se"
signup_authn_failure_redirect_url: str = "https://dashboard.eduid.se"
signup_authn_success_redirect_url: HttpUrlStr = "https://eduid.se/profile/"
signup_authn_failure_redirect_url: HttpUrlStr = "https://eduid.se/profile/"
fallback_frontend_action_redirect_url: HttpUrlStr = "https://eduid.se/profile/"
saml2_login_redirect_url: str
saml2_logout_redirect_url: str
saml2_strip_saml_user_suffix: str

token_service_url: str
Loading

0 comments on commit d124178

Please sign in to comment.