# OAuth and OIDC Provider Metadata Vendor-Intake Gate (`kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate`) Actor

Writes the security opinion that a vendor-intake review needs before a company connects to a SaaS vendor, an identity provider or a remote MCP server. For each issuer that you name, the Actor reads the public OpenID Connect Discovery or RFC 8414 metadata

- **URL**: https://apify.com/kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run\_starts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## OAuth and OIDC Provider Metadata Vendor-Intake Gate

Write the security opinion that a vendor-intake review needs, before your
company connects to a SaaS vendor, an identity provider or a remote MCP server.

You give a list of issuer base URLs. For each issuer the Actor reads the public
OpenID Connect Discovery or RFC 8414 metadata document, the published JSON Web
Key Set, and the RFC 9728 protected-resource metadata, and it reads the dynamic
client registration endpoint. It then gives you one dataset row for each issuer
with the endpoints, the declared flows, the key-set defects, a list of findings
and an A to F risk grade. Attach that row to your review ticket as the evidence
of your written opinion.

### The Actor reads public metadata only

- Every request is a GET.
- The Actor sends no credential and no client identifier.
- The Actor starts no login flow.
- The Actor never sends a POST to the registration endpoint, so it never
  registers a client at the vendor. The registration probe is a plain read that
  only shows whether the endpoint answers a stranger.
- Only HTTPS is allowed. A URL that carries credentials is refused. A host that
  resolves to a loopback, private, link-local or otherwise reserved address is
  refused, and each redirect hop passes the same guard again.
- A refused issuer line does not stop the run. It gets its own dataset row with
  the finding `OIDC-DISCOVERY-UNREACHABLE`, it is not charged, and the run
  audits every other line of the list and ends with SUCCEEDED.
- No browser, no proxy, no external database and no paid API.

### Input

Run the Actor with an empty input to audit the three public identity providers
of the default fixture. Every field below has a default.

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `issuers` | array of strings | three public issuers | 1 to 200 issuer base URLs, for example `https://accounts.google.com`, or full `.well-known` metadata URLs. For a base URL the Actor tries the OpenID Connect Discovery path and the RFC 8414 path in turn and keeps the first document that parses. Repeated URLs are read once. |
| `checks` | array of strings | every group | The groups of rules to apply. See the table below. |
| `baselineKey` | string | `""` (empty) | Optional name of a stored snapshot, for example `Q3 vendor review`. Set it to get the `changed_since_baseline` flag. Leave it empty for a single audit with no stored state. Any name works: the run makes it safe as a record name (see below). |
| `requestTimeoutSeconds` | integer | `15` | How long one HTTPS request may take. Allowed range 5 to 60. |

#### Check groups

| Group | What it examines |
| --- | --- |
| `metadata` | The document is reachable, is valid JSON, and its `issuer` field names the audited URL. This group is always on, because every other group reads the fields of this document. |
| `transport` | Every endpoint URL is HTTPS, and the metadata was served over TLS 1.2 or later. |
| `pkce` | `S256` is declared, and the weak `plain` method is not offered. |
| `grants` | No implicit or hybrid flow, so no token travels in a URL. |
| `client_auth` | The token endpoint does not accept the `none` client authentication method. |
| `jwks` | The key set is reachable and holds no short RSA key, no unapproved curve, no shared secret, no private key material and no expired certificate. |
| `registration` | Whether the dynamic client registration endpoint answers an anonymous read. |
| `resource_metadata` | Whether the issuer publishes RFC 9728 protected-resource metadata. |
| `endpoints` | Whether the authorization, token, revocation and introspection endpoints are named. |

Switching a group off also stops the requests that only that group needs.
Without `jwks` the Actor does not read the key set, without `registration` it
does not probe the registration endpoint, and without `resource_metadata` it
does not look for the protected-resource document.

### Output

The dataset holds one row for each issuer, then one summary row. The `row_type`
field tells the two apart.

#### Issuer row (`row_type: "issuer"`)

`issuer`, `discovery_url`, `http_status`, `metadata_valid_json`,
`issuer_matches_url`, `tls_version`, `endpoints` (an object with
`authorization`, `token`, `jwks`, `registration`, `revocation` and
`introspection`), `endpoints_https_only`, `pkce_s256_supported`,
`plain_pkce_offered`, `response_types_supported`, `implicit_grant_offered`,
`token_endpoint_auth_methods`, `none_auth_method_offered`, `jwks_reachable`,
`jwks_key_count`, `weak_or_expired_key_flags`,
`dynamic_client_registration_open`, `protected_resource_metadata_present`,
`findings` (each with `rule_id`, `severity`, `message` and `evidence_field`),
`finding_count`, `highest_severity`, `risk_grade`, `changed_since_baseline`,
`baseline_changes`, `unreachable` and `note`.

#### Summary row (`row_type: "summary"`)

`issuers_requested`, `issuers_audited`, `issuers_with_high_severity`,
`findings_by_rule`, `unreachable_issuers`, `issuers_changed_since_baseline`,
`grades`, `baseline_key`, `checks` and `note`.

`issuers_requested` counts the issuers that you gave. `issuers_audited` counts
only the issuers that returned a metadata document that the rules could read, so
it always matches the number of `issuer_audited` events that the run charges.
The rest of the issuers are named in `unreachable_issuers`.

#### Risk grade

| Grade | Meaning |
| --- | --- |
| `F` | Two or more high-severity findings. |
| `E` | One high-severity finding. |
| `D` | Three or more medium-severity findings. |
| `C` | One or two medium-severity findings. |
| `B` | Only low-severity findings. |
| `A` | No finding. |

### Changed since the last review

Give a `baselineKey`, for example `q3-vendor-review`. The Actor stores one small
snapshot for each issuer under that name, and it compares the next run against
it. The snapshot holds the endpoints, the declared methods, the key identifiers
and the rule identifiers that fired.

- On a first run there is nothing to compare against, so
  `changed_since_baseline` is `null`. That is normal and is not a defect.
- On a later run `changed_since_baseline` is `true` or `false`, and
  `baseline_changes` names the fields that moved.
- The name is only a record name in a key-value store. The run replaces every
  character that is not a letter, a digit, a dot, a dash or an underscore with a
  dash, and it keeps the first 60 characters. `Q3 vendor review` therefore
  becomes `Q3-vendor-review`. The summary row reports the name that the run
  used, so give the same name again at the next review.

Version 1 works with no stored state, because a single audit already gives you
the review artefact.

### A business verdict never fails the run

A failed gate, an unreachable vendor, a vendor without a valid discovery
document and a run with no finding are all results of this product. Each one is
a dataset row plus a run status message, and the run ends with SUCCEEDED. Only a
real malfunction, such as an input that cannot be parsed, gives a FAILED run.

### Pricing

The Actor uses the pay-per-event model. It charges these three events.

| Event | Unit | Price (USD) | When it is charged |
| --- | --- | --- | --- |
| `run_start` | per run | 0.01 | Once for each run, after the input parses. |
| `issuer_audited` | per issuer metadata set read and audited | 0.015 | Once for each issuer that returned a valid metadata document. An issuer that gave no valid document was not audited, so it is not charged. |
| `baseline_change_reported` | per issuer that differs from its stored baseline | 0.01 | Only when you give a `baselineKey` and the issuer really moved. A first run and an unchanged issuer are not charged. |

The default maximum total charge for one run is 10 USD, which covers a vendor
portfolio of about 600 issuers.

### Limits

| Limit | Value |
| --- | --- |
| Issuers in one run | 1 to 200 |
| Requests for each issuer | about 6 to 10 small GET requests |
| Issuers read at one time | 5, so the Actor does not load the vendor |
| Request timeout | 5 to 60 s, default 15 s |
| Redirect hops for one fetch | 3, and each hop passes the public-target guard |
| Response body read | 512 KiB, and a larger body is not parsed |
| Baseline name | made safe, then cut to 60 letters, digits, dots, dashes or underscores |

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `issuers` (type: `array`):

1 to 200 issuer base URLs, for example https://accounts.google.com, or full .well-known metadata URLs. The Actor does not crawl. For a base URL it tries the OpenID Connect Discovery path and the RFC 8414 path in turn and keeps the first document that parses. HTTPS only. A line that is not a public HTTPS target, for example an http:// URL, a URL with credentials or a host that resolves to a loopback, private or reserved address, is refused. A refused line gets its own dataset row with the finding OIDC-DISCOVERY-UNREACHABLE, it is not charged, and the run still audits every other line.

## `checks` (type: `array`):

The groups of rules that the audit applies. Leave every group selected for a full vendor-intake opinion. The metadata group is always on, because every other group reads the fields of the discovery document. Switching a group off also stops the requests that only that group needs: jwks stops the key-set read, registration stops the read-only registration probe, and resource\_metadata stops the protected-resource metadata read.

## `baselineKey` (type: `string`):

Optional name of a stored snapshot, for example "Q3 vendor review". The run compares each issuer against the snapshot of the same name and sets changed\_since\_baseline, then it writes the snapshot back for the next review. Leave this empty to run a single audit with no stored state; changed\_since\_baseline is then null. Any name works: the run replaces every character that is not a letter, a digit, a dot, a dash or an underscore with a dash, and keeps the first 60 characters. The summary row reports the name that the run used, so give the same name again at the next review.

## `requestTimeoutSeconds` (type: `integer`):

How long one HTTPS request may take before the Actor gives up on it and reports the issuer as unreachable. Each issuer costs about 6 to 10 small GET requests.

## Actor input object example

```json
{
  "issuers": [
    "https://accounts.google.com",
    "https://appleid.apple.com",
    "https://token.actions.githubusercontent.com"
  ],
  "checks": [
    "metadata",
    "transport",
    "pkce",
    "grants",
    "client_auth",
    "jwks",
    "registration",
    "resource_metadata",
    "endpoints"
  ],
  "baselineKey": "",
  "requestTimeoutSeconds": 15
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "issuers": [
        "https://accounts.google.com",
        "https://appleid.apple.com",
        "https://token.actions.githubusercontent.com"
    ],
    "checks": [
        "metadata",
        "transport",
        "pkce",
        "grants",
        "client_auth",
        "jwks",
        "registration",
        "resource_metadata",
        "endpoints"
    ],
    "baselineKey": "",
    "requestTimeoutSeconds": 15
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "issuers": [
        "https://accounts.google.com",
        "https://appleid.apple.com",
        "https://token.actions.githubusercontent.com",
    ],
    "checks": [
        "metadata",
        "transport",
        "pkce",
        "grants",
        "client_auth",
        "jwks",
        "registration",
        "resource_metadata",
        "endpoints",
    ],
    "baselineKey": "",
    "requestTimeoutSeconds": 15,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "issuers": [
    "https://accounts.google.com",
    "https://appleid.apple.com",
    "https://token.actions.githubusercontent.com"
  ],
  "checks": [
    "metadata",
    "transport",
    "pkce",
    "grants",
    "client_auth",
    "jwks",
    "registration",
    "resource_metadata",
    "endpoints"
  ],
  "baselineKey": "",
  "requestTimeoutSeconds": 15
}' |
apify call kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/oauth-and-oidc-provider-metadata-vendor-intake-gate"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/z2lxEtqj95w5BmA4P/builds/nwgfb9ZalE8aCF7HZ/openapi.json
