Skip to content

Commit

Permalink
providers/oauth2: initial client_credentials grant support
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
  • Loading branch information
BeryJu committed Mar 5, 2022
1 parent 680d4fc commit 52e7c8f
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 41 deletions.
2 changes: 1 addition & 1 deletion authentik/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
return
# Since we can't directly pass other variables to signals, and we want to log the method
# and the token used, we assume we're running in a flow and set a variable in the context
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
flow_plan: FlowPlan = request.session.get(SESSION_KEY_PLAN, FlowPlan(""))
flow_plan.context[PLAN_CONTEXT_METHOD] = method
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(sanitize_dict(kwargs))
request.session[SESSION_KEY_PLAN] = flow_plan
Expand Down
3 changes: 3 additions & 0 deletions authentik/providers/oauth2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"

PROMPT_NONE = "none"
PROMPT_CONSNET = "consent"
PROMPT_LOGIN = "login"

SCOPE_OPENID = "openid"
SCOPE_OPENID_PROFILE = "profile"
SCOPE_OPENID_EMAIL = "email"
Expand Down
4 changes: 2 additions & 2 deletions authentik/providers/oauth2/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class TokenError(OAuth2Error):
https://tools.ietf.org/html/rfc6749#section-5.2
"""

_errors = {
errors = {
"invalid_request": "The request is otherwise malformed",
"invalid_client": "Client authentication failed (e.g., unknown client, "
"no client authentication included, or unsupported "
Expand All @@ -188,7 +188,7 @@ class TokenError(OAuth2Error):
def __init__(self, error):
super().__init__()
self.error = error
self.description = self._errors[error]
self.description = self.errors[error]


class BearerTokenError(OAuth2Error):
Expand Down
174 changes: 174 additions & 0 deletions authentik/providers/oauth2/tests/test_token_client_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""Test token view"""
from json import loads

from django.test import RequestFactory
from django.urls import reverse
from jwt import decode

from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
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.managed.manager import ObjectManager
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS,
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
)
from authentik.providers.oauth2.errors import TokenError
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase


class TestTokenClientCredentials(OAuthTestCase):
"""Test token (client_credentials) view"""

def setUp(self) -> None:
super().setUp()
ObjectManager().run()
self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create(
name="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
self.user = create_test_admin_user("sa")
self.user.attributes[USER_ATTRIBUTE_SA] = True
self.user.save()
self.token = Token.objects.create(
identifier="sa-token",
user=self.user,
intent=TokenIntents.INTENT_APP_PASSWORD,
expiring=False,
)

def test_wrong_user(self):
"""test invalid username"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"username": "saa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)

def test_wrong_token(self):
"""test invalid token"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key + "foo",
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)

def test_non_sa(self):
"""test non service-account"""
self.user.attributes[USER_ATTRIBUTE_SA] = False
self.user.save()
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)

def test_no_provider(self):
"""test no provider"""
self.app.provider = None
self.app.save()
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)

def test_permission_denied(self):
"""test permission denied"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.app,
order=0,
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)

def test_successful(self):
"""test successful"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], "bearer")
_, alg = self.provider.get_jwt_key()
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
2 changes: 2 additions & 0 deletions authentik/providers/oauth2/views/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from authentik.providers.oauth2.constants import (
ACR_AUTHENTIK_DEFAULT,
GRANT_TYPE_AUTHORIZATION_CODE,
GRANT_TYPE_CLIENT_CREDENTIALS,
GRANT_TYPE_REFRESH_TOKEN,
SCOPE_OPENID,
)
Expand Down Expand Up @@ -78,6 +79,7 @@ def get_info(self, provider: OAuth2Provider) -> dict[str, Any]:
GRANT_TYPE_AUTHORIZATION_CODE,
GRANT_TYPE_REFRESH_TOKEN,
GrantTypes.IMPLICIT,
GRANT_TYPE_CLIENT_CREDENTIALS,
],
"id_token_signing_alg_values_supported": [supported_alg],
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
Expand Down

0 comments on commit 52e7c8f

Please sign in to comment.