forked from CGM_Public/pretix_original
OIDC: Implement PKCE in OP and RP
This commit is contained in:
@@ -676,6 +676,8 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
popup_origin = None
|
||||
|
||||
nonce = get_random_string(32)
|
||||
pkce_code_verifier = get_random_string(64)
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'] = pkce_code_verifier
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
|
||||
@@ -684,7 +686,7 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
})
|
||||
|
||||
if self.provider.method == "oidc":
|
||||
return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri))
|
||||
return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri, pkce_code_verifier))
|
||||
else:
|
||||
raise Http404("Unknown SSO method.")
|
||||
|
||||
@@ -718,6 +720,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
r = super().dispatch(request, *args, **kwargs)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_nonce', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_popup_origin', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested', None)
|
||||
@@ -763,6 +766,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
self.provider,
|
||||
request.GET.get('code'),
|
||||
redirect_uri,
|
||||
request.session.get(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'),
|
||||
)
|
||||
except ValidationError as e:
|
||||
for msg in e:
|
||||
|
||||
@@ -72,6 +72,9 @@ We currently do not implement the following optional parts of the spec:
|
||||
We also implement the Discovery extension (without issuer discovery)
|
||||
as per https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
|
||||
We also implement the PKCE extension for OAuth:
|
||||
https://www.rfc-editor.org/rfc/rfc7636
|
||||
|
||||
The implementation passed the certification tests against the following profiles, but we did not
|
||||
acquire formal certification:
|
||||
|
||||
@@ -136,19 +139,22 @@ class AuthorizeView(View):
|
||||
self._construct_redirect_uri(redirect_uri, response_mode, qs)
|
||||
)
|
||||
|
||||
def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce):
|
||||
def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method):
|
||||
form = AuthenticationForm(data=request.POST if "login-email" in request.POST else None, request=request,
|
||||
prefix="login")
|
||||
if "login-email" in request.POST and form.is_valid():
|
||||
customer_login(request, form.get_customer())
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, form.get_customer())
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method, form.get_customer())
|
||||
else:
|
||||
return render(request, 'pretixpresale/organizers/customer_login.html', {
|
||||
'providers': [],
|
||||
'form': form,
|
||||
})
|
||||
|
||||
def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, customer):
|
||||
def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
|
||||
code_challenge_method, customer):
|
||||
response_type = response_type.split(' ')
|
||||
qs = {}
|
||||
id_token_kwargs = {}
|
||||
@@ -162,6 +168,8 @@ class AuthorizeView(View):
|
||||
expires=now() + timedelta(minutes=10),
|
||||
auth_time=get_customer_auth_time(self.request),
|
||||
nonce=nonce,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method,
|
||||
)
|
||||
qs['code'] = grant.code
|
||||
id_token_kwargs['with_code'] = grant.code
|
||||
@@ -209,6 +217,8 @@ class AuthorizeView(View):
|
||||
prompt = request_data.get("prompt")
|
||||
response_type = request_data.get("response_type")
|
||||
scope = request_data.get("scope", "").split(" ")
|
||||
code_challenge = request_data.get("code_challenge")
|
||||
code_challenge_method = request_data.get("code_challenge_method")
|
||||
|
||||
if not client_id:
|
||||
return self._final_error("invalid_request", "client_id missing")
|
||||
@@ -247,6 +257,16 @@ class AuthorizeView(View):
|
||||
return self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
if code_challenge and code_challenge_method != "S256":
|
||||
# "Clients re permitted to use "plain" only if they cannot support "S256" for some technical reason and
|
||||
# know via out-of-band configuration that the S256 MUST be implemented, plain is not mandatory."
|
||||
return self._redirect_error("invalid_request", "code_challenge transform algorithm not supported",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
if client.require_pkce and not code_challenge:
|
||||
return self._redirect_error("invalid_request", "code_challenge (PKCE) required",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
has_valid_session = bool(request.customer)
|
||||
if has_valid_session and max_age:
|
||||
try:
|
||||
@@ -262,9 +282,11 @@ class AuthorizeView(View):
|
||||
has_valid_session = False
|
||||
|
||||
if has_valid_session:
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, request.customer)
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
|
||||
code_challenge_method, request.customer)
|
||||
else:
|
||||
return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce)
|
||||
return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method)
|
||||
|
||||
|
||||
class TokenView(View):
|
||||
@@ -362,6 +384,24 @@ class TokenView(View):
|
||||
"error_description": "Mismatch of redirect_uri"
|
||||
}, status=400)
|
||||
|
||||
if grant.code_challenge:
|
||||
if not request.POST.get("code_verifier"):
|
||||
return JsonResponse({
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Missing of code_verifier"
|
||||
}, status=400)
|
||||
|
||||
if grant.code_challenge_method == "S256":
|
||||
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
|
||||
print(grant.code_challenge, expected_challenge)
|
||||
if expected_challenge != grant.code_challenge:
|
||||
return JsonResponse({
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Mismatch of code_verifier with code_challenge"
|
||||
}, status=400)
|
||||
else:
|
||||
raise ValueError("Unsupported code_challenge_method in database")
|
||||
|
||||
with transaction.atomic():
|
||||
token = self.client.access_tokens.create(
|
||||
customer=grant.customer,
|
||||
@@ -503,6 +543,7 @@ class ConfigurationView(View):
|
||||
'token_endpoint_auth_methods_supported': [
|
||||
'client_secret_post', 'client_secret_basic'
|
||||
],
|
||||
'code_challenge_methods_supported': ['S256'],
|
||||
'claims_supported': [
|
||||
'iss',
|
||||
'aud',
|
||||
|
||||
Reference in New Issue
Block a user