CorePass
Start integrating

Authentication: the OAuth 2.0 login flow

How CorePass Connector authenticates a person with OAuth 2.0 and a QR code, what the login page has to contain, and what the token you get back identifies.

7 minute read · Updated 2026-09-05


CorePass Connector authenticates people with OAuth 2.0 and authorizes access to their data with cryptography, and the two halves are independent: you can ship the login on its own and add data requests later. This page is about the first half.

What the connector does #

The connector is an authentication and authorization service. It speaks OAuth 2.0 for authentication, and it uses peer-to-peer, permissionless communication to authorize a person's data. The practical consequence for your application is that a sign-in involves no passwords, no email addresses and no phone numbers: a person on a desktop scans a QR code, a person on a phone taps a link, and the approval happens inside the CorePass application.

Signing in and signing up are the same act. If you want a separate sign-up page — to show a newcomer more context — build it with exactly the same markup; nothing about the flow changes.

The login page #

You design the page. The connector fills in two values: a base64 PNG of the QR code, and the link behind it.

The QR code, for desktop
<img alt="{{.link}}" src=" data:image/png;charset=utf-8;base64,{{.qrcode}}" />
The link, for mobile browsers
<button>
  <a class="link" href="{{.link}}"> Login with CorePass </a>
</button>

Style both however you like. The page as a whole, with the hidden form and the script, looks like this:

login.html
<div class="login-qrcode-container">
  <img
    alt="{{.link}}"
    src=" data:image/png;charset=utf-8;base64,{{.qrcode}}"
    class="login-qrcode-img"
  />
</div>

<button class="login-btn">
  <a class="link" href="{{.link}}"> Login with CorePass </a>
</button>

<form id="login-form" method="POST">
  <input type="hidden" name="challenge" value="{{.challenge}}" />
  <input type="hidden" name="identifier" value="{{.identifier}}" />
  <input type="hidden" name="session" value="{{.session}}" />
</form>

<script src="static/js/login.js"></script>

The two file names are fixed. The login page must be login.html and the sign-up page register.html. Both must include <script src="static/js/login.js"></script>, or the page will never learn that the person approved the sign-in.

login.js #

The script does two things. On load it posts the hidden form to the connector's check endpoint on a five-second interval, and when the connector answers with a destination it sends the browser there — that is the moment the person's approval on their phone becomes a redirect in their browser. It also exposes a small helper that swaps login for register in the current URL, which is how the two pages link to each other. Wire that helper to your own “create an account” link.

The flow, end to end #

  1. The person clicks your sign-in control and is redirected to the CorePass login page you designed.
  2. They scan the QR code, or tap the link if they are already on their phone, and approve the sign-in in the CorePass application.
  3. They are redirected back to your application with a code in the URL.
  4. You exchange that code for a token using the app-auth library.

That is the whole flow. A person who does not yet have a CorePass account installs the application and creates one, and then signs in the same way.

What the token identifies #

The token carries the person's Core ID — their address in the Core ecosystem. That address is what identifies the person in your application, and it is the value every other endpoint on this API expects as user. It is stable, it belongs to the person rather than to you, and it is the only thing about them you are obliged to keep.

Business login #

A merchant that wants to act as a business rather than as a person adds one scope to the authorize request:

Scopes
openid offline offline_access            personal login
openid offline offline_access business   business login

Your OAuth client has to be registered with the business scope. Asking for a scope the client does not hold is refused, not silently downgraded.

The login page then shows a corepass:bizauth/ QR rather than the personal one, and the business's members approve it.

Reading the business claims #

Both the id_token and the access token come back carrying a business block:

Token claims
{
  "sub":        "coreid:<member address>",   // the MEMBER, not the business
  "login_type": "business",
  "business": {
    "id": "2f1c9f4e-…", "name": "…", "domain": "…",
    "verified": true, "status": "VERIFIED"
  },
  "acting_member": {
    "id": "9a1c9f4e-…", "name": "…", "authority": "DIRECTOR",
    "voting_rights": true, "voting_weight": 2, "is_creator": true
  },
  "approved_at": 1787472000
}

sub does not change. It still names the member who acted, not the business they acted for. Reading the business off sub is the mistake to avoid here: sub says who acted, business says who they acted for. Gate on business.verified — an unverified business cannot answer a data request, and status is not a synonym for that flag.

The assertion behind a business login is verified on chain. A business wallet is a contract rather than an account, so what the phone forwards is a bundle of member signatures; CorePass calls isValidSignature on the business's wallet contract at the latest block and requires the magic value. Nothing about that changes what you read — the claims above are unchanged — but two behaviours follow from it:

  • A business login can fail because the member who approved it no longer has voting rights as of the current block, even though they did when the QR was scanned. The roster is read from the chain, not from a cached copy.
  • A Core node outage answers 503 rather than “not signed”, so the phone retries instead of asking the members to approve again. If you are proxying business login, pass the 503 through rather than mapping it to a refusal.

The digest itself is unchanged:

The business login digest
BusinessLogin(
  bytes32 session,
  address business,
  address member,
  string  audience,
  string  authority,
  uint256 issuedAt
)
// domain: CorePassBusinessLogin / 1

The same verification recipe is documented under signature requests, where you have to run it yourself.