Brionic ID

Connect an app

Brionic ID is a plain OpenID Connect provider. If your framework already has an OIDC client, you do not need this page: point it at the discovery document and it will configure itself.

https://id.brionicx.com/.well-known/openid-configuration

Everything below is read from that same document, so the two cannot disagree.

Endpoints

PurposeURL
Issuer https://id.brionicx.com
Authorization https://id.brionicx.com/authorize
Token https://id.brionicx.com/token
User info https://id.brionicx.com/userinfo
Public keys https://id.brionicx.com/.well-known/jwks.json
Sign out https://id.brionicx.com/logout

What it accepts

Getting credentials

Sign in and open Developer to register your own application. You will provide:

A confidential client's secret is shown once, at creation, and only its digest is kept. Nobody can read it back to you afterwards, so store it before you close the window.

The exchange, end to end

1. Send them to sign in

https://id.brionicx.com/authorize  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
  &scope=openid%20email%20profile
  &state=RANDOM_PER_REQUEST
  &nonce=RANDOM_PER_REQUEST
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

Keep state and the PKCE verifier in the session. Check state when they come back, or you have built an open door for someone else's authorization code.

2. Swap the code for tokens

curl -X POST https://id.brionicx.com/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=THE_CODE \
  -d redirect_uri=https://your.app/callback \
  -d code_verifier=THE_VERIFIER

3. Verify the ID token

Fetch the keys from https://id.brionicx.com/.well-known/jwks.json, then check the signature and these claims before you trust anything in it:

4. Ask who they are

curl https://id.brionicx.com/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

sub is the only identifier that is stable. An address can be changed, removed, or moved to another account, so key your users on sub and treat email as a display detail.

Claims you can receive

iss sub aud exp iat auth_time nonce amr azp email email_verified name

amr tells you how they proved who they were on this session, so a sensitive action can insist on a passkey rather than an emailed link. auth_time tells you when, so you can ask them to prove it again.

Before you start, two things that catch people

Signing out

Decide which of these two you mean before you write any of it. Almost every app wants the first, and the second is the one that is easy to reach by accident.

They press sign out in your app Your app Their other Brionic apps Getting back in
Out of your app — start here signed out untouched one press, nothing typed
Out of Brionic ID — only if you mean it signed out signed out as well a full sign-in again

⚠️ Discovery advertises an end_session_endpoint and it reads like the sign-out endpoint, so it is the one people wire the button to. It is the second row. Choosing it means somebody who signs out of your app also loses the tab they had open in another one, which they experience as being logged out of things they never touched, by an app they were not using.

Unless you can say why this app is the exception, build the first and offer the second somewhere separate, worded so nobody presses it by accident.

Out of your app, which is what you usually want

Destroy your own session and stop. Do not send them here. They stay signed in to Brionic ID, so when they come back and press your sign-in button they are returned to you straight away without typing anything.

That is single sign-on working, not a bug, though it does surprise people the first time. If the point is to hand the device to someone else, use the second kind.

Out of Brionic ID, everywhere

Worth offering on a shared or borrowed machine, and worth naming as such: “Sign out of everything” rather than “Sign out”. Send them to https://id.brionicx.com/logout with id_token_hint and, if you registered one, a post_logout_redirect_uri. This ends the session itself, so it signs them out of every other Brionic app as well.

Send the id_token_hint. With a valid one we know which app is asking and end the session immediately; without it we have to ask the person to confirm first, because otherwise any page on the internet could sign them out by linking here.

The post_logout_redirect_uri has to be one you registered, matched in full. An address we do not recognise is ignored rather than refused, and the person simply lands on our page instead of back on yours, which is the failure people spend an afternoon on. A state value is handed back on the end of it.

Being told when a session ends elsewhere

Register a logout endpoint and we will POST a signed logout_token to it whenever a session you hold a grant from ends, whether from the account page, another app, or an operator. You are told about any sign-in you were given tokens from — offline_access is not required and never was relevant to this. Four steps, and the first is the one people miss.

⚠️ If you have not registered an endpoint, you get nothing. There is no error and no warning anywhere: sign-out simply has nowhere to reach you, and the person stays signed in to your app after signing out here. Check yours on the applications page before assuming this works.

  1. At sign-in, keep the sid from the ID token on your own session record, beside the sub. Skip this and nothing else here can work: you will be told that a session ended and have nothing to match it against.
  2. Register your logout endpoint against your application, on the applications page, so we know where to send it. The same page is where a post_logout_redirect_uri goes.
  3. Check the token before you act on it. It arrives as a form field named logout_token on an unauthenticated public endpoint, so it is the checks below that make it trustworthy and nothing else.
  4. End the session whose sid matches, and only that one. Reply 200. A body is not read.

⚠️ Prove it rather than assume it. Sign in to your app, sign out of Brionic ID from the account page, then reload your app: you should be signed out. A receiver that was never reached looks exactly like a receiver that works, because the only visible symptom is a session quietly staying open.

⚠️ An endpoint that skips step 3 lets anyone who finds the URL sign your users out at will. Verify all of it:

signature   verifies against https://id.brionicx.com/.well-known/jwks.json
iss         https://id.brionicx.com
aud         your client id
typ         logout+jwt
events      contains "http://schemas.openid.net/event/backchannel-logout"
nonce       MUST BE ABSENT — its presence means you were handed an ID token
iat         recent, and the same jti twice is a replay

Your sid is yours alone. Two apps are given different values for the same visit, so neither can use it to work out it is looking at the same person as the other.

⚠️ One attempt, five seconds, no retry. If your endpoint is slow, down, or behind a deploy, that notice is gone and we will not send it again. Treat it as a prompt to end a session early, never as the thing your security depends on: keep your own sessions short enough that one lost message does not matter.

If a token arrives with no sid, it belongs to a grant made before we issued them. Ending every session for that sub is the safe reading, but do not make it your normal path: it signs the person out of your app on every device they own because they signed out of one, which is the behaviour that makes people think sign-out is broken in the other direction.

An access token already issued keeps working until it expires, which is minutes rather than hours. Nothing can recall one that is already in someone's hands, so treat sign-out as ending the ability to get new tokens rather than as an instant off switch.

Sign in with Apple

You do not need an Apple Developer account, a Services ID, or a signing key. We hold the relationship with Apple and carry the cost of maintaining it, so putting a real Sign in with Apple button on your page is one link.

<a href="https://id.brionicx.com/authorize?client_id=YOUR_CLIENT_ID
         &redirect_uri=https%3A%2F%2Fyour.app%2Fcallback
         &response_type=code&scope=openid%20email%20profile
         &code_challenge=…&code_challenge_method=S256
         &state=…&idp=apple">Sign in with Apple</a>

It is an ordinary authorization request with idp=apple on the end. The person goes straight to Apple, and comes back to your callback the way every other sign-in does. Nothing else about your integration changes.

If you ship an iOS app, read this part

Three things about Apple that surprise people

The sub you receive is ours and is stable for that person in your app, so none of Apple's own identifiers reach you and nothing changes if we ever alter how we talk to Apple. Signing in with Apple and signing in with Google makes two separate accounts, deliberately: an address alone never links anything here, and it is the person who joins them from their account settings.

The sign-in button

We do not render this — it lives on your page, so none of it is enforced. It is here because you need one, and building it yourself means measuring contrast for a mark you did not draw.

⚠️ The mark is inlined into the markup, not loaded from us. Hotlinking it would put our uptime in the critical path of your login page, and would tell us who was looking at it before they had chosen to sign in. Inline SVG needs no request, no CORS and no change to your img-src.

Themes

Pick one. Each is shown on the kind of page it is meant for.

Continue with Brionic ID
Light bid-btn on its own The default. An outline on a white or near-white page.
Continue with Brionic ID
Soft bid-btn-soft Filled, for light pages where an outline reads as too quiet.
Continue with Brionic ID
Dark bid-btn-dark For dark pages.
Continue with Brionic ID
Contrast bid-btn-contrast Near-black, for interfaces whose primary button is black.

There is no brand-blue filled theme, and that is a measurement rather than a taste. The mark carries its own steel field, so a button filled with the same blue leaves it at 1.6:1 against its own background and the silhouette dissolves. Going dark enough to separate arrives at bid-btn-contrast, which is already in the list.

Shape and width

Both combine with any theme above.

Continue with Brionic ID
Pillbid-btn-pillFor interfaces with fully rounded controls.
Continue with Brionic ID
Full widthbid-btn-blockFor stacked sign-in forms, and the answer to a label that will not fit.

Markup

Point href at whatever route in your app starts the exchange, and add a theme class beside bid-btn if you want one.

<a class="bid-btn" href="/auth/brionic-id">
  <svg width="20" height="20" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
    <defs>
      <linearGradient id="bidbtn-field" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0" stop-color="#4682b4"/><stop offset="1" stop-color="#1b3648"/>
      </linearGradient>
      <radialGradient id="bidbtn-ball" cx=".34" cy=".3" r=".75">
        <stop offset="0" stop-color="#ffffff"/><stop offset=".5" stop-color="#dbe8f2"/><stop offset="1" stop-color="#7f9db3"/>
      </radialGradient>
      <radialGradient id="bidbtn-core" cx=".34" cy=".3" r=".75">
        <stop offset="0" stop-color="#fdf0c0"/><stop offset=".45" stop-color="#e8bf4c"/><stop offset="1" stop-color="#9c7405"/>
      </radialGradient>
    </defs>
    <rect width="64" height="64" rx="14" fill="url(#bidbtn-field)"/>
    <g fill="none" stroke="#cfe0ee" stroke-width="5" stroke-linecap="round">
      <path d="M35.42 22.6 38.16 15.08"/>
      <path d="M38.43 39.66 43.57 45.79"/>
      <path d="M22.15 33.74 14.27 35.13"/>
    </g>
    <g fill="url(#bidbtn-ball)">
      <circle cx="38.16" cy="15.08" r="8"/>
      <circle cx="43.57" cy="45.79" r="8"/>
      <circle cx="14.27" cy="35.13" r="8"/>
    </g>
    <circle cx="32" cy="32" r="10.5" fill="url(#bidbtn-core)"/>
  </svg>
  Continue with Brionic ID
</a>

Styles

Self-contained, so it inherits nothing from your stylesheet or ours. This is read straight out of the stylesheet this page is using, so the two cannot disagree.

.bid-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    min-height: 44px;
    padding: 0.7rem 1.15rem;
    font: 600 0.95rem/1.2 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    color: #0d1c27;
    background: #ffffff;
    /* 3.85:1 against white. The border is the only thing describing the
       button's edge, so it has to meet non-text contrast on its own. */
    border: 1px solid #6d8598;
    border-radius: 0.6rem;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}

.bid-btn:hover {
    background: #f4f9fc;
}

.bid-btn:focus-visible {
    outline: 2px solid #1f6193;
    outline-offset: 2px;
}

.bid-btn svg {
    flex: none;
}

/* Themes. Deliberately classes and not prefers-color-scheme: the host page may
   be light while the visitor's system is dark, and a button that followed the
   system would go black on a white page. Only the app knows which surface it
   put us on.

   ⚠️ Each theme's edge was measured against the page it is meant for, not
   against its own fill. On the pale themes the border is the only edge, so it
   carries the contrast; on the dark themes the fill already separates and the
   border is there for dark pages. Both pale themes use the same border for
   that reason, even though it looks light against the soft fill. */

/* Filled, for light pages that want more presence than an outline. */
.bid-btn-soft {
    background: #e4ecf3;
}

.bid-btn-soft:hover {
    background: #d3e0ea;
}

/* For dark pages. */
.bid-btn-dark {
    color: #e6eef5;
    background: #10242f;
    border-color: #557f99;
}

.bid-btn-dark:hover {
    background: #16303d;
}

.bid-btn-dark:focus-visible {
    outline-color: #79bce8;
}

/* Near-black, for interfaces whose primary button is black. */
.bid-btn-contrast {
    color: #ffffff;
    background: #0d1c27;
    border-color: #5a7183;
}

.bid-btn-contrast:hover {
    background: #16303d;
}

.bid-btn-contrast:focus-visible {
    outline-color: #79bce8;
}

/* Shape and width, combinable with any theme above. */
.bid-btn-pill {
    border-radius: 999px;
}

/* Sign-in forms usually stack full-width controls. This is also the answer to a
   label that will not fit: give it the width, rather than shrinking the text. */
.bid-btn-block {
    display: flex;
    width: 100%;
}

The mark, the palette and the rules for using it are on the brand page.

That is the whole protocol surface

The discovery document is the reference. This page only explains it.

Open the discovery document