Real bidders¶
Caution
So far the bidder is just a name you type. You can bid as "Your Boss", as "nobody yet", or as anyone else, so the name proves nothing. A real auction needs the edge to know who is bidding.
Goal: people sign in, each bid carries their real identity, and only signed-in users can bid. Anyone can still watch.
Step 1: Add authentication¶
One command sets up secure sign-in:
It writes an identity section into synqt.yaml with secure defaults (the login flow
runs on the edge, the browser never holds a secret, the session is a hardened cookie),
lists the secret it needs in .env.example, and scaffolds an identity mapping hook. Two
steps are yours: register the app with GitHub, and store the secret.
First, register a GitHub OAuth app. On GitHub, open Settings, then Developer settings, then OAuth Apps, then New OAuth App. Fill in:
- Application name: anything, for example
Gavel (dev). - Homepage URL: the address
synqt devprinted,http://127.0.0.1:8080unless you passed--port. - Authorization callback URL: the same address followed by
/auth/callback, sohttp://127.0.0.1:8080/auth/callback. It must match exactly,127.0.0.1included: GitHub treatslocalhostas a different host and refuses a callback registered for the other one.
Click Register. GitHub shows a Client ID and a button that generates a Client secret.
Second, store the two values. The Client ID is not secret, so it goes in synqt.yaml,
in the provider entry synqt add auth created:
The Client secret goes only in web/edge/.env, which only the edge reads and git
ignores:
Caution
The Client secret never goes in synqt.yaml, in any file under client/, or anywhere
the browser can reach. It lives only in web/edge/.env, on the edge. SynQt refuses to
build if a secret is wired anywhere the client could see it, but do not rely on that
safety net.
Note
The command turns on every protection at once (PKCE, a secure cookie, the secret kept on the server), because someone eventually forgets an optional safety control. Every setting that works is also secure. For everything it turned on, see authentication.
Step 2: Use the real identity instead of a typed name¶
Now the edge knows who is calling, so the bidder should come from that identity, not
from a text field. Change the edge's export: in synqt.yaml, and state on the member
who may reach it:
<user> is the gate. A caller without that scope does not have the member, and the edge
refuses the call before your function runs, so you write no check in the QML and cannot
forget one. Update web/edge/Edge.qml to use the caller's identity:
function placeBid(amount) {
if (amount <= auction.highBid) {
Caller.emitBidRejected("Your bid must beat " + auction.highBid + ".");
return;
}
auction.highBid = amount;
auction.highBidder = Caller.identity.name; // their real name, from sign in
}
Note
Scopes are your app's permission levels (by default anonymous, user, moderator,
admin), and <user> on a member means "at least a signed-in user". The function keeps
only the decision the topology cannot make: whether this bid is high enough.
Caller.identity is the authenticated profile. It comes from the login the bidder
completed, so they cannot type it.
Step 3: Update the UI for sign in¶
In client/app/Main.qml, replace the bidding row and add sign-in. The view shows a Sign
in button to anonymous visitors and the bid controls only to signed-in users:
RowLayout {
spacing: 8
visible: !Session.hasScope("user")
Button {
text: "Sign in to bid"
onClicked: Session.login()
}
}
RowLayout {
spacing: 8
visible: Session.hasScope("user")
Label { text: "Signed in as " + (Session.identity ? Session.identity.name : "") }
TextField {
id: amountField
placeholderText: "Amount"
inputMethodHints: Qt.ImhDigitsOnly
}
Button {
text: "Place bid"
onClicked: {
Server.placeBid(parseInt(amountField.text));
amountField.clear();
}
}
}
Session is the browser's read only view of who you are. Session.login() starts the
sign-in flow, and Session.hasScope("user") is true once you are signed in.
Step 4: Run it¶
Save and look at the browser. It shows "Sign in to bid". Click it and complete the GitHub login. You come back signed in, with your name shown and the bid box available. Bid, and your real name holds the high bid.
Try it, then think¶
Question
Does hiding the bid controls stop signed-out people from bidding? Sign out (or open a private window), open the browser's developer console, and run:
Predict what happens before you press Enter.
Solution
The edge rejects the bid, and the standing bid stays where it was.
Hiding the controls only removed the button from view. A visitor can still call the
slot directly, as you just did. The <user> gate on placeBid stopped the bid; the edge
applies it before the function runs.
This is the lesson of the base case again, for permissions:
the owner authorizes every call, against Caller. Showing or hiding a control on the
client is never the security boundary. SynQt's security model rests on this; see
security.
Bonus: an auctioneer who can close a lot¶
Give one person, the auctioneer, the power to close the current lot and put up the next one. This uses a higher permission level, admin.
Add this to the edge's export:, gated one level higher:
The <admin> on the member already decides who may call it, so the function in
web/edge/Edge.qml only does the work:
function closeLot(nextItem) {
// (A later part records the winner here before resetting.)
auction.itemName = nextItem;
auction.highBid = 0;
auction.highBidder = "nobody yet";
}
Make yourself the auctioneer by mapping your identity to the admin scope. Open
web/edge/identity/map.qml (scaffolded by synqt add auth) and return Scope.Admin
for your own account:
import SynQt
IdentityMapping {
readonly property var auctioneers: ["your-github-username"]
function scopeFor(identity): int {
if (auctioneers.indexOf(identity.login) !== -1) {
return Scope.Admin;
}
return Scope.User; // everyone else who signs in
}
}
Scope is generated from scopes.order in your synqt.yaml, so Admin exists because
admin is declared there. If you misspell it, synqt check names the missing member and
lists the valid ones, so you do not find out when somebody signs in and can reach
nothing.
Add an auctioneer control to client/app/Main.qml, visible only to admins:
RowLayout {
spacing: 8
visible: Session.hasScope("admin")
TextField { id: nextItemField; placeholderText: "Next item" }
Button {
text: "Close lot"
onClicked: Server.closeLot(nextItemField.text)
}
}
Note
The mapping keys on identity.login (the GitHub username). identity.sub (the stable
id) works too. identity.email does not: a GitHub account that keeps its email private
may expose no address even after sign-in. The identity fields are defined in
authentication.
Sign in as yourself and you can close the lot and start the next one. The edge refuses
anyone else who tries, including from the console with closeLot.
What you learned¶
synqt add authsets up secure sign-in in one step, with no insecure setting.- Identity comes from a real login, through
Caller.identity, and the caller cannot fake it. - Authorization is per member: you write
<scope>on the member, and the edge applies it before your code runs. The slot keeps only the judgement the topology cannot make, againstCaller. - Scopes are permission levels. An admin can do what a user cannot.
- Hiding controls in the UI is a courtesy, not security.