# Endpoints a pushing provider calls — Query, ContentType, OidcIssuer

> A provider that PUSHES to your app dictates its own handshake, and three endpoint declarations meet the common ones. `Query` names the query-string parameters the function may read with `Request.Query(name)` (Microsoft Graph's `validationToken`). `ContentType` answers the function's `string` as the whole response body instead of the JSON envelope (Graph wants the token echoed as `text/plain`). `OidcIssuer` has the PLATFORM verify the request's OpenID Connect bearer token — Google Cloud Pub/Sub signs every push with one — and `Request.OidcEmail(audience)` answers who it was issued to, for that audience only; `Request.OidcClaim(audience, name)` answers any other claim of it — a CI job's repository, branch and workflow. The token itself never reaches your code.

<!-- id: api-push-endpoints · area: api · stability: preview · html: https://osysharp.com/reference/api/push-endpoints/ -->

## Summary        {#summary}
Three more things an endpoint may declare — each a grant, like `Raw` and `Headers` on [[api-rest#raw-and-headers]]:

```osy syntax
new Endpoint(GraphHook) { Method = HttpMethod.Post, Path = "/graph", Raw = true,
                          Query = ["validationToken"], ContentType = "text/plain" }
new Endpoint(PubSubHook) { Method = HttpMethod.Post, Path = "/pubsub", Raw = true,
                           OidcIssuer = "https://accounts.google.com" }
```

## Signature      {#signature}
```osy syntax
// on the endpoint
Query       = ["name", …]            // query-string parameters the function may read — matched EXACTLY
ContentType = "text/plain"           // the function's `string` IS the response body, with this Content-Type
OidcIssuer  = "https://issuer"       // the platform verifies the request's bearer token against this issuer

// in the function
string? Request.Query(string name)        // that parameter's value, or null when the request carried none
string? Request.OidcEmail(string audience) // the verified token's email — when it was issued FOR this audience
                                           // and its email is verified; null otherwise
string? Request.OidcClaim(string audience, string name)  // any one-string claim of that verified token, for this
                                                         // audience; null otherwise

// in a test only
TestOidc.Bearer(string issuer, string audience)   // from here on, the request carried a verified token from `issuer`
TestOidc.Claim(string name, string value)         // …carrying this claim
```

## Description    {#description}

### `Query` — a handshake in the query string   {#query}
Microsoft Graph proves an endpoint is yours by POSTing `?validationToken=<token>` to it and expecting the token back.
Meta's webhooks send `hub.challenge` the same way. An endpoint binds only its path and its JSON body, so a query
parameter is DECLARED, exactly as a header is: `Query = ["validationToken"]`, read with `Request.Query("validationToken")`.
A parameter the endpoint did not declare is not readable — the compiler refuses the read and names what IS declared.
Names are matched exactly (a query string's names are case-sensitive, unlike headers).

### `ContentType` — answer in the sender's format   {#content-type}
Every endpoint answers in the JSON envelope ([[api-rest#envelope]]) — so a function returning `string` answers
`{"result": "…"}`. A handshake that compares the body byte for byte needs the bare text. `ContentType = "text/plain"`
makes the function's `string` the WHOLE body, with that Content-Type. The function must return `string`; anything else
is refused at compile time.

### `OidcIssuer` — a signed push, verified by the platform   {#oidc}
Google Cloud Pub/Sub (and Cloud Tasks, Cloud Scheduler, Eventarc) authenticate a push with an OpenID Connect token in
`Authorization: Bearer …`. That header can never be declared: it is how a caller authenticates to the platform, and an
app that could read it could replay its callers' credentials. So the platform verifies it instead:

- the signature against the keys the declared issuer publishes (`<issuer>/.well-known/openid-configuration`, fetched
  once and again when they rotate);
- `iss` is the declared issuer; the token has not expired.

Your function then asks `Request.OidcEmail(audience)`: the token's `email` if — and only if — it was issued FOR that
audience (a token Google minted for somebody else's service never answers for yours) and the issuer verified the
email. Compare it with the account you expect; anything else is `null`. The token itself never reaches Osy#.

The platform logs every token it could not verify, with the reason ("the request carried no bearer token", "signed
with a key the issuer does not publish", "expired at …", "issued by …"), and every one it did, with the email and
audience — so a misconfigured push subscription is found in the log rather than guessed at.

### `Request.OidcClaim` — what else the token says   {#oidc-claim}
A token states more than an email. A CI job's token — GitHub Actions signs one per job for `permissions: id-token: write`
— names the `repository`, the `ref` it ran on, the `job_workflow_ref` (the workflow file and the ref it came from) and
the `sha`. `Request.OidcClaim(audience, name)` answers any claim the verified token carries as ONE string, under the same
rule as `OidcEmail`: valid, issued for that audience, or `null`. A claim that is a list or an object answers `null` too —
a check compares one value. Compare every claim you rely on with what you expect; `job_workflow_ref` alone pins the
repository, the workflow and the branch.

### Testing a verified token: `TestOidc`   {#test-oidc}
In a `.osy` test there is no request, so `Request.OidcEmail` and `Request.OidcClaim` have nothing to read. `TestOidc.Bearer(issuer,
audience)` stands in for the platform's verification from there to the end of the test body, and `TestOidc.Claim(name,
value)` adds a claim to it (`email`, `email_verified` and `sub` fill the matching fields too). A second `Bearer` starts a
new token. Both are refused outside test code, by the compiler and again at run time: in app code they would be a way to
claim any identity.

```osy title="a receiver, driven with and without a verified token" syntax
[Test]
void only_our_main_branch_is_accepted() {
  TestOidc.Bearer("https://token.actions.githubusercontent.com", "builds.example.com");
  TestOidc.Claim("repository", "acme/shop");
  TestOidc.Claim("ref", "refs/heads/main");
  Assert.True(Built().FromOurMain);
  TestOidc.Bearer("https://token.actions.githubusercontent.com", "builds.example.com");
  TestOidc.Claim("repository", "acme/shop");
  TestOidc.Claim("ref", "refs/heads/feature");
  Assert.False(Built().FromOurMain);
}
```

### Open at the transport, proven in the body   {#auth}
A pushing provider has no account in your app, so these endpoints live on a `RestApi` with `Auth = new ApiAuth {
Anonymous = true }` and prove the sender in the function — `OidcEmail` for Pub/Sub, a per-subscription secret for
Graph. Answer a request that does not prove itself with an error (`throw new ValidationException(…)` → `400`).

## Examples       {#examples}

```osy title="Graph's handshake, and a Pub/Sub push from one service account" test app=api-push-endpoints
class PushReceipt { public bool FromOurSubscription; }

/// Graph: echo the validation token, as the whole body.
[AllowAnonymous]
string GraphHook() {
  return Request.Query("validationToken") ?? "";
}

/// Pub/Sub: accept only a push its push subscription's service account signed, for this endpoint.
[AllowAnonymous]
PushReceipt PubSubHook() {
  var who = Request.OidcEmail("https://desk.example.com/api/rest/v1/hooks/pubsub");
  return new PushReceipt { FromOurSubscription = who == "push@acme-desk.iam.gserviceaccount.com" };
}

app.Apis = [
  new RestApi("Hooks") {
    Route = "hooks",
    Auth  = new ApiAuth { Anonymous = true },
    Endpoints = [
      new Endpoint(GraphHook) {
        Method = HttpMethod.Post, Path = "/graph", Raw = true, Query = ["validationToken"], ContentType = "text/plain",
      },
      new Endpoint(PubSubHook) { Method = HttpMethod.Post, Path = "/pubsub", Raw = true, OidcIssuer = "https://accounts.google.com" },
    ],
  },
];
```

```osy title="A CI job's finished build, accepted only from one repository's main branch" test app=api-push-endpoints-ci
class BuildReceipt { public bool FromOurMain; }

/// GitHub Actions signs each job's token; the platform verifies it against GitHub's published keys.
[AllowAnonymous]
BuildReceipt Built() {
  var repository = Request.OidcClaim("builds.example.com", "repository");
  var branch = Request.OidcClaim("builds.example.com", "ref");
  return new BuildReceipt { FromOurMain = repository == "acme/shop" && branch == "refs/heads/main" };
}

app.Apis = [
  new RestApi("Builds") {
    Route = "builds",
    Auth  = new ApiAuth { Anonymous = true },
    Endpoints = [
      new Endpoint(Built) { Method = HttpMethod.Post, Path = "/finished", OidcIssuer = "https://token.actions.githubusercontent.com" },
    ],
  },
];
```

## See also       {#see-also}
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — publishing endpoints, `Raw`, `Headers`, the envelope, who `user` is
- [Pushed mail — Gmail through Pub/Sub, Microsoft 365 through Graph notifications](https://osysharp.com/reference/ui/mail-receiving-push/) — the watched-mailbox push door, built on these
- [Correlation — finding a run by a business key](https://osysharp.com/reference/workflow/correlate/) — handing a verified push to the run it is about
