This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/authentik/providers/oauth2/errors.py
Jens L 80f4fccd35
providers/oauth2: OpenID conformance ()
* don't open inspector by default when debug is enabled

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* encode error in fragment when using hybrid grant_type

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* require nonce for all response_types that get an id_token from the authorization endpoint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* don't set empty family_name

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only set at_hash when response has token

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* cleaner way to get login time

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove authentication requirement from authentication flow

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use wrapper

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix auth_time not being handled correctly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* minor cleanup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add test files

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove USER_LOGIN_AUTHENTICATED

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework prompt=login handling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* also set last login uid for max_age check to prevent double login when max_age and prompt=login is set

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-23 15:26:41 +01:00

300 lines
9.9 KiB
Python

"""OAuth errors"""
from typing import Optional
from urllib.parse import quote, urlparse
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from authentik.events.models import Event, EventAction
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.views import bad_request_message
from authentik.providers.oauth2.models import GrantTypes
class OAuth2Error(SentryIgnoredException):
"""Base class for all OAuth2 Errors"""
error: str
description: str
def create_dict(self):
"""Return error as dict for JSON Rendering"""
return {
"error": self.error,
"error_description": self.description,
}
def __repr__(self) -> str:
return self.error
def to_event(self, message: Optional[str] = None, **kwargs) -> Event:
"""Create configuration_error Event."""
return Event.new(
EventAction.CONFIGURATION_ERROR,
message=message or self.description,
**kwargs,
)
class RedirectUriError(OAuth2Error):
"""The request fails due to a missing, invalid, or mismatching
redirection URI (redirect_uri)."""
error = "Redirect URI Error"
description = (
"The request fails due to a missing, invalid, or mismatching "
"redirection URI (redirect_uri)."
)
provided_uri: str
allowed_uris: list[str]
def __init__(self, provided_uri: str, allowed_uris: list[str]) -> None:
super().__init__()
self.provided_uri = provided_uri
self.allowed_uris = allowed_uris
def to_event(self, **kwargs) -> Event:
return super().to_event(
(
f"Invalid redirect URI was used. Client used '{self.provided_uri}'. "
f"Allowed redirect URIs are {','.join(self.allowed_uris)}"
),
**kwargs,
)
class ClientIdError(OAuth2Error):
"""The client identifier (client_id) is missing or invalid."""
error = "Client ID Error"
description = "The client identifier (client_id) is missing or invalid."
client_id: str
def __init__(self, client_id: str) -> None:
super().__init__()
self.client_id = client_id
def to_event(self, **kwargs) -> Event:
return super().to_event(f"Invalid client identifier: {self.client_id}.", **kwargs)
class UserAuthError(OAuth2Error):
"""
Specific to the Resource Owner Password Credentials flow when
the Resource Owners credentials are not valid.
"""
error = "access_denied"
description = "The resource owner or authorization server denied the request."
class TokenIntrospectionError(OAuth2Error):
"""
Specific to the introspection endpoint. This error will be converted
to an "active: false" response, as per the spec.
See https://datatracker.ietf.org/doc/html/rfc7662
"""
class AuthorizeError(OAuth2Error):
"""General Authorization Errors"""
errors = {
# OAuth2 errors.
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
"invalid_request": "The request is otherwise malformed",
"unauthorized_client": (
"The client is not authorized to request an authorization code using this method"
),
"access_denied": "The resource owner or authorization server denied the request",
"unsupported_response_type": (
"The authorization server does not support obtaining an authorization code "
"using this method"
),
"invalid_scope": "The requested scope is invalid, unknown, or malformed",
"server_error": "The authorization server encountered an error",
"temporarily_unavailable": (
"The authorization server is currently unable to handle the request due to a "
"temporary overloading or maintenance of the server"
),
# OpenID errors.
# http://openid.net/specs/openid-connect-core-1_0.html#AuthError
"interaction_required": (
"The Authorization Server requires End-User interaction of some form to proceed"
),
"login_required": "The Authorization Server requires End-User authentication",
"account_selection_required": (
"The End-User is required to select a session at the Authorization Server"
),
"consent_required": "The Authorization Server requires End-Userconsent",
"invalid_request_uri": (
"The request_uri in the Authorization Request returns an error or contains invalid data"
),
"invalid_request_object": "The request parameter contains an invalid Request Object",
"request_not_supported": "The provider does not support use of the request parameter",
"request_uri_not_supported": (
"The provider does not support use of the request_uri parameter"
),
"registration_not_supported": (
"The provider does not support use of the registration parameter"
),
}
# pylint: disable=too-many-arguments
def __init__(
self,
redirect_uri: str,
error: str,
grant_type: str,
state: str,
description: Optional[str] = None,
):
super().__init__()
self.error = error
if description:
self.description = description
else:
self.description = self.errors[error]
self.redirect_uri = redirect_uri
self.grant_type = grant_type
self.state = state
def get_response(self, request: HttpRequest) -> HttpResponse:
"""Wrapper around `self.create_uri()` that checks if the resulting URI is valid
(we might not have self.redirect_uri set), and returns a valid HTTP Response"""
uri = self.create_uri()
if urlparse(uri).scheme != "":
return HttpResponseRedirect(uri)
return bad_request_message(request, self.description, title=self.error)
def create_uri(self) -> str:
"""Get a redirect URI with the error message"""
description = quote(str(self.description))
# See:
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
fragment_or_query = (
"#" if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID] else "?"
)
uri = (
f"{self.redirect_uri}{fragment_or_query}error="
f"{self.error}&error_description={description}"
)
# Add state if present.
uri = uri + (f"&state={self.state}" if self.state else "")
return uri
class TokenError(OAuth2Error):
"""
OAuth2 token endpoint errors.
https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
"""
errors = {
"invalid_request": "The request is otherwise malformed",
"invalid_client": (
"Client authentication failed (e.g., unknown client, no client authentication "
"included, or unsupported authentication method)"
),
"invalid_grant": (
"The provided authorization grant or refresh token is invalid, expired, revoked, "
"does not match the redirection URI used in the authorization request, "
"or was issued to another client"
),
"unauthorized_client": (
"The authenticated client is not authorized to use this authorization grant type"
),
"unsupported_grant_type": (
"The authorization grant type is not supported by the authorization server"
),
"invalid_scope": (
"The requested scope is invalid, unknown, malformed, or exceeds the scope "
"granted by the resource owner"
),
}
def __init__(self, error):
super().__init__()
self.error = error
self.description = self.errors[error]
class TokenRevocationError(OAuth2Error):
"""
Specific to the revocation endpoint.
See https://datatracker.ietf.org/doc/html/rfc7662
"""
errors = TokenError.errors | {
"unsupported_token_type": (
"The authorization server does not support the revocation of the presented "
"token type. That is, the client tried to revoke an access token on a server not"
"supporting this feature."
)
}
def __init__(self, error: str):
super().__init__()
self.error = error
self.description = self.errors[error]
class DeviceCodeError(OAuth2Error):
"""
Device-code flow errors
See https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
"""
errors = {
"authorization_pending": (
"The authorization request is still pending as the end user hasn't "
"yet completed the user-interaction steps"
),
"access_denied": "The authorization request was denied.",
"expired_token": (
'The "device_code" has expired, and the device authorization '
"session has concluded. The client MAY commence a new device "
"authorization request but SHOULD wait for user interaction before "
"restarting to avoid unnecessary polling."
),
}
def __init__(self, error: str):
super().__init__()
self.error = error
self.description = self.errors[error]
class BearerTokenError(OAuth2Error):
"""
OAuth2 errors.
https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
"""
errors = {
"invalid_request": ("The request is otherwise malformed", 400),
"invalid_token": (
(
"The access token provided is expired, revoked, malformed, "
"or invalid for other reasons"
),
401,
),
"insufficient_scope": (
"The request requires higher privileges than provided by the access token",
403,
),
}
def __init__(self, code):
super().__init__()
self.code = code
error_tuple = self.errors.get(code, ("", ""))
self.description = error_tuple[0]
self.status = error_tuple[1]