Exemple #1
0
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}")
Exemple #2
0
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)
Exemple #3
0
async def test_verify_oidc(tmp_path: Path, respx_mock: respx.Router,
                           factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    verifier = factory.create_token_verifier()

    now = datetime.now(timezone.utc)
    exp = now + timedelta(days=24)
    payload: Dict[str, Any] = {
        "aud": config.verifier.oidc_aud,
        "iat": int(now.timestamp()),
        "exp": int(exp.timestamp()),
    }
    keypair = config.issuer.keypair
    token = encode_token(payload, keypair)
    excinfo: ExceptionInfo[Exception]

    # Missing iss.
    with pytest.raises(InvalidIssuerError) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "No iss claim in token"

    # Missing kid.
    payload["iss"] = "https://bogus.example.com/"
    token = encode_token(payload, keypair)
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "No kid in token header"

    # Unknown issuer.
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(InvalidIssuerError) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "Unknown issuer: https://bogus.example.com/"

    # Unknown kid.
    payload["iss"] = config.verifier.oidc_iss
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"kid a-kid not allowed for {config.verifier.oidc_iss}"
    assert str(excinfo.value) == expected

    # Missing username claim.
    await mock_oidc_provider_config(respx_mock, keypair)
    kid = config.verifier.oidc_kids[0]
    token = encode_token(payload, config.issuer.keypair, kid=kid)
    with pytest.raises(MissingClaimsException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"No {config.verifier.username_claim} claim in token"
    assert str(excinfo.value) == expected

    # Missing UID claim.
    await mock_oidc_provider_config(respx_mock, keypair)
    payload[config.verifier.username_claim] = "some-user"
    token = encode_token(payload, config.issuer.keypair, kid=kid)
    with pytest.raises(MissingClaimsException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"No {config.verifier.uid_claim} claim in token"
    assert str(excinfo.value) == expected
async def test_not_configured(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    token_data = await create_session_token(factory)

    caplog.clear()
    r = await client.get(
        "/auth/tokens/influxdb/new",
        headers={"Authorization": f"bearer {token_data.token}"},
    )

    assert r.status_code == 404
    assert r.json()["detail"]["type"] == "not_supported"

    assert parse_log(caplog) == [{
        "error": "No InfluxDB issuer configuration",
        "event": "Not configured",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl":
            (f"https://{TEST_HOSTNAME}/auth/tokens/influxdb/new"),
            "remoteIp": "127.0.0.1",
        },
        "scope": "user:token",
        "severity": "warning",
        "token": token_data.token.key,
        "token_source": "bearer",
        "user": token_data.username,
    }]
Exemple #5
0
async def test_issue_token(tmp_path: Path, factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    issuer = factory.create_token_issuer()

    token_data = await create_session_token(factory)
    oidc_token = issuer.issue_token(token_data, jti="new-jti", scope="openid")

    assert oidc_token.claims == {
        "aud": config.issuer.aud,
        "exp": ANY,
        "iat": ANY,
        "iss": config.issuer.iss,
        "jti": "new-jti",
        "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,
    }

    now = time.time()
    assert now - 5 <= oidc_token.claims["iat"] <= now + 5
    expected_exp = now + config.issuer.exp_minutes * 60
    assert expected_exp - 5 <= oidc_token.claims["exp"] <= expected_exp + 5
Exemple #6
0
async def test_key_retrieval(tmp_path: Path, respx_mock: respx.Router,
                             factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc-no-kids")
    factory.reconfigure(config)
    assert config.oidc
    verifier = factory.create_token_verifier()

    # Initial working JWKS configuration.
    jwks = config.issuer.keypair.public_key_as_jwks("some-kid")

    # Register that handler at the well-known JWKS endpoint.  This will return
    # a connection refused from the OpenID Connect endpoint.
    jwks_url = urljoin(config.oidc.issuer, "/.well-known/jwks.json")
    oidc_url = urljoin(config.oidc.issuer, "/.well-known/openid-configuration")
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(404)

    # Check token verification with this configuration.
    token = await create_upstream_oidc_token(kid="some-kid")
    assert await verifier.verify_oidc_token(token)

    # Wrong algorithm for the key.
    jwks.keys[0].alg = "ES256"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    with pytest.raises(UnknownAlgorithmException):
        await verifier.verify_oidc_token(token)

    # Should go back to working if we fix the algorithm and add more keys.
    # Add an explicit 404 from the OpenID connect endpoint.
    respx_mock.get(oidc_url).respond(404)
    jwks.keys[0].alg = ALGORITHM
    keypair = RSAKeyPair.generate()
    jwks.keys.insert(0, keypair.public_key_as_jwks("a-kid").keys[0])
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    assert await verifier.verify_oidc_token(token)

    # Try with a new key ID and return a malformed reponse.
    respx_mock.get(jwks_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="malformed")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Return a 404 error.
    respx_mock.get(jwks_url).respond(404)
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Fix the JWKS handler but register a malformed URL as the OpenID Connect
    # configuration endpoint, which should be checked first.
    jwks.keys[1].kid = "another-kid"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="another-kid")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Try again with a working OpenID Connect configuration.
    respx_mock.get(oidc_url).respond(json={"jwks_uri": jwks_url})
    assert await verifier.verify_oidc_token(token)
async def test_influxdb_force_username(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    config = await configure(tmp_path, "influxdb-username")
    factory.reconfigure(config)
    token_data = await create_session_token(factory)
    assert token_data.expires
    influxdb_secret = config.issuer.influxdb_secret
    assert influxdb_secret

    caplog.clear()
    r = await client.get(
        "/auth/tokens/influxdb/new",
        headers={"Authorization": f"bearer {token_data.token}"},
    )

    assert r.status_code == 200
    data = r.json()
    claims = jwt.decode(data["token"], influxdb_secret, algorithms=["HS256"])
    assert claims == {
        "username": "******",
        "exp": int(token_data.expires.timestamp()),
        "iat": ANY,
    }

    assert parse_log(caplog) == [{
        "event": "Issued InfluxDB token",
        "influxdb_username": "******",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl":
            (f"https://{TEST_HOSTNAME}/auth/tokens/influxdb/new"),
            "remoteIp": "127.0.0.1",
        },
        "scope": "user:token",
        "severity": "info",
        "token": token_data.token.key,
        "token_source": "bearer",
        "user": token_data.username,
    }]
Exemple #8
0
async def test_verify_oidc_no_kids(tmp_path: Path, respx_mock: respx.Router,
                                   factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc-no-kids")
    factory.reconfigure(config)
    keypair = config.issuer.keypair
    verifier = factory.create_token_verifier()
    await mock_oidc_provider_config(respx_mock, keypair, "kid")

    now = datetime.now(timezone.utc)
    exp = now + timedelta(days=24)
    payload: Dict[str, Any] = {
        "aud": config.verifier.oidc_aud,
        "iat": int(now.timestamp()),
        "iss": config.verifier.oidc_iss,
        "exp": int(exp.timestamp()),
    }
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"Issuer {config.verifier.oidc_iss} has no kid a-kid"
    assert str(excinfo.value) == expected
Exemple #9
0
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
Exemple #10
0
async def test_login(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    clients = [OIDCClient(client_id="some-id", client_secret="some-secret")]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    token_data = await create_session_token(factory)
    await set_session_cookie(client, token_data.token)
    return_url = f"https://{TEST_HOSTNAME}:4444/foo?a=bar&b=baz"

    # Log in
    caplog.clear()
    r = await client.get(
        "/auth/openid/login",
        params={
            "response_type": "code",
            "scope": "openid",
            "client_id": "some-id",
            "state": "random-state",
            "redirect_uri": return_url,
        },
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    assert url.scheme == "https"
    assert url.netloc == f"{TEST_HOSTNAME}:4444"
    assert url.path == "/foo"
    assert url.query
    query = parse_qs(url.query)
    assert query == {
        "a": ["bar"],
        "b": ["baz"],
        "code": [ANY],
        "state": ["random-state"],
    }
    code = query["code"][0]

    assert parse_log(caplog) == [{
        "event": "Returned OpenID Connect authorization code",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl": ANY,
            "remoteIp": "127.0.0.1",
        },
        "return_url": return_url,
        "scope": "user:token",
        "severity": "info",
        "token": token_data.token.key,
        "token_source": "cookie",
        "user": token_data.username,
    }]

    # Redeem the code for a token and check the result.
    caplog.clear()
    r = await client.post(
        "/auth/openid/token",
        data={
            "grant_type": "authorization_code",
            "client_id": "some-id",
            "client_secret": "some-secret",
            "code": code,
            "redirect_uri": return_url,
        },
    )
    assert r.status_code == 200
    assert r.headers["Cache-Control"] == "no-store"
    assert r.headers["Pragma"] == "no-cache"
    data = r.json()
    assert data == {
        "access_token": ANY,
        "token_type": "Bearer",
        "expires_in": ANY,
        "id_token": ANY,
    }
    assert isinstance(data["expires_in"], int)
    exp_seconds = config.issuer.exp_minutes * 60
    assert exp_seconds - 5 <= data["expires_in"] <= exp_seconds

    assert data["access_token"] == data["id_token"]
    verifier = factory.create_token_verifier()
    token = verifier.verify_internal_token(OIDCToken(encoded=data["id_token"]))
    assert token.claims == {
        "aud": config.issuer.aud,
        "exp": ANY,
        "iat": ANY,
        "iss": config.issuer.iss,
        "jti": OIDCAuthorizationCode.from_str(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,
    }
    now = time.time()
    expected_exp = now + config.issuer.exp_minutes * 60
    assert expected_exp - 5 <= token.claims["exp"] <= expected_exp
    assert now - 5 <= token.claims["iat"] <= now

    username = token_data.username
    assert parse_log(caplog) == [{
        "event":
        f"Retrieved token for user {username} via OpenID Connect",
        "httpRequest": {
            "requestMethod": "POST",
            "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token",
            "remoteIp": "127.0.0.1",
        },
        "severity":
        "info",
        "token":
        OIDCAuthorizationCode.from_str(code).key,
        "user":
        username,
    }]
Exemple #11
0
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",
    }
Exemple #12
0
async def test_login_errors(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    clients = [OIDCClient(client_id="some-id", client_secret="some-secret")]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    token_data = await create_session_token(factory)
    await set_session_cookie(client, token_data.token)

    # No parameters at all.
    r = await client.get("/auth/openid/login")
    assert r.status_code == 422

    # Good client ID but missing redirect_uri.
    login_params = {"client_id": "some-id"}
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 422

    # Bad client ID.
    caplog.clear()
    login_params = {
        "client_id": "bad-client",
        "redirect_uri": f"https://{TEST_HOSTNAME}/",
    }
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 400
    data = r.json()
    assert data["detail"][0]["type"] == "invalid_client"
    assert "Unknown client_id bad-client" in data["detail"][0]["msg"]

    assert parse_log(caplog) == [{
        "error": "Unknown client_id bad-client in OpenID Connect request",
        "event": "Invalid request",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl": ANY,
            "remoteIp": "127.0.0.1",
        },
        "return_url": f"https://{TEST_HOSTNAME}/",
        "scope": "user:token",
        "severity": "warning",
        "token": ANY,
        "token_source": "cookie",
        "user": token_data.username,
    }]

    # Bad redirect_uri.
    login_params["client_id"] = "some-id"
    login_params["redirect_uri"] = "https://foo.example.com/"
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 422
    assert "URL is not at" in r.text

    # Valid redirect_uri but missing response_type.
    login_params["redirect_uri"] = f"https://{TEST_HOSTNAME}/app"
    caplog.clear()
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    assert url.scheme == "https"
    assert url.netloc == TEST_HOSTNAME
    assert url.path == "/app"
    assert url.query
    query = parse_qs(url.query)
    assert query == {
        "error": ["invalid_request"],
        "error_description": ["Missing response_type parameter"],
    }

    assert parse_log(caplog) == [{
        "error": "Missing response_type parameter",
        "event": "Invalid request",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl": ANY,
            "remoteIp": "127.0.0.1",
        },
        "return_url": login_params["redirect_uri"],
        "scope": "user:token",
        "severity": "warning",
        "token": ANY,
        "token_source": "cookie",
        "user": token_data.username,
    }]

    # Invalid response_type.
    login_params["response_type"] = "bogus"
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 307
    assert query_from_url(r.headers["Location"]) == {
        "error": ["invalid_request"],
        "error_description": ["code is the only supported response_type"],
    }

    # Valid response_type but missing scope.
    login_params["response_type"] = "code"
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 307
    assert query_from_url(r.headers["Location"]) == {
        "error": ["invalid_request"],
        "error_description": ["Missing scope parameter"],
    }

    # Invalid scope.
    login_params["scope"] = "user:email"
    r = await client.get("/auth/openid/login", params=login_params)
    assert r.status_code == 307
    assert query_from_url(r.headers["Location"]) == {
        "error": ["invalid_request"],
        "error_description": ["openid is the only supported scope"],
    }