Support RFC7636 PKCE in the OAuth 2.0 flow. (#14750)
PKCE can protect against certain attacks and is enabled by default. Support can be controlled manually by setting the pkce_method of each oidc_providers entry to 'auto' (default), 'always', or 'never'. This is required by Twitter OAuth 2.0 support.pull/14786/head
parent
747f8eb231
commit
630d0aeaf6
|
@ -0,0 +1 @@
|
||||||
|
Support [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636) Proof Key for Code Exchange for OAuth single sign-on.
|
|
@ -3053,8 +3053,13 @@ Options for each entry include:
|
||||||
values are `client_secret_basic` (default), `client_secret_post` and
|
values are `client_secret_basic` (default), `client_secret_post` and
|
||||||
`none`.
|
`none`.
|
||||||
|
|
||||||
|
* `pkce_method`: Whether to use proof key for code exchange when requesting
|
||||||
|
and exchanging the token. Valid values are: `auto`, `always`, or `never`. Defaults
|
||||||
|
to `auto`, which uses PKCE if supported during metadata discovery. Set to `always`
|
||||||
|
to force enable PKCE or `never` to force disable PKCE.
|
||||||
|
|
||||||
* `scopes`: list of scopes to request. This should normally include the "openid"
|
* `scopes`: list of scopes to request. This should normally include the "openid"
|
||||||
scope. Defaults to ["openid"].
|
scope. Defaults to `["openid"]`.
|
||||||
|
|
||||||
* `authorization_endpoint`: the oauth2 authorization endpoint. Required if
|
* `authorization_endpoint`: the oauth2 authorization endpoint. Required if
|
||||||
provider discovery is disabled.
|
provider discovery is disabled.
|
||||||
|
|
|
@ -117,6 +117,7 @@ OIDC_PROVIDER_CONFIG_SCHEMA = {
|
||||||
# to avoid importing authlib here.
|
# to avoid importing authlib here.
|
||||||
"enum": ["client_secret_basic", "client_secret_post", "none"],
|
"enum": ["client_secret_basic", "client_secret_post", "none"],
|
||||||
},
|
},
|
||||||
|
"pkce_method": {"type": "string", "enum": ["auto", "always", "never"]},
|
||||||
"scopes": {"type": "array", "items": {"type": "string"}},
|
"scopes": {"type": "array", "items": {"type": "string"}},
|
||||||
"authorization_endpoint": {"type": "string"},
|
"authorization_endpoint": {"type": "string"},
|
||||||
"token_endpoint": {"type": "string"},
|
"token_endpoint": {"type": "string"},
|
||||||
|
@ -289,6 +290,7 @@ def _parse_oidc_config_dict(
|
||||||
client_secret=oidc_config.get("client_secret"),
|
client_secret=oidc_config.get("client_secret"),
|
||||||
client_secret_jwt_key=client_secret_jwt_key,
|
client_secret_jwt_key=client_secret_jwt_key,
|
||||||
client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"),
|
client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"),
|
||||||
|
pkce_method=oidc_config.get("pkce_method", "auto"),
|
||||||
scopes=oidc_config.get("scopes", ["openid"]),
|
scopes=oidc_config.get("scopes", ["openid"]),
|
||||||
authorization_endpoint=oidc_config.get("authorization_endpoint"),
|
authorization_endpoint=oidc_config.get("authorization_endpoint"),
|
||||||
token_endpoint=oidc_config.get("token_endpoint"),
|
token_endpoint=oidc_config.get("token_endpoint"),
|
||||||
|
@ -357,6 +359,10 @@ class OidcProviderConfig:
|
||||||
# 'none'.
|
# 'none'.
|
||||||
client_auth_method: str
|
client_auth_method: str
|
||||||
|
|
||||||
|
# Whether to enable PKCE when exchanging the authorization & token.
|
||||||
|
# Valid values are 'auto', 'always', and 'never'.
|
||||||
|
pkce_method: str
|
||||||
|
|
||||||
# list of scopes to request
|
# list of scopes to request
|
||||||
scopes: Collection[str]
|
scopes: Collection[str]
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ from authlib.jose import JsonWebToken, JWTClaims
|
||||||
from authlib.jose.errors import InvalidClaimError, JoseError, MissingClaimError
|
from authlib.jose.errors import InvalidClaimError, JoseError, MissingClaimError
|
||||||
from authlib.oauth2.auth import ClientAuth
|
from authlib.oauth2.auth import ClientAuth
|
||||||
from authlib.oauth2.rfc6749.parameters import prepare_grant_uri
|
from authlib.oauth2.rfc6749.parameters import prepare_grant_uri
|
||||||
|
from authlib.oauth2.rfc7636.challenge import create_s256_code_challenge
|
||||||
from authlib.oidc.core import CodeIDToken, UserInfo
|
from authlib.oidc.core import CodeIDToken, UserInfo
|
||||||
from authlib.oidc.discovery import OpenIDProviderMetadata, get_well_known_url
|
from authlib.oidc.discovery import OpenIDProviderMetadata, get_well_known_url
|
||||||
from jinja2 import Environment, Template
|
from jinja2 import Environment, Template
|
||||||
|
@ -475,6 +476,16 @@ class OidcProvider:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If PKCE support is advertised ensure the wanted method is available.
|
||||||
|
if m.get("code_challenge_methods_supported") is not None:
|
||||||
|
m.validate_code_challenge_methods_supported()
|
||||||
|
if "S256" not in m["code_challenge_methods_supported"]:
|
||||||
|
raise ValueError(
|
||||||
|
'"S256" not in "code_challenge_methods_supported" ({supported!r})'.format(
|
||||||
|
supported=m["code_challenge_methods_supported"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if m.get("response_types_supported") is not None:
|
if m.get("response_types_supported") is not None:
|
||||||
m.validate_response_types_supported()
|
m.validate_response_types_supported()
|
||||||
|
|
||||||
|
@ -602,6 +613,11 @@ class OidcProvider:
|
||||||
if self._config.jwks_uri:
|
if self._config.jwks_uri:
|
||||||
metadata["jwks_uri"] = self._config.jwks_uri
|
metadata["jwks_uri"] = self._config.jwks_uri
|
||||||
|
|
||||||
|
if self._config.pkce_method == "always":
|
||||||
|
metadata["code_challenge_methods_supported"] = ["S256"]
|
||||||
|
elif self._config.pkce_method == "never":
|
||||||
|
metadata.pop("code_challenge_methods_supported", None)
|
||||||
|
|
||||||
self._validate_metadata(metadata)
|
self._validate_metadata(metadata)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
@ -653,7 +669,7 @@ class OidcProvider:
|
||||||
|
|
||||||
return jwk_set
|
return jwk_set
|
||||||
|
|
||||||
async def _exchange_code(self, code: str) -> Token:
|
async def _exchange_code(self, code: str, code_verifier: str) -> Token:
|
||||||
"""Exchange an authorization code for a token.
|
"""Exchange an authorization code for a token.
|
||||||
|
|
||||||
This calls the ``token_endpoint`` with the authorization code we
|
This calls the ``token_endpoint`` with the authorization code we
|
||||||
|
@ -666,6 +682,7 @@ class OidcProvider:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
code: The authorization code we got from the callback.
|
code: The authorization code we got from the callback.
|
||||||
|
code_verifier: The PKCE code verifier to send, blank if unused.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dict containing various tokens.
|
A dict containing various tokens.
|
||||||
|
@ -696,6 +713,8 @@ class OidcProvider:
|
||||||
"code": code,
|
"code": code,
|
||||||
"redirect_uri": self._callback_url,
|
"redirect_uri": self._callback_url,
|
||||||
}
|
}
|
||||||
|
if code_verifier:
|
||||||
|
args["code_verifier"] = code_verifier
|
||||||
body = urlencode(args, True)
|
body = urlencode(args, True)
|
||||||
|
|
||||||
# Fill the body/headers with credentials
|
# Fill the body/headers with credentials
|
||||||
|
@ -914,11 +933,14 @@ class OidcProvider:
|
||||||
- ``scope``: the list of scopes set in ``oidc_config.scopes``
|
- ``scope``: the list of scopes set in ``oidc_config.scopes``
|
||||||
- ``state``: a random string
|
- ``state``: a random string
|
||||||
- ``nonce``: a random string
|
- ``nonce``: a random string
|
||||||
|
- ``code_challenge``: a RFC7636 code challenge (if PKCE is supported)
|
||||||
|
|
||||||
In addition generating a redirect URL, we are setting a cookie with
|
In addition to generating a redirect URL, we are setting a cookie with
|
||||||
a signed macaroon token containing the state, the nonce and the
|
a signed macaroon token containing the state, the nonce, the
|
||||||
client_redirect_url params. Those are then checked when the client
|
client_redirect_url, and (optionally) the code_verifier params. The state,
|
||||||
comes back from the provider.
|
nonce, and client_redirect_url are then checked when the client comes back
|
||||||
|
from the provider. The code_verifier is passed back to the server during
|
||||||
|
the token exchange and compared to the code_challenge sent in this request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: the incoming request from the browser.
|
request: the incoming request from the browser.
|
||||||
|
@ -935,10 +957,25 @@ class OidcProvider:
|
||||||
|
|
||||||
state = generate_token()
|
state = generate_token()
|
||||||
nonce = generate_token()
|
nonce = generate_token()
|
||||||
|
code_verifier = ""
|
||||||
|
|
||||||
if not client_redirect_url:
|
if not client_redirect_url:
|
||||||
client_redirect_url = b""
|
client_redirect_url = b""
|
||||||
|
|
||||||
|
metadata = await self.load_metadata()
|
||||||
|
|
||||||
|
# Automatically enable PKCE if it is supported.
|
||||||
|
extra_grant_values = {}
|
||||||
|
if metadata.get("code_challenge_methods_supported"):
|
||||||
|
code_verifier = generate_token(48)
|
||||||
|
|
||||||
|
# Note that we verified the server supports S256 earlier (in
|
||||||
|
# OidcProvider._validate_metadata).
|
||||||
|
extra_grant_values = {
|
||||||
|
"code_challenge_method": "S256",
|
||||||
|
"code_challenge": create_s256_code_challenge(code_verifier),
|
||||||
|
}
|
||||||
|
|
||||||
cookie = self._macaroon_generaton.generate_oidc_session_token(
|
cookie = self._macaroon_generaton.generate_oidc_session_token(
|
||||||
state=state,
|
state=state,
|
||||||
session_data=OidcSessionData(
|
session_data=OidcSessionData(
|
||||||
|
@ -946,6 +983,7 @@ class OidcProvider:
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
client_redirect_url=client_redirect_url.decode(),
|
client_redirect_url=client_redirect_url.decode(),
|
||||||
ui_auth_session_id=ui_auth_session_id or "",
|
ui_auth_session_id=ui_auth_session_id or "",
|
||||||
|
code_verifier=code_verifier,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -966,7 +1004,6 @@ class OidcProvider:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
metadata = await self.load_metadata()
|
|
||||||
authorization_endpoint = metadata.get("authorization_endpoint")
|
authorization_endpoint = metadata.get("authorization_endpoint")
|
||||||
return prepare_grant_uri(
|
return prepare_grant_uri(
|
||||||
authorization_endpoint,
|
authorization_endpoint,
|
||||||
|
@ -976,6 +1013,7 @@ class OidcProvider:
|
||||||
scope=self._scopes,
|
scope=self._scopes,
|
||||||
state=state,
|
state=state,
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
|
**extra_grant_values,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_oidc_callback(
|
async def handle_oidc_callback(
|
||||||
|
@ -1003,7 +1041,9 @@ class OidcProvider:
|
||||||
# Exchange the code with the provider
|
# Exchange the code with the provider
|
||||||
try:
|
try:
|
||||||
logger.debug("Exchanging OAuth2 code for a token")
|
logger.debug("Exchanging OAuth2 code for a token")
|
||||||
token = await self._exchange_code(code)
|
token = await self._exchange_code(
|
||||||
|
code, code_verifier=session_data.code_verifier
|
||||||
|
)
|
||||||
except OidcError as e:
|
except OidcError as e:
|
||||||
logger.warning("Could not exchange OAuth2 code: %s", e)
|
logger.warning("Could not exchange OAuth2 code: %s", e)
|
||||||
self._sso_handler.render_error(request, e.error, e.error_description)
|
self._sso_handler.render_error(request, e.error, e.error_description)
|
||||||
|
|
|
@ -110,6 +110,9 @@ class OidcSessionData:
|
||||||
ui_auth_session_id: str
|
ui_auth_session_id: str
|
||||||
"""The session ID of the ongoing UI Auth ("" if this is a login)"""
|
"""The session ID of the ongoing UI Auth ("" if this is a login)"""
|
||||||
|
|
||||||
|
code_verifier: str
|
||||||
|
"""The random string used in the RFC7636 code challenge ("" if PKCE is not being used)."""
|
||||||
|
|
||||||
|
|
||||||
class MacaroonGenerator:
|
class MacaroonGenerator:
|
||||||
def __init__(self, clock: Clock, location: str, secret_key: bytes):
|
def __init__(self, clock: Clock, location: str, secret_key: bytes):
|
||||||
|
@ -187,6 +190,7 @@ class MacaroonGenerator:
|
||||||
macaroon.add_first_party_caveat(
|
macaroon.add_first_party_caveat(
|
||||||
f"ui_auth_session_id = {session_data.ui_auth_session_id}"
|
f"ui_auth_session_id = {session_data.ui_auth_session_id}"
|
||||||
)
|
)
|
||||||
|
macaroon.add_first_party_caveat(f"code_verifier = {session_data.code_verifier}")
|
||||||
macaroon.add_first_party_caveat(f"time < {expiry}")
|
macaroon.add_first_party_caveat(f"time < {expiry}")
|
||||||
|
|
||||||
return macaroon.serialize()
|
return macaroon.serialize()
|
||||||
|
@ -278,6 +282,7 @@ class MacaroonGenerator:
|
||||||
v.satisfy_general(lambda c: c.startswith("idp_id = "))
|
v.satisfy_general(lambda c: c.startswith("idp_id = "))
|
||||||
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
|
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
|
||||||
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
|
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
|
||||||
|
v.satisfy_general(lambda c: c.startswith("code_verifier = "))
|
||||||
satisfy_expiry(v, self._clock.time_msec)
|
satisfy_expiry(v, self._clock.time_msec)
|
||||||
|
|
||||||
v.verify(macaroon, self._secret_key)
|
v.verify(macaroon, self._secret_key)
|
||||||
|
@ -287,11 +292,13 @@ class MacaroonGenerator:
|
||||||
idp_id = get_value_from_macaroon(macaroon, "idp_id")
|
idp_id = get_value_from_macaroon(macaroon, "idp_id")
|
||||||
client_redirect_url = get_value_from_macaroon(macaroon, "client_redirect_url")
|
client_redirect_url = get_value_from_macaroon(macaroon, "client_redirect_url")
|
||||||
ui_auth_session_id = get_value_from_macaroon(macaroon, "ui_auth_session_id")
|
ui_auth_session_id = get_value_from_macaroon(macaroon, "ui_auth_session_id")
|
||||||
|
code_verifier = get_value_from_macaroon(macaroon, "code_verifier")
|
||||||
return OidcSessionData(
|
return OidcSessionData(
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
idp_id=idp_id,
|
idp_id=idp_id,
|
||||||
client_redirect_url=client_redirect_url,
|
client_redirect_url=client_redirect_url,
|
||||||
ui_auth_session_id=ui_auth_session_id,
|
ui_auth_session_id=ui_auth_session_id,
|
||||||
|
code_verifier=code_verifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate_base_macaroon(self, type: MacaroonType) -> pymacaroons.Macaroon:
|
def _generate_base_macaroon(self, type: MacaroonType) -> pymacaroons.Macaroon:
|
||||||
|
|
|
@ -396,6 +396,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
self.assertEqual(params["client_id"], [CLIENT_ID])
|
self.assertEqual(params["client_id"], [CLIENT_ID])
|
||||||
self.assertEqual(len(params["state"]), 1)
|
self.assertEqual(len(params["state"]), 1)
|
||||||
self.assertEqual(len(params["nonce"]), 1)
|
self.assertEqual(len(params["nonce"]), 1)
|
||||||
|
self.assertNotIn("code_challenge", params)
|
||||||
|
|
||||||
# Check what is in the cookies
|
# Check what is in the cookies
|
||||||
self.assertEqual(len(req.cookies), 2) # two cookies
|
self.assertEqual(len(req.cookies), 2) # two cookies
|
||||||
|
@ -411,12 +412,117 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
macaroon = pymacaroons.Macaroon.deserialize(cookie)
|
macaroon = pymacaroons.Macaroon.deserialize(cookie)
|
||||||
state = get_value_from_macaroon(macaroon, "state")
|
state = get_value_from_macaroon(macaroon, "state")
|
||||||
nonce = get_value_from_macaroon(macaroon, "nonce")
|
nonce = get_value_from_macaroon(macaroon, "nonce")
|
||||||
|
code_verifier = get_value_from_macaroon(macaroon, "code_verifier")
|
||||||
redirect = get_value_from_macaroon(macaroon, "client_redirect_url")
|
redirect = get_value_from_macaroon(macaroon, "client_redirect_url")
|
||||||
|
|
||||||
self.assertEqual(params["state"], [state])
|
self.assertEqual(params["state"], [state])
|
||||||
self.assertEqual(params["nonce"], [nonce])
|
self.assertEqual(params["nonce"], [nonce])
|
||||||
|
self.assertEqual(code_verifier, "")
|
||||||
self.assertEqual(redirect, "http://client/redirect")
|
self.assertEqual(redirect, "http://client/redirect")
|
||||||
|
|
||||||
|
@override_config({"oidc_config": DEFAULT_CONFIG})
|
||||||
|
def test_redirect_request_with_code_challenge(self) -> None:
|
||||||
|
"""The redirect request has the right arguments & generates a valid session cookie."""
|
||||||
|
req = Mock(spec=["cookies"])
|
||||||
|
req.cookies = []
|
||||||
|
|
||||||
|
with self.metadata_edit({"code_challenge_methods_supported": ["S256"]}):
|
||||||
|
url = urlparse(
|
||||||
|
self.get_success(
|
||||||
|
self.provider.handle_redirect_request(
|
||||||
|
req, b"http://client/redirect"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the code_challenge param is added to the redirect.
|
||||||
|
params = parse_qs(url.query)
|
||||||
|
self.assertEqual(len(params["code_challenge"]), 1)
|
||||||
|
|
||||||
|
# Check what is in the cookies
|
||||||
|
self.assertEqual(len(req.cookies), 2) # two cookies
|
||||||
|
cookie_header = req.cookies[0]
|
||||||
|
|
||||||
|
# The cookie name and path don't really matter, just that it has to be coherent
|
||||||
|
# between the callback & redirect handlers.
|
||||||
|
parts = [p.strip() for p in cookie_header.split(b";")]
|
||||||
|
self.assertIn(b"Path=/_synapse/client/oidc", parts)
|
||||||
|
name, cookie = parts[0].split(b"=")
|
||||||
|
self.assertEqual(name, b"oidc_session")
|
||||||
|
|
||||||
|
# Ensure the code_verifier is set in the cookie.
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(cookie)
|
||||||
|
code_verifier = get_value_from_macaroon(macaroon, "code_verifier")
|
||||||
|
self.assertNotEqual(code_verifier, "")
|
||||||
|
|
||||||
|
@override_config({"oidc_config": {**DEFAULT_CONFIG, "pkce_method": "always"}})
|
||||||
|
def test_redirect_request_with_forced_code_challenge(self) -> None:
|
||||||
|
"""The redirect request has the right arguments & generates a valid session cookie."""
|
||||||
|
req = Mock(spec=["cookies"])
|
||||||
|
req.cookies = []
|
||||||
|
|
||||||
|
url = urlparse(
|
||||||
|
self.get_success(
|
||||||
|
self.provider.handle_redirect_request(req, b"http://client/redirect")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the code_challenge param is added to the redirect.
|
||||||
|
params = parse_qs(url.query)
|
||||||
|
self.assertEqual(len(params["code_challenge"]), 1)
|
||||||
|
|
||||||
|
# Check what is in the cookies
|
||||||
|
self.assertEqual(len(req.cookies), 2) # two cookies
|
||||||
|
cookie_header = req.cookies[0]
|
||||||
|
|
||||||
|
# The cookie name and path don't really matter, just that it has to be coherent
|
||||||
|
# between the callback & redirect handlers.
|
||||||
|
parts = [p.strip() for p in cookie_header.split(b";")]
|
||||||
|
self.assertIn(b"Path=/_synapse/client/oidc", parts)
|
||||||
|
name, cookie = parts[0].split(b"=")
|
||||||
|
self.assertEqual(name, b"oidc_session")
|
||||||
|
|
||||||
|
# Ensure the code_verifier is set in the cookie.
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(cookie)
|
||||||
|
code_verifier = get_value_from_macaroon(macaroon, "code_verifier")
|
||||||
|
self.assertNotEqual(code_verifier, "")
|
||||||
|
|
||||||
|
@override_config({"oidc_config": {**DEFAULT_CONFIG, "pkce_method": "never"}})
|
||||||
|
def test_redirect_request_with_disabled_code_challenge(self) -> None:
|
||||||
|
"""The redirect request has the right arguments & generates a valid session cookie."""
|
||||||
|
req = Mock(spec=["cookies"])
|
||||||
|
req.cookies = []
|
||||||
|
|
||||||
|
# The metadata should state that PKCE is enabled.
|
||||||
|
with self.metadata_edit({"code_challenge_methods_supported": ["S256"]}):
|
||||||
|
url = urlparse(
|
||||||
|
self.get_success(
|
||||||
|
self.provider.handle_redirect_request(
|
||||||
|
req, b"http://client/redirect"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the code_challenge param is added to the redirect.
|
||||||
|
params = parse_qs(url.query)
|
||||||
|
self.assertNotIn("code_challenge", params)
|
||||||
|
|
||||||
|
# Check what is in the cookies
|
||||||
|
self.assertEqual(len(req.cookies), 2) # two cookies
|
||||||
|
cookie_header = req.cookies[0]
|
||||||
|
|
||||||
|
# The cookie name and path don't really matter, just that it has to be coherent
|
||||||
|
# between the callback & redirect handlers.
|
||||||
|
parts = [p.strip() for p in cookie_header.split(b";")]
|
||||||
|
self.assertIn(b"Path=/_synapse/client/oidc", parts)
|
||||||
|
name, cookie = parts[0].split(b"=")
|
||||||
|
self.assertEqual(name, b"oidc_session")
|
||||||
|
|
||||||
|
# Ensure the code_verifier is blank in the cookie.
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(cookie)
|
||||||
|
code_verifier = get_value_from_macaroon(macaroon, "code_verifier")
|
||||||
|
self.assertEqual(code_verifier, "")
|
||||||
|
|
||||||
@override_config({"oidc_config": DEFAULT_CONFIG})
|
@override_config({"oidc_config": DEFAULT_CONFIG})
|
||||||
def test_callback_error(self) -> None:
|
def test_callback_error(self) -> None:
|
||||||
"""Errors from the provider returned in the callback are displayed."""
|
"""Errors from the provider returned in the callback are displayed."""
|
||||||
|
@ -601,7 +707,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
payload=token
|
payload=token
|
||||||
)
|
)
|
||||||
code = "code"
|
code = "code"
|
||||||
ret = self.get_success(self.provider._exchange_code(code))
|
ret = self.get_success(self.provider._exchange_code(code, code_verifier=""))
|
||||||
kwargs = self.fake_server.request.call_args[1]
|
kwargs = self.fake_server.request.call_args[1]
|
||||||
|
|
||||||
self.assertEqual(ret, token)
|
self.assertEqual(ret, token)
|
||||||
|
@ -615,13 +721,34 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
self.assertEqual(args["client_secret"], [CLIENT_SECRET])
|
self.assertEqual(args["client_secret"], [CLIENT_SECRET])
|
||||||
self.assertEqual(args["redirect_uri"], [CALLBACK_URL])
|
self.assertEqual(args["redirect_uri"], [CALLBACK_URL])
|
||||||
|
|
||||||
|
# Test providing a code verifier.
|
||||||
|
code_verifier = "code_verifier"
|
||||||
|
ret = self.get_success(
|
||||||
|
self.provider._exchange_code(code, code_verifier=code_verifier)
|
||||||
|
)
|
||||||
|
kwargs = self.fake_server.request.call_args[1]
|
||||||
|
|
||||||
|
self.assertEqual(ret, token)
|
||||||
|
self.assertEqual(kwargs["method"], "POST")
|
||||||
|
self.assertEqual(kwargs["uri"], self.fake_server.token_endpoint)
|
||||||
|
|
||||||
|
args = parse_qs(kwargs["data"].decode("utf-8"))
|
||||||
|
self.assertEqual(args["grant_type"], ["authorization_code"])
|
||||||
|
self.assertEqual(args["code"], [code])
|
||||||
|
self.assertEqual(args["client_id"], [CLIENT_ID])
|
||||||
|
self.assertEqual(args["client_secret"], [CLIENT_SECRET])
|
||||||
|
self.assertEqual(args["redirect_uri"], [CALLBACK_URL])
|
||||||
|
self.assertEqual(args["code_verifier"], [code_verifier])
|
||||||
|
|
||||||
# Test error handling
|
# Test error handling
|
||||||
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
||||||
code=400, payload={"error": "foo", "error_description": "bar"}
|
code=400, payload={"error": "foo", "error_description": "bar"}
|
||||||
)
|
)
|
||||||
from synapse.handlers.oidc import OidcError
|
from synapse.handlers.oidc import OidcError
|
||||||
|
|
||||||
exc = self.get_failure(self.provider._exchange_code(code), OidcError)
|
exc = self.get_failure(
|
||||||
|
self.provider._exchange_code(code, code_verifier=""), OidcError
|
||||||
|
)
|
||||||
self.assertEqual(exc.value.error, "foo")
|
self.assertEqual(exc.value.error, "foo")
|
||||||
self.assertEqual(exc.value.error_description, "bar")
|
self.assertEqual(exc.value.error_description, "bar")
|
||||||
|
|
||||||
|
@ -629,7 +756,9 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
self.fake_server.post_token_handler.return_value = FakeResponse(
|
self.fake_server.post_token_handler.return_value = FakeResponse(
|
||||||
code=500, body=b"Not JSON"
|
code=500, body=b"Not JSON"
|
||||||
)
|
)
|
||||||
exc = self.get_failure(self.provider._exchange_code(code), OidcError)
|
exc = self.get_failure(
|
||||||
|
self.provider._exchange_code(code, code_verifier=""), OidcError
|
||||||
|
)
|
||||||
self.assertEqual(exc.value.error, "server_error")
|
self.assertEqual(exc.value.error, "server_error")
|
||||||
|
|
||||||
# Internal server error with JSON body
|
# Internal server error with JSON body
|
||||||
|
@ -637,21 +766,27 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
code=500, payload={"error": "internal_server_error"}
|
code=500, payload={"error": "internal_server_error"}
|
||||||
)
|
)
|
||||||
|
|
||||||
exc = self.get_failure(self.provider._exchange_code(code), OidcError)
|
exc = self.get_failure(
|
||||||
|
self.provider._exchange_code(code, code_verifier=""), OidcError
|
||||||
|
)
|
||||||
self.assertEqual(exc.value.error, "internal_server_error")
|
self.assertEqual(exc.value.error, "internal_server_error")
|
||||||
|
|
||||||
# 4xx error without "error" field
|
# 4xx error without "error" field
|
||||||
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
||||||
code=400, payload={}
|
code=400, payload={}
|
||||||
)
|
)
|
||||||
exc = self.get_failure(self.provider._exchange_code(code), OidcError)
|
exc = self.get_failure(
|
||||||
|
self.provider._exchange_code(code, code_verifier=""), OidcError
|
||||||
|
)
|
||||||
self.assertEqual(exc.value.error, "server_error")
|
self.assertEqual(exc.value.error, "server_error")
|
||||||
|
|
||||||
# 2xx error with "error" field
|
# 2xx error with "error" field
|
||||||
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
self.fake_server.post_token_handler.return_value = FakeResponse.json(
|
||||||
code=200, payload={"error": "some_error"}
|
code=200, payload={"error": "some_error"}
|
||||||
)
|
)
|
||||||
exc = self.get_failure(self.provider._exchange_code(code), OidcError)
|
exc = self.get_failure(
|
||||||
|
self.provider._exchange_code(code, code_verifier=""), OidcError
|
||||||
|
)
|
||||||
self.assertEqual(exc.value.error, "some_error")
|
self.assertEqual(exc.value.error, "some_error")
|
||||||
|
|
||||||
@override_config(
|
@override_config(
|
||||||
|
@ -688,7 +823,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
# timestamps.
|
# timestamps.
|
||||||
self.reactor.advance(1000)
|
self.reactor.advance(1000)
|
||||||
start_time = self.reactor.seconds()
|
start_time = self.reactor.seconds()
|
||||||
ret = self.get_success(self.provider._exchange_code(code))
|
ret = self.get_success(self.provider._exchange_code(code, code_verifier=""))
|
||||||
|
|
||||||
self.assertEqual(ret, token)
|
self.assertEqual(ret, token)
|
||||||
|
|
||||||
|
@ -739,7 +874,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
payload=token
|
payload=token
|
||||||
)
|
)
|
||||||
code = "code"
|
code = "code"
|
||||||
ret = self.get_success(self.provider._exchange_code(code))
|
ret = self.get_success(self.provider._exchange_code(code, code_verifier=""))
|
||||||
|
|
||||||
self.assertEqual(ret, token)
|
self.assertEqual(ret, token)
|
||||||
|
|
||||||
|
@ -1203,6 +1338,7 @@ class OidcHandlerTestCase(HomeserverTestCase):
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
client_redirect_url=client_redirect_url,
|
client_redirect_url=client_redirect_url,
|
||||||
ui_auth_session_id=ui_auth_session_id,
|
ui_auth_session_id=ui_auth_session_id,
|
||||||
|
code_verifier="",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ class MacaroonGeneratorTestCase(TestCase):
|
||||||
nonce="nonce",
|
nonce="nonce",
|
||||||
client_redirect_url="https://example.com/",
|
client_redirect_url="https://example.com/",
|
||||||
ui_auth_session_id="",
|
ui_auth_session_id="",
|
||||||
|
code_verifier="",
|
||||||
)
|
)
|
||||||
token = self.macaroon_generator.generate_oidc_session_token(
|
token = self.macaroon_generator.generate_oidc_session_token(
|
||||||
state, session_data, duration_in_ms=2 * 60 * 1000
|
state, session_data, duration_in_ms=2 * 60 * 1000
|
||||||
|
|
Loading…
Reference in New Issue