# Sign in with URnetwork

Let people admit your app or agent to their private network, so it can
coordinate with everything else they have running there.

![The Sign in with URnetwork button, in both variants: the dark button on a light surface, the light button on a dark surface](/docs-assets/sign-in-with-urnetwork-buttons.svg)

[Download the button kit](/docs-assets/sign-in-with-urnetwork-kit.zip) — markup,
styles, both variants as single assets, and the mark on its own. The kit is
final and you can adopt it today, ahead of everything else on this page.

> **In development — not yet live.** This page describes a design under
> construction. The authorization server is written but not deployed:
> `auth.bringyour.com` does not resolve yet, and none of the endpoints below can
> be called today. `client_id`s are issued on request rather than self-serve.
> Everything marked *planned* is not built. The button kit is real and final,
> and you can adopt it now.

## What it is

Signing in is the admission step. What it grants is a *place on a network* —
addressable, discoverable by the person's other software, and revocable by them
at any time.

That makes it a different thing from the sign-in buttons it resembles. You are
not primarily learning who somebody is. You are being let into somewhere.

The grant has two halves, consented and revoked separately:

- **Identity** — that a URnetwork account approved you, and nothing more.
- **Network** — which network you may act on, named on the consent screen and
  bound into the token when it is issued.

An app may ask for identity alone. Network access is never implied by signing
in.

## The button

Use the dark button on light and mid-tone surfaces, the light button on dark
ones. There is no hosted script: the kit is HTML, CSS and one inline SVG, so
placing the button on your page transmits nothing until somebody clicks it.

Prefer the markup below over the SVG assets where you can — the label then
renders in your page's own type and stays selectable, searchable and
translatable. The single-asset SVGs in the
[kit](/docs-assets/sign-in-with-urnetwork-kit.zip) are for places that take an
image and nothing else.

The button reads **Sign in with** followed by the URnetwork lockup. Only the
first part is text, which is what makes the button translate cleanly: render
"Sign in with" in your interface language and leave the lockup alone, so the
brand reads the same everywhere.

```html
<a class="ur-signin" href="YOUR_AUTHORIZE_URL" aria-label="Sign in with URnetwork">
  <span>Sign in with</span>
  <svg class="ur-signin-lockup" viewBox="0 0 980 139" fill="currentColor" aria-hidden="true">
    <!-- the lockup, 10 paths — copy them from the kit -->
  </svg>
</a>
```

The lockup is drawn with `currentColor`, so it inherits the button's text
colour and one copy serves both variants. The `aria-label` carries the whole
name, since half of it is a graphic.

```css
.ur-signin {
  display: inline-flex; align-items: center; gap: 10px;
  height: 44px; padding: 0 20px; min-width: 200px;
  border-radius: 12px; border: 1px solid transparent;
  background: #101010; color: #f8f8f8;
  font: 500 15px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.ur-signin:hover         { background: #1c1c1c; }
.ur-signin:focus-visible { outline: 2px solid #0099ff; outline-offset: 2px; }
.ur-signin-lockup        { height: 16px; width: auto; flex: none; }

/* Light variant, for dark surfaces */
.ur-signin.is-light       { background: #f8f8f8; color: #101010; border-color: rgba(16,16,16,.14); }
.ur-signin.is-light:hover { background: #ececec; }
```

The label uses a system stack rather than the URnetwork brand face, which cannot
be redistributed to other sites — and pinning a webfont would put a network
request back into the button.

### Brand rules

| | |
| --- | --- |
| Height | 44px minimum — also the tap target. Never smaller. |
| Corner radius | 12px |
| Lockup | 16px tall, `currentColor`, never redrawn, recoloured or rebuilt from parts |
| Clear space | 8px on all four sides |
| Focus ring | 2px `#0099ff`, offset 2px |
| Ground | `#101010` dark, `#f8f8f8` light. No other colour, no gradient. |

Do not set the word URnetwork in your own type in place of the lockup, do not
use the mark without the label, and do not animate or skew the button.
Translate "Sign in with" into your interface language; the lockup never
changes.

## How it works

*Planned. The endpoints below are not deployed.*

Authorization code with PKCE, and nothing else — this is OAuth 2.1, so there is
no implicit grant and `S256` is mandatory.

| | |
| --- | --- |
| Issuer | `https://auth.bringyour.com` |
| Discovery | `/.well-known/openid-configuration` and `/.well-known/oauth-authorization-server` |
| Keys | `/.well-known/jwks.json` |
| Authorize | `https://ur.io/authorize` |
| Token | `POST /oauth/token` |
| UserInfo | `GET /oauth/userinfo` |
| Revoke | `POST /oauth/revoke` |

The authorization endpoint sits on a different origin from the issuer on
purpose. The consent screen has to reuse the signed-in session, and browser
storage is scoped to an origin, so the page lives where the session already is.
RFC 8414 permits this and the issuer identity is unaffected — keep validating
`iss` against `auth.bringyour.com`.

Consent is always shown the first time you ask for a given set of scopes, even
when the person is already signed in. Approving a third party silently would
make the page a confused deputy.

## Joining the network

*Planned.*

An OAuth access token authorizes; it does not speak the `connect` protocol. To
take up the place on the network you were granted, exchange it:

```
POST /network/auth-client
Authorization: Bearer <access token>
```

You receive a platform JWT carrying a `clientId` — the address the protocol
knows you by. From there you are an ordinary member of that network, and
`/network/peers` is how you discover what else has been admitted.

Three things surprise people:

1. This is the only crossing between the two credential systems, and it is an
   authorization decision made server-side rather than one token that verifies
   in two places. The signing key sets stay disjoint.
2. Provisioning a client **bills the network**. The consent screen says so; it
   is repeated here so nobody learns it from an invoice.
3. The credential is scoped to the one network bound at authorization. It never
   follows somebody to another network.

When two apps need to know they are serving the same person, the shared handle
is the network they were both admitted to — not the person. That is deliberate:
a network is consented, named on screen and revocable, where a durable user
identifier would be none of those things.

## Tokens

*Planned.*

| Token | Lifetime | Notes |
| --- | --- | --- |
| Access | 1 hour | Audience-bound to one resource |
| Refresh | 90 days, sliding | Opaque and rotating — each use retires the last |
| ID token | 1 hour | Addressed to your `client_id`; never send it to a resource server |

Refresh tokens rotate, and reuse of a retired one is treated as theft: the whole
token family is revoked. Store the newest one you were given.

## Requirements for your app

- **PKCE with `S256`.** Not optional.
- **Exact-match redirect URIs**, registered in advance, HTTPS only. Native apps
  may use `http://127.0.0.1:<port>/…`.
- **Verify `iss`** on the authorization response — this is what defends against
  mix-up attacks.
- **Verify `aud`** on the ID token equals your `client_id`.
- **Do not present a Sign in with URnetwork token to the Operator API.** The key
  sets are disjoint; it cannot work, and that is the design rather than a gap.

## Getting a `client_id`

Issuance is reviewed, not open. An admitted app can address somebody's devices,
so clients are vetted before they can ask for identity or network access.
Dynamic registration stays available for MCP-only scopes, where the reach is a
provider lookup.

There is no self-serve console yet. Ask for a `client_id` and we will issue one.

## Revoking

*Planned.*

People manage grants from the connected-apps screen in the URnetwork app, and
revoking one removes the network clients it provisioned. What your app observes
is the connect session dropping and the `clientId` no longer resolving — a
revocation, not a fault.

Access tokens live an hour, so a revocation takes effect within the hour rather
than instantly.
