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.
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.
<img alt="{{.link}}" src=" data:image/png;charset=utf-8;base64,{{.qrcode}}" /> <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:
<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 #
- The person clicks your sign-in control and is redirected to the CorePass login page you designed.
- 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.
- They are redirected back to your application with a code in the URL.
- 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:
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:
{
"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:
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.