TykTechnologies / tyk

Tyk Open Source API Gateway written in Go, supporting REST, GraphQL, TCP and gRPC protocols
Other
9.69k stars 1.09k forks source link

[TT-10493] Missing OIDC scope will grant access to APIs #4980

Open david-woelfle opened 1 year ago

david-woelfle commented 1 year ago

Branch/Environment/Version

Describe the bug I use the GW with Keycloak via OIDC and configure the access rights to the APIs via the scope of the JWT (i.e. via 'Roles' that can be configured in Keycloak). While this seems to work generally fine, there seem to exist two error cases where the GW provides access to the target even though no permission where granted in the respective scope.

  1. If the claim does not exist within the JWT payload like this (note that there is no roles claim here):

    {
    "exp": 1682505346,
    "iat": 1682505046,
    "jti": "7d1ae257-65e9-477e-97b9-83616d6b2444",
    "iss": "http://keycloak.example.com/auth/realms/MyRealm",
    "aud": "my-keycloak-client-id",
    "sub": "34f33fef-a7eb-41c5-a1ed-7a2318963a18",
    "typ": "Bearer",
    "azp": "my-keycloak-client-id",
    "session_state": "cbbef6aa-c48d-46a2-adf8-7dbb5e587b12",
    "acr": "1",
    "scope": "openid",
    "sid": "cbbef6aa-c48d-46a2-adf8-7dbb5e587b12"
    }
  2. If the claim exists within the JWT payload but contains a scope for which no matching policy is defined.

    {
    "exp": 1682505346,
    "iat": 1682505046,
    "jti": "7d1ae257-65e9-477e-97b9-83616d6b2444",
    "iss": "http://keycloak.example.com/auth/realms/MyRealm",
    "aud": "my-keycloak-client-id",
    "sub": "34f33fef-a7eb-41c5-a1ed-7a2318963a18",
    "typ": "Bearer",
    "azp": "my-keycloak-client-id",
    "session_state": "cbbef6aa-c48d-46a2-adf8-7dbb5e587b12",
    "acr": "1",
    "scope": "openid",
    "sid": "cbbef6aa-c48d-46a2-adf8-7dbb5e587b12"
    "roles": [
    "role2"
    ]
    }

Reproduction steps Steps to reproduce the behavior:

  1. Setup Keycloak, create realm and a oidc-connect client. It is necessary to add two Mappers to the client to make this generally work (I can provide more detailes here if required):
    • A mapper of type Audiance that adds the keycloak client id to the AUD field of the JWT.
    • A mapper of type User Client Role that adds the roles in keycloak to the roles claim of the JWT.
  2. Create a API definition like this:
    {
    "name": "OpenID Connect tester",
    "slug": "openid-connect-tester",
    "api_id": "openid-connect-tester",
    "org_id": "my-org",
    "domain": "localhost",
    "use_openid": true,
    "openid_options": {
        "providers": [
            {
                "issuer": "http://keycloak.example.com/auth/realms/MyRealm",
                "client_ids": {
                    "bXkta2V5Y2xvYWstY2xpZW50LWlk": "oidc_default"
                }
            }
        ],
        "segregate_by_client": false
    },
    "version_data": {
        "not_versioned": true,
        "versions": {
            "Default": {
                "name": "Default",
                "use_extended_paths": true
            }
        }
    },
    "scopes": {
        "oidc": {
            "scope_claim_name": "roles",
            "scope_to_policy": {
                "role1": "openid-connect-tester"
            }
        }
    },
    "proxy": {
        "listen_path": "/openid-connect-tester/",
        "target_url": "http://httpbin.org/headers",
        "strip_listen_path": true,
        "preserve_host_header": true
    },
    "active": true
    }
  3. Create a API definition like this (This API definition will never be triggered in practice. It is allowed by the oidc-default policy to effectively block all access not granted by scopes:
    {
    "name": "OIDC Default (Dummy) API",
    "slug": "oidc_default",
    "api_id": "oidc_default",
    "org_id": "my-org",
    "domain": "non-existing.subdomain.example.com",
    "use_keyless": true,
    "version_data": {
        "not_versioned": true,
        "versions": {
            "Default": {
                "name": "Default",
                "use_extended_paths": true
            }
        }
    },
    "proxy": {
        "listen_path": "/",
        "target_url": "https://www.example.com/",
        "strip_listen_path": false,
        "preserve_host_header": false
    },
    "active": true
    }
  4. Create a policy file like this:
    {
    "oidc_default": {
        "id": "oidc_default",
        "org_id": "my-org",
        "access_rights": {
            "oidc_default": {
                "api_id": "oidc_default",
                "versions": [
                    "Default"
                ]
            }
        },
        "active": true,
        "per_api": true
    },
    "openid-connect-tester": {
        "id": "openid-connect-tester",
        "org_id": "my-org",
        "access_rights": {
            "openid-connect-tester": {
                "api_id": "openid-connect-tester",
                "versions": [
                    "Default"
                ]
            }
        },
        "per_api": true,
        "active": true
    }
    }
  5. Create a user in Keycloak, and assign no roles to that user. You should now be able to access the openid-connect-tester API even though the user has not the correct roles assigned (case 1 above).
  6. Create a role role2 in keycloak and assign it to the user. It is still possible to access the openid-connect-tester API even though the user has not the correct role (role1) assigned.

Actual behavior GW grants access to target for every user with a valid JWT from keycloak, even though the user has not been granted the permissions as scope (see above).

Expected behavior GW should block access to target for every user that hasn't explicitly granted permissions.

Logs (debug mode or log file): Can be provided if required.

Configuration (tyk config file):

{
  "log_level": "warn",
  "listen_port": 8080,
  "control_api_hostname": "localhost",
  "control_api_port": 9000,
  "secret": "verysecret123",
  "template_path": "/opt/tyk-gateway/templates",
  "tyk_js_path": "/opt/tyk-gateway/js/tyk.js",
  "middleware_path": "./middleware",
  "use_db_app_configs": false,
  "app_path": "/opt/tyk-gateway/apps/",
  "storage": {
    "type": "redis",
    "host": "gateway-redis",
    "port": 6379,
    "username": "",
    "password": "",
    "database": 0,
    "optimisation_max_idle": 2000,
    "optimisation_max_active": 4000
  },
  "enable_analytics": true,
  "analytics_config": {
    "type": "",
    "ignored_ips": [],
    "pool_size": 4,
    "normalise_urls": {
      "enabled": true,
      "normalise_uuids": true
    }
  },
  "enable_custom_domains": true,
  "health_check": {
    "enable_health_checks": false,
    "health_check_value_timeouts": 60
  },
  "optimisations_use_async_session_write": false,
  "enable_non_transactional_rate_limiter": true,
  "enable_sentinel_rate_limiter": false,
  "enable_redis_rolling_limiter": false,
  "allow_master_keys": false,
  "policies": {
    "policy_source": "file",
    "policy_record_name": "/opt/tyk-gateway/policies/policies.json"
  },
  "hash_keys": true,
  "close_connections": false,
  "http_server_options": {
    "enable_websockets": true
  },
  "allow_insecure_configs": false,
  "coprocess_options": {
    "enable_coprocess": true,
    "python_path_prefix": "/opt/tyk-gateway"
  },
  "enable_bundle_downloader": false,
  "global_session_lifetime": 100,
  "force_global_session_lifetime": false,
  "max_idle_connections_per_host": 500,
  "enable_jsvm": true
}
andyo-tyk commented 1 year ago

Hi @david-woelfle, Thanks for raising this - we'll take a look and see what we can do on this.

joshblakeley commented 5 months ago

Note for other users. The OIDC is more or less deprecated in favour of the JWT middleware for doing token validation and authZ in OIDC flows. If using the JWT middleware this bug is not present.