# Connect an account from a page — Security.OAuthConnectUrl

> Lets an admin CONNECT an account the app then uses with nobody present — a support inbox a background poll reads — from a page: `Security.OAuthConnectUrl(client, secret, returnTo, account)` answers the provider's consent URL; the platform's callback keeps the refresh token the provider grants as the APP secret `secret`, where the app reads it as `Secret.X` like any other credential, and sends the admin back to `returnTo`. Only the account asked for is accepted; nobody is signed in by it.

<!-- id: config-oauth-connect · area: config · stability: preview · html: https://osysharp.com/reference/config/oauth-connect/ -->

## Summary        {#summary}
`OAuthClient.X.ConnectedToken` spends the SIGNED-IN person's grant, so it fails where nobody is signed in — a
workflow step, a poll. An account the APP acts through (a support inbox) is connected once, by an admin, and kept as
the app's own secret:

```osy syntax
[Authorize(IsAdmin)]
public string ConnectSupport() {
  return Security.OAuthConnectUrl("GoogleMail", "SupportMailboxToken", "/settings/mailboxes", "support@acme.com");
}
// the page:  action Connect() { Navigation.Open(ConnectSupport(), sameTab: true); }
```

## Signature      {#signature}
```osy syntax
string Security.OAuthConnectUrl(
  string client,    // an app.OAuthClients entry with OAuthCapability.Connection — its Scopes are what is asked
  string secret,    // an app.Secrets name — where the granted refresh token is KEPT
  string returnTo,  // a path on this app ("/settings/mailboxes") — where the admin lands afterwards
  string account)   // the account they must connect AS ("" = any) — any other is refused
```

Needs `use Osysharp.Oauth;`. Server-only.

## Description    {#description}

### What happens   {#flow}
1. The function (behind the app's own authorization — `[Authorize(IsAdmin)]`) answers the provider's consent URL.
   It seals the client, the secret, the account, `returnTo` and the person who started it into a single-use state
   that expires in fifteen minutes.
2. The page navigates there; the account's owner signs in to the provider and consents.
3. The provider returns to the platform's OAuth callback. The platform exchanges the code (PKCE, the client secret),
   checks the account is the one asked for, and writes the refresh token into the secret — then sends the admin to
   `returnTo` with `#oauth_connected=<secret>` in the fragment, or `#oauth_connect_error=<why>` (the wrong account; no
   refresh token granted; the secret not declared). Nobody is signed in, and the token never reaches the browser.
4. The app reads it as `Secret.X` — `Security.IsSecretSet("X")` says whether it is connected yet.

### Why a secret   {#why-a-secret}
Everything a credential needs is already true of a secret: encrypted at rest, unreadable by a page, confined so a
function cannot return it, never recorded in a workflow's durable history, and leashed so a package reads only the
secrets the app granted it. Keeping the grant there means a poll uses a connected inbox exactly as it would use one
whose token an operator set by hand, and "reconnect" is simply connecting again.

### Who may start it   {#who}
The app decides, with its own authorization on the function that calls this — the platform adds no rule of its own.
A PACKAGE may aim it only at a secret the app granted it (`use … { secret "X"; }`), as for reading one.

### The provider side   {#provider}
The client's `Scopes` are what the consent screen asks for — for a Gmail inbox `openid`, `email`,
`https://www.googleapis.com/auth/gmail.readonly` and `…/gmail.send`. The platform asks for offline access (Google:
`access_type=offline`, `prompt=consent`), which is what yields a refresh token; a provider that grants none is refused
with that reason. Register the platform's callback (`https://<app's host>/api/oauth/callback`) as a redirect URI.

## Examples       {#examples}

```osy title="connect a support inbox from an admin page" test app=config-oauth-connect
using Osysharp.Oauth;

[Role] enum AppRole { Authenticator, Admin }
[RoleGrant] entity RoleGrant { [Required] User Member; [Required] AppRole Role; security { allow read when IsAuthenticated; } }
policy IsAdmin => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Admin);

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

app.Secrets = [ new Secret("GoogleMailSecret"), new Secret("SupportMailboxToken") ];

app.OAuthClients = [
  new OAuthClient("GoogleMail") {
    Provider     = OAuthProvider.Google,
    Capabilities = [OAuthCapability.Connection],
    ClientId     = "1234-abc.apps.googleusercontent.com",
    ClientSecret = Secret.GoogleMailSecret,
    Scopes       = ["openid", "email", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.send"],
  },
];

/// The page's "Connect" button calls this; the inbox's owner consents, and the grant becomes SupportMailboxToken.
[Authorize(IsAdmin)]
public string ConnectSupport() {
  return Security.OAuthConnectUrl("GoogleMail", "SupportMailboxToken", "/settings/mailboxes", "support@acme.example");
}

/// Whether the inbox is connected yet.
[Authorize(IsAdmin)]
public bool SupportConnected() {
  return Security.IsSecretSet("SupportMailboxToken");
}
```

## See also       {#see-also}
- [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — declaring the client, and `ConnectedToken` / `ServiceToken`
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where the grant is kept, and how it is read
- [Pushed mail — Gmail through Pub/Sub, Microsoft 365 through Graph notifications](https://osysharp.com/reference/ui/mail-receiving-push/) — a connected Gmail inbox, pushed through Pub/Sub
