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— anOAuthProviderenum member naming the identity provider:Google,Github,Microsoft, orOidc(a generic OpenID Connect provider).Capabilities— a list ofOAuthCapability, 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— aSecret.Xhandle 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#
| verb | whose authority the call carries | needs |
|---|---|---|
.ServiceToken | the app's own — no user at all, so it works in a workflow step, a background job, an [AllowAnonymous] function | Capabilities = [OAuthCapability.Service] |
.ConnectedToken | the 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 theapp { }manifest declares the dependency.using Osysharp.Oauth;at the top of a source file imports theUserOAuthLinkname 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#
- declaring secrets (app.Secrets) —
app.Secrets, where theSecret.Xused byClientSecretis declared - [AuthMethod] — a function an unauthenticated visitor may call — declaring an auth method that signs users in via an
OAuthClient - auth bootstrap (login, before anyone is signed in) — granting the first user their role after an OAuth sign-in
- use —
use Osysharp.Oauth;(the dependency) andusing Osysharp.Oauth;(the import)