b0fbd576fc
* ATH-01-001: resolve path and check start before loading blueprints
This is even less of an issue since 411ef239f6
, since with that commit we only allow files that the listing returns
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-010: fix missing user filter for webauthn device
This prevents an attack that is only possible when an attacker can intercept HTTP traffic and in the case of HTTPS decrypt it.
* ATH-01-008: fix web forms not submitting correctly when pressing enter
When submitting some forms with the Enter key instead of clicking "Confirm"/etc, the form would not get submitted correctly
This would in the worst case is when setting a user's password, where the new password can end up in the URL, but the password was not actually saved to the user.
* ATH-01-004: remove env from admin system endpoint
this endpoint already required admin access, but for debugging the env variables are used very little
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-003 / ATH-01-012: disable htmlLabels in mermaid
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-005: use hmac.compare_digest for secret_key authentication
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-009: migrate impersonation to use API
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-010: rework
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-014: save authenticator validation state in flow context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
bugfixes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* ATH-01-012: escape quotation marks
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* add website
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* update release ntoes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* update with all notes
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
* fix format
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""API Authentication"""
|
|
from hmac import compare_digest
|
|
from typing import Any, Optional
|
|
|
|
from django.conf import settings
|
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
|
from rest_framework.exceptions import AuthenticationFailed
|
|
from rest_framework.request import Request
|
|
from structlog.stdlib import get_logger
|
|
|
|
from authentik.core.middleware import CTX_AUTH_VIA
|
|
from authentik.core.models import Token, TokenIntents, User
|
|
from authentik.outposts.models import Outpost
|
|
from authentik.providers.oauth2.constants import SCOPE_AUTHENTIK_API
|
|
|
|
LOGGER = get_logger()
|
|
|
|
|
|
def validate_auth(header: bytes) -> Optional[str]:
|
|
"""Validate that the header is in a correct format,
|
|
returns type and credentials"""
|
|
auth_credentials = header.decode().strip()
|
|
if auth_credentials == "" or " " not in auth_credentials:
|
|
return None
|
|
auth_type, _, auth_credentials = auth_credentials.partition(" ")
|
|
if auth_type.lower() != "bearer":
|
|
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
|
raise AuthenticationFailed("Unsupported authentication type")
|
|
if auth_credentials == "": # nosec # noqa
|
|
raise AuthenticationFailed("Malformed header")
|
|
return auth_credentials
|
|
|
|
|
|
def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|
"""raw_header in the Format of `Bearer ....`"""
|
|
user = auth_user_lookup(raw_header)
|
|
if not user:
|
|
return None
|
|
if not user.is_active:
|
|
raise AuthenticationFailed("Token invalid/expired")
|
|
return user
|
|
|
|
|
|
def auth_user_lookup(raw_header: bytes) -> Optional[User]:
|
|
"""raw_header in the Format of `Bearer ....`"""
|
|
from authentik.providers.oauth2.models import AccessToken
|
|
|
|
auth_credentials = validate_auth(raw_header)
|
|
if not auth_credentials:
|
|
return None
|
|
# first, check traditional tokens
|
|
key_token = Token.filter_not_expired(
|
|
key=auth_credentials, intent=TokenIntents.INTENT_API
|
|
).first()
|
|
if key_token:
|
|
CTX_AUTH_VIA.set("api_token")
|
|
return key_token.user
|
|
# then try to auth via JWT
|
|
jwt_token = AccessToken.filter_not_expired(
|
|
token=auth_credentials, _scope__icontains=SCOPE_AUTHENTIK_API
|
|
).first()
|
|
if jwt_token:
|
|
# Double-check scopes, since they are saved in a single string
|
|
# we want to check the parsed version too
|
|
if SCOPE_AUTHENTIK_API not in jwt_token.scope:
|
|
raise AuthenticationFailed("Token invalid/expired")
|
|
CTX_AUTH_VIA.set("jwt")
|
|
return jwt_token.user
|
|
# then try to auth via secret key (for embedded outpost/etc)
|
|
user = token_secret_key(auth_credentials)
|
|
if user:
|
|
CTX_AUTH_VIA.set("secret_key")
|
|
return user
|
|
raise AuthenticationFailed("Token invalid/expired")
|
|
|
|
|
|
def token_secret_key(value: str) -> Optional[User]:
|
|
"""Check if the token is the secret key
|
|
and return the service account for the managed outpost"""
|
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
|
|
|
if not compare_digest(value, settings.SECRET_KEY):
|
|
return None
|
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
|
if not outposts:
|
|
return None
|
|
outpost = outposts.first()
|
|
return outpost.user
|
|
|
|
|
|
class TokenAuthentication(BaseAuthentication):
|
|
"""Token-based authentication using HTTP Bearer authentication"""
|
|
|
|
def authenticate(self, request: Request) -> tuple[User, Any] | None:
|
|
"""Token-based authentication using HTTP Bearer authentication"""
|
|
auth = get_authorization_header(request)
|
|
|
|
user = bearer_auth(auth)
|
|
# None is only returned when the header isn't set.
|
|
if not user:
|
|
return None
|
|
|
|
return (user, None) # pragma: no cover
|