Osy#betaRun it on Osyrin: osyrin.com

Reference / Config

OAuth clients (app.OAuthClients)

app.OAuthClients = [ new OAuthClient("Name") { Provider = OAuthProvider.Google, Capabilities = [OAuthCapability.Login], ClientId = "…", ClientSecret = Secret.X } ];

`app.OAuthClients` declares the third-party OAuth providers your app uses — for signing users in (Login), for connecting to an external API on a user's behalf (Connection), and for your app holding its own machine credential (Service). Each is `new OAuthClient("Name")` naming a Provider (Google/Github/Microsoft/Oidc), the Capabilities, a ClientId, and a ClientSecret (a `Secret.X` handle). Referenced elsewhere by its `OAuthClient.Name` handle.

stable2 examples compiled by CIconfigoauthauthsecurity

Summary#

app.OAuthClients declares the third-party OAuth providers your application talks to. There are two reasons to declare one: to let users sign in with an external identity (Login), to connect to an external API and act on a user's behalf (Connection), and for your app to hold its own credential with a service (Service). Each client is a new OAuthClient("Name") that names a Provider, the Capabilities you need, a ClientId, and a ClientSecret (a Secret.X handle). The client's Name is how the rest of your app refers back to it as OAuthClient.Name.

app.OAuthClients = [
  new OAuthClient("Google") {
    Provider     = OAuthProvider.Google,
    Capabilities = [OAuthCapability.Login],
    ClientId     = "your-client-id",
    ClientSecret = Secret.GoogleOAuth,
  },
];

Signature#

using Osysharp.Oauth;                          // the capability the client types come from

app.Secrets = [ new Secret("GoogleOAuth") ];   // the handle the client below reads

app.OAuthClients = [
  new OAuthClient("Name") {            // one entry per provider client
    Provider     = OAuthProvider.Google,          // Google | Github | Microsoft | Oidc
    Capabilities = [OAuthCapability.Login, OAuthCapability.Connection],
    ClientId     = "your-client-id",              // the provider-issued client id
    ClientSecret = Secret.GoogleOAuth,            // a Secret.X handle, never a literal
  },
];

app.OAuthClients is a list — an app may declare several clients, one per provider (or several against the same provider for different capabilities).

Description#

An OAuthClient has:

  • Provider — an OAuthProvider enum member naming the identity provider: Google, Github, Microsoft, or Oidc (a generic OpenID Connect provider).

  • Capabilities — a list of OAuthCapability, and it decides who the token belongs to:

    • Login — users sign into your app with this provider.
    • Connection — your app calls the provider's API as a signed-in user, using a token that person granted.
    • Service — your app calls the API as itself, with no user involved. See [[#service]] below.

    A client may declare more than one.

  • ClientId — a string: the client identifier the provider issued when you registered your app with it.

  • ClientSecret — a Secret.X handle referring to a secret declared in declaring secrets (app.Secrets). You never write the secret value inline; you reference the named secret.

The client's Name (the argument to new OAuthClient("…")) is its handle. That name is how the rest of your app refers back to the client as OAuthClient.Name — for example an OAuthAuth method that signs users in via this provider references the client by its OAuthClient.Name handle.

When your app is the one with the account#

Connection borrows a token from the person who is signed in. Sometimes there is no such person — a scheduled job, a workflow step, a webhook receiver — and the account belongs to your app rather than to any user. That is OAuthCapability.Service: the client-credentials grant, where your ClientId and ClientSecret are themselves the credential.

Declare the capability:

app.Secrets = [ new Secret("WarehouseSecret") ];

app.OAuthClients = [
  new OAuthClient("Warehouse") {
    Provider     = OAuthProvider.Oidc,
    Capabilities = [OAuthCapability.Service],
    Issuer       = "https://example.cloud.databricks.com/oidc",   // the token endpoint is discovered from this
    Scopes       = ["all-apis"],
    ClientId     = "your-service-principal-id",
    ClientSecret = Secret.WarehouseSecret,
  },
];

Because no user is involved, a service credential works in exactly the places ConnectedOAuth cannot: a background job, a workflow step, an [AllowAnonymous] function.

Spending the token — OAuthClient.<Name>.ServiceToken / .ConnectedToken#

A declared client's token is read as a member of the client handle, the same shape Secret.<Name> uses — and you put it wherever the API wants it, usually an Authorization header on a typed client class:

app Warehouse {
  model "model/**/*.osy";
  use Osysharp.Http;
  use Osysharp.Oauth;
}

using Osysharp.Http;

app.Secrets = [ new Secret("WarehouseSecret") ];

app.OAuthClients = [
  new OAuthClient("Warehouse") {
    Provider     = OAuthProvider.Oidc,
    Capabilities = [OAuthCapability.Service],
    Issuer       = "https://example.cloud.databricks.com/oidc",
    Scopes       = ["all-apis"],
    ClientId     = "your-service-principal-id",
    ClientSecret = Secret.WarehouseSecret,
  },
];

string Clusters() {
  var headers = new Dictionary<string, string>();
  headers.Add("Authorization", "Bearer " + OAuthClient.Warehouse.ServiceToken);
  var r = Http.Get("https://example.cloud.databricks.com/api/2.0/clusters/list", headers);
  return r.IsSuccess ? r.Body : "";
}

Every read hands you a fresh-enough token; you never write one down and there is nothing to refresh by hand.

Which verb — it is the PRINCIPAL, and the call site says so#

verbwhose authority the call carriesneeds
.ServiceTokenthe app's own — no user at all, so it works in a workflow step, a background job, an [AllowAnonymous] functionCapabilities = [OAuthCapability.Service]
.ConnectedTokenthe signed-in person's — delegated, "as me"Capabilities = [OAuthCapability.Connection], and a signed-in caller who has connected that account

⛔ The verb is explicit rather than inferred from the declaration, and that is deliberate. These two differ in whose authority an outbound call carries — so a reviewer reading the call site must be able to see which without opening another file, and a client may declare both capabilities anyway. A client declared only for OAuthCapability.Login has neither grant: Login authenticates a person, it does not hand you anything to spend, and asking it for either token is refused by name.

⚠ .ConnectedToken fails where there is no person, which is correct and is the commonest surprise: the same function body works under a request and throws in a worker or a workflow step. That throw is catch (OAuthConnectionFailedException e)-able, and it also fires when the user simply has not connected that account yet, or has connected it without a scope you now need.

A token is a credential, and is confined like one#

⛔ You cannot return it, and you cannot put it on a screen. return OAuthClient.X.ServiceToken; is refused (SECRET_ESCAPES), directly or through a local, and a component body that reads one is refused because it would be sent to the browser — where a bearer token is worse than a secret, being directly spendable against someone else's API with nothing else needed. Spend it where it is needed and return the RESULT, exactly as with a secret.

⚑ Why a token read is never stale on a durable resume. An outbound call inside a workflow is a durable step, so you might expect a memoized token to come back expired days later. It cannot: the read is its own node kind rather than a stdlib call, and memoization is keyed on a call's durability classification — so a leaf that is not a call has nothing to memoize against and is re-read on every evaluation. That also means a token is never recorded in a run's durable history, which an External classification would have done.

⚠ Provider must be Oidc, with an Issuer. The client-credentials grant is an OpenID Connect mechanism, and the token endpoint is discovered from the issuer. Google's and GitHub's consumer OAuth authenticate a person and have no such grant — naming one is refused, by name, rather than failing at the first request.

⚠ Scopes are sent with the grant. A service token with the wrong scopes is refused by the service you are calling, not by your app, which is the least helpful place to find out — so declare them here.

The capability: use Osysharp.Oauth;#

Per-user OAuth links are a capability, Osysharp.Oauth. Declaring an OAuthClient sets up the provider; the capability adds the per-user side: a UserOAuthLink entity that ties one of your app's users to the identity (and, for a Connection client, the stored tokens) they hold with a provider. The platform writes these links when a user signs in or connects; the use/using gates whether your own code may name and query them.

  • use Osysharp.Oauth; in the app { } manifest declares the dependency.
  • using Osysharp.Oauth; at the top of a source file imports the UserOAuthLink name so a function or query in that file may reference a user's linked provider identities.

See use for how use (the dependency) and using (the file-level import) differ.

Examples#

Declare the secret, then declare a Google client that lets users sign in:

using Osysharp.Oauth;

app.Secrets = [ new Secret("GoogleOAuth") ];

app.OAuthClients = [
  new OAuthClient("Google") {
    Provider = OAuthProvider.Google,
    Capabilities = [OAuthCapability.Login],
    ClientId = "your-client-id",
    ClientSecret = Secret.GoogleOAuth,
  },
];

See also#

Related

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…

[AuthMethod] — a function an unauthenticated visitor may call

`[AuthMethod]` marks a sign-in function — login, signup, password-reset — as reachable by a visitor who is not signed…

auth bootstrap (login, before anyone is signed in)

Under deny-all, login faces a paradox: it must read a user row *before* anyone is authenticated. `app.AuthBootstrap`…

use

Declares a capability your app depends on, written inside the `app { }` manifest block. It provisions the capability…

a typed HTTP client (a class over Http.*)

A typed HTTP client is an **ordinary class** that wraps an external API: a `BaseUrl` field, a credential field, one…