async def test_redeem_code(tmp_path: Path, factory: ComponentFactory) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] config = await configure(tmp_path, "github", oidc_clients=clients) factory.reconfigure(config) oidc_service = factory.create_oidc_service() token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" code = await oidc_service.issue_code("client-2", redirect_uri, token) oidc_token = await oidc_service.redeem_code("client-2", "client-2-secret", redirect_uri, code) assert oidc_token.claims == { "aud": config.issuer.aud, "iat": ANY, "exp": ANY, "iss": config.issuer.iss, "jti": code.key, "name": token_data.name, "preferred_username": token_data.username, "scope": "openid", "sub": token_data.username, config.issuer.username_claim: token_data.username, config.issuer.uid_claim: token_data.uid, } redis = await redis_dependency() assert not await redis.get(f"oidc:{code.key}")
async def test_redeem_code_errors(tmp_path: Path, factory: ComponentFactory) -> None: clients = [ OIDCClient(client_id="client-1", client_secret="client-1-secret"), OIDCClient(client_id="client-2", client_secret="client-2-secret"), ] config = await configure(tmp_path, "github", oidc_clients=clients) factory.reconfigure(config) oidc_service = factory.create_oidc_service() token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" code = await oidc_service.issue_code("client-2", redirect_uri, token) with pytest.raises(InvalidClientError): await oidc_service.redeem_code("some-client", "some-secret", redirect_uri, code) with pytest.raises(InvalidClientError): await oidc_service.redeem_code("client-2", "some-secret", redirect_uri, code) with pytest.raises(InvalidGrantError): await oidc_service.redeem_code( "client-2", "client-2-secret", redirect_uri, OIDCAuthorizationCode(), ) with pytest.raises(InvalidGrantError): await oidc_service.redeem_code("client-1", "client-1-secret", redirect_uri, code) with pytest.raises(InvalidGrantError): await oidc_service.redeem_code("client-2", "client-2-secret", "https://foo.example.com/", code)
async def test_issue_code(tmp_path: Path, factory: ComponentFactory) -> None: clients = [OIDCClient(client_id="some-id", client_secret="some-secret")] config = await configure(tmp_path, "github", oidc_clients=clients) factory.reconfigure(config) oidc_service = factory.create_oidc_service() token_data = await create_session_token(factory) token = token_data.token redirect_uri = "https://example.com/" assert config.oidc_server assert list(config.oidc_server.clients) == clients with pytest.raises(UnauthorizedClientException): await oidc_service.issue_code("unknown-client", redirect_uri, token) code = await oidc_service.issue_code("some-id", redirect_uri, token) redis = await redis_dependency() encrypted_code = await redis.get(f"oidc:{code.key}") assert encrypted_code fernet = Fernet(config.session_secret.encode()) serialized_code = json.loads(fernet.decrypt(encrypted_code)) assert serialized_code == { "code": { "key": code.key, "secret": code.secret, }, "client_id": "some-id", "redirect_uri": redirect_uri, "token": { "key": token.key, "secret": token.secret, }, "created_at": ANY, } now = time.time() assert now - 2 < serialized_code["created_at"] < now
async def test_token_errors( tmp_path: Path, client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture, ) -> None: clients = [ OIDCClient(client_id="some-id", client_secret="some-secret"), OIDCClient(client_id="other-id", client_secret="other-secret"), ] config = await configure(tmp_path, "github", oidc_clients=clients) factory.reconfigure(config) token_data = await create_session_token(factory) token = token_data.token oidc_service = factory.create_oidc_service() redirect_uri = f"https://{TEST_HOSTNAME}/app" code = await oidc_service.issue_code("some-id", redirect_uri, token) # Missing parameters. request: Dict[str, str] = {} caplog.clear() r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_request", "error_description": "Invalid token request", } assert parse_log(caplog) == [{ "error": "Invalid token request", "event": "Invalid request", "httpRequest": { "requestMethod": "POST", "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token", "remoteIp": "127.0.0.1", }, "severity": "warning", }] # Invalid grant type. request = { "grant_type": "bogus", "client_id": "other-client", "code": "nonsense", "redirect_uri": f"https://{TEST_HOSTNAME}/", } caplog.clear() r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "unsupported_grant_type", "error_description": "Invalid grant type bogus", } assert parse_log(caplog) == [{ "error": "Invalid grant type bogus", "event": "Unsupported grant type", "httpRequest": { "requestMethod": "POST", "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token", "remoteIp": "127.0.0.1", }, "severity": "warning", }] # Invalid code. request["grant_type"] = "authorization_code" r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", } # No client_secret. request["code"] = str(OIDCAuthorizationCode()) caplog.clear() r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", "error_description": "No client_secret provided", } assert parse_log(caplog) == [{ "error": "No client_secret provided", "event": "Unauthorized client", "httpRequest": { "requestMethod": "POST", "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token", "remoteIp": "127.0.0.1", }, "severity": "warning", }] # Incorrect client_id. request["client_secret"] = "other-secret" r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", "error_description": "Unknown client ID other-client", } # Incorrect client_secret. request["client_id"] = "some-id" r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_client", "error_description": "Invalid secret for some-id", } # No stored data. request["client_secret"] = "some-secret" bogus_code = OIDCAuthorizationCode() request["code"] = str(bogus_code) caplog.clear() r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", } log = json.loads(caplog.record_tuples[0][2]) assert log["event"] == "Invalid authorization code" assert log["error"] == f"Unknown authorization code {bogus_code.key}" # Corrupt stored data. redis = await redis_dependency() await redis.set(bogus_code.key, "XXXXXXX") r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", } # Correct code, but invalid client_id for that code. bogus_code = await oidc_service.issue_code("other-id", redirect_uri, token) request["code"] = str(bogus_code) r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", } # Correct code and client_id but invalid redirect_uri. request["code"] = str(code) r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", } # Delete the underlying token. token_service = factory.create_token_service() async with factory.session.begin(): await token_service.delete_token(token.key, token_data, token_data.username, ip_address="127.0.0.1") request["redirect_uri"] = redirect_uri r = await client.post("/auth/openid/token", data=request) assert r.status_code == 400 assert r.json() == { "error": "invalid_grant", "error_description": "Invalid authorization code", }