Skip to content

Commit

Permalink
providers/oauth2: token revoke (#3077)
Browse files Browse the repository at this point in the history
  • Loading branch information
BeryJu committed Jun 11, 2022
1 parent 24a21c1 commit 8dbb0bd
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 91 deletions.
123 changes: 79 additions & 44 deletions authentik/providers/oauth2/errors.py
Expand Up @@ -95,38 +95,45 @@ class TokenIntrospectionError(OAuth2Error):
class AuthorizeError(OAuth2Error):
"""General Authorization Errors"""

_errors = {
errors = {
# OAuth2 errors.
# https://tools.ietf.org/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",
"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",
"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-User" "consent",
"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",
"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"
),
}

def __init__(
Expand All @@ -138,7 +145,7 @@ def __init__(
):
super().__init__()
self.error = error
self.description = self._errors[error]
self.description = self.errors[error]
self.redirect_uri = redirect_uri
self.grant_type = grant_type
self.state = state
Expand Down Expand Up @@ -170,19 +177,25 @@ class TokenError(OAuth2Error):

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",
"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):
Expand All @@ -191,17 +204,39 @@ def __init__(self, error):
self.description = self.errors[error]


class TokenRevocationError(OAuth2Error):
"""
Specific to the revocation endpoint.
See https://tools.ietf.org/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 BearerTokenError(OAuth2Error):
"""
OAuth2 errors.
https://tools.ietf.org/html/rfc6750#section-3.1
"""

_errors = {
errors = {
"invalid_request": ("The request is otherwise malformed", 400),
"invalid_token": (
"The access token provided is expired, revoked, malformed, "
"or invalid for other reasons",
(
"The access token provided is expired, revoked, malformed, "
"or invalid for other reasons"
),
401,
),
"insufficient_scope": (
Expand All @@ -213,6 +248,6 @@ class BearerTokenError(OAuth2Error):
def __init__(self, code):
super().__init__()
self.code = code
error_tuple = self._errors.get(code, ("", ""))
error_tuple = self.errors.get(code, ("", ""))
self.description = error_tuple[0]
self.status = error_tuple[1]
98 changes: 98 additions & 0 deletions authentik/providers/oauth2/tests/test_introspect.py
@@ -0,0 +1,98 @@
"""Test introspect view"""
import json
from base64 import b64encode
from dataclasses import asdict

from django.urls import reverse

from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id, generate_key
from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.tests.utils import OAuthTestCase


class TesOAuth2Introspection(OAuthTestCase):
"""Test introspect view"""

def setUp(self) -> None:
super().setUp()
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="",
signing_key=create_test_cert(),
)
self.app = Application.objects.create(
name=generate_id(), slug=generate_id(), provider=self.provider
)
self.app.save()
self.user = create_test_admin_user()
self.token: RefreshToken = RefreshToken.objects.create(
provider=self.provider,
user=self.user,
access_token=generate_id(),
refresh_token=generate_id(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
self.auth = b64encode(
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
).decode()

def test_introspect(self):
"""Test introspect"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-introspection"),
HTTP_AUTHORIZATION=f"Basic {self.auth}",
data={"token": self.token.refresh_token, "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"aud": None,
"sub": "bar",
"exp": None,
"iat": None,
"iss": "foo",
"active": True,
"client_id": self.provider.client_id,
},
)

def test_introspect_invalid_token(self):
"""Test introspect (invalid token)"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-introspection"),
HTTP_AUTHORIZATION=f"Basic {self.auth}",
data={"token": generate_id(), "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"active": False,
},
)

def test_introspect_invalid_auth(self):
"""Test introspect (invalid auth)"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-introspection"),
HTTP_AUTHORIZATION="Basic qwerqrwe",
data={"token": generate_id(), "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"active": False,
},
)
74 changes: 74 additions & 0 deletions authentik/providers/oauth2/tests/test_revoke.py
@@ -0,0 +1,74 @@
"""Test revoke view"""
import json
from base64 import b64encode
from dataclasses import asdict

from django.urls import reverse

from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id, generate_key
from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.tests.utils import OAuthTestCase


class TesOAuth2Revoke(OAuthTestCase):
"""Test revoke view"""

def setUp(self) -> None:
super().setUp()
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="",
signing_key=create_test_cert(),
)
self.app = Application.objects.create(
name=generate_id(), slug=generate_id(), provider=self.provider
)
self.app.save()
self.user = create_test_admin_user()
self.token: RefreshToken = RefreshToken.objects.create(
provider=self.provider,
user=self.user,
access_token=generate_id(),
refresh_token=generate_id(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
self.auth = b64encode(
f"{self.provider.client_id}:{self.provider.client_secret}".encode()
).decode()

def test_revoke(self):
"""Test revoke"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-revoke"),
HTTP_AUTHORIZATION=f"Basic {self.auth}",
data={"token": self.token.refresh_token, "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 200)

def test_revoke_invalid(self):
"""Test revoke (invalid token)"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-revoke"),
HTTP_AUTHORIZATION=f"Basic {self.auth}",
data={"token": self.token.refresh_token + "foo", "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 200)

def test_revoke_invalid_auth(self):
"""Test revoke (invalid auth)"""
res = self.client.post(
reverse("authentik_providers_oauth2:token-revoke"),
HTTP_AUTHORIZATION="Basic fqewr",
data={"token": self.token.refresh_token, "token_type_hint": "refresh_token"},
)
self.assertEqual(res.status_code, 401)
4 changes: 2 additions & 2 deletions authentik/providers/oauth2/tests/test_userinfo.py
Expand Up @@ -19,9 +19,9 @@ class TestUserinfo(OAuthTestCase):
def setUp(self) -> None:
super().setUp()
ObjectManager().run()
self.app = Application.objects.create(name="test", slug="test")
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
name="test",
name=generate_id(),
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
Expand Down
6 changes: 6 additions & 0 deletions authentik/providers/oauth2/urls.py
Expand Up @@ -10,6 +10,7 @@
from authentik.providers.oauth2.views.jwks import JWKSView
from authentik.providers.oauth2.views.provider import ProviderInfoView
from authentik.providers.oauth2.views.token import TokenView
from authentik.providers.oauth2.views.token_revoke import TokenRevokeView
from authentik.providers.oauth2.views.userinfo import UserInfoView

urlpatterns = [
Expand All @@ -29,6 +30,11 @@
csrf_exempt(TokenIntrospectionView.as_view()),
name="token-introspection",
),
path(
"revoke/",
csrf_exempt(TokenRevokeView.as_view()),
name="token-revoke",
),
path(
"<slug:application_slug>/end-session/",
RedirectView.as_view(pattern_name="authentik_core:if-session-end"),
Expand Down

0 comments on commit 8dbb0bd

Please sign in to comment.