providers/oauth2: pass scope and other parameters to access policy request context

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2641
This commit is contained in:
Jens Langhammer 2022-04-01 21:39:05 +02:00
parent 71c6313c46
commit 4be238018b
5 changed files with 44 additions and 15 deletions

View File

@ -14,7 +14,7 @@ from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
@ -103,11 +103,16 @@ class PolicyAccessView(AccessMixin, View):
response.policy_result = result response.policy_result = result
return response return response
def modify_policy_request(self, request: PolicyRequest) -> PolicyRequest:
"""optionally modify the policy request"""
return request
def user_has_access(self, user: Optional[User] = None) -> PolicyResult: def user_has_access(self, user: Optional[User] = None) -> PolicyResult:
"""Check if user has access to application.""" """Check if user has access to application."""
user = user or self.request.user user = user or self.request.user
policy_engine = PolicyEngine(self.application, user or self.request.user, self.request) policy_engine = PolicyEngine(self.application, user or self.request.user, self.request)
policy_engine.use_cache = False policy_engine.use_cache = False
policy_engine.request = self.modify_policy_request(policy_engine.request)
policy_engine.build() policy_engine.build()
result = policy_engine.result result = policy_engine.result
LOGGER.debug( LOGGER.debug(

View File

@ -27,6 +27,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import PolicyAccessView, RequestValidationError from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
PROMPT_CONSNET, PROMPT_CONSNET,
@ -438,6 +439,16 @@ class AuthorizationFlowInitView(PolicyAccessView):
self.provider = get_object_or_404(OAuth2Provider, client_id=client_id) self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
self.application = self.provider.application self.application = self.provider.application
def modify_policy_request(self, request: PolicyRequest) -> PolicyRequest:
request.context["oauth_scopes"] = self.params.scope
request.context["oauth_grant_type"] = self.params.grant_type
request.context["oauth_code_challenge"] = self.params.code_challenge
request.context["oauth_code_challenge_method"] = self.params.code_challenge_method
request.context["oauth_max_age"] = self.params.max_age
request.context["oauth_redirect_uri"] = self.params.redirect_uri
request.context["oauth_response_type"] = self.params.response_type
return request
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell""" """Start FlowPLanner, return to flow executor shell"""

View File

@ -93,6 +93,18 @@ class TokenParams:
code_verifier=request.POST.get("code_verifier"), code_verifier=request.POST.get("code_verifier"),
) )
def __check_policy_access(self, app: Application, request: HttpRequest, **kwargs):
engine = PolicyEngine(app, self.user, request)
engine.request.context["oauth_scopes"] = self.scope
engine.request.context["oauth_grant_type"] = self.grant_type
engine.request.context["oauth_code_verifier"] = self.code_verifier
engine.request.context.update(kwargs)
engine.build()
result = engine.result
if not result.passing:
LOGGER.info("User not authenticated for application", user=self.user, app=app)
raise TokenError("invalid_grant")
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest): def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
if self.grant_type in [GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN]: if self.grant_type in [GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN]:
if ( if (
@ -233,12 +245,7 @@ class TokenParams:
app = Application.objects.filter(provider=self.provider).first() app = Application.objects.filter(provider=self.provider).first()
if not app or not app.provider: if not app or not app.provider:
raise TokenError("invalid_grant") raise TokenError("invalid_grant")
engine = PolicyEngine(app, self.user, request) self.__check_policy_access(app, request)
engine.build()
result = engine.result
if not result.passing:
LOGGER.info("User not authenticated for application", user=self.user, app=app)
raise TokenError("invalid_grant")
return None return None
def __post_init_client_credentials_jwt(self, request: HttpRequest): def __post_init_client_credentials_jwt(self, request: HttpRequest):
@ -279,13 +286,8 @@ class TokenParams:
LOGGER.info("client_credentials grant for provider without application") LOGGER.info("client_credentials grant for provider without application")
raise TokenError("invalid_grant") raise TokenError("invalid_grant")
engine = PolicyEngine(app, self.user, request) self.__check_policy_access(app, request, oauth_jwt=token)
engine.request.context["JWT"] = token
engine.build()
result = engine.result
if not result.passing:
LOGGER.info("JWT not authenticated for application", app=app)
raise TokenError("invalid_grant")
self.user, _ = User.objects.update_or_create( self.user, _ = User.objects.update_or_create(
username=f"{self.provider.name}-{token.get('sub')}", username=f"{self.provider.name}-{token.get('sub')}",
defaults={ defaults={

View File

@ -51,5 +51,5 @@ Input JWTs are checked to be signed by any of the selected *Verification certifi
To do additional checks, you can use *[Expression policies](../../policies/expression)*: To do additional checks, you can use *[Expression policies](../../policies/expression)*:
```python ```python
return request.context["JWT"]["iss"] == "https://my.issuer" return request.context["oauth_jwt"]["iss"] == "https://my.issuer"
``` ```

View File

@ -43,3 +43,14 @@ Refresh tokens can be used as long-lived tokens to access user data, and further
### `client_credentials`: ### `client_credentials`:
See [Machine-to-machine authentication](./client_credentials) See [Machine-to-machine authentication](./client_credentials)
## Scope authorization
By default, every user that has access to an application can request any of the configured scopes. Starting with authentik 2022.4, you can do additional checks for the scope in an expression policy (bound to the application):
```python
# There are additional fields set in the context, use `ak_logger.debug(request.context)` to see them.
if "my-admin-scope" in request.context["oauth_scopes"]:
return ak_is_group_member(request.user, name="my-admin-group")
return True
```