"Sign in with iVendNext" — for a partner app
This recipe lets the users of a partner app outside the shop — a web page, a mobile app, a portal — sign in with their own iVendNext account. Every call the app then makes runs with that person's permissions and nothing more. It uses the standard authorisation code flow with PKCE (S256). The reference is tools/sign_in/signin_flow.py (Python, standard library only).
When to use it: your app acts for a named person in the shop, not for a shared integration user.
1. The shop registers your app (once)
The shop's administrator creates an OAuth Client for you, with:
- the app's name;
- your exact redirect address(es);
- grant type Authorization Code, response type Code;
- scopes
openid all.
They give you its client id. Optionally, Allowed Roles limits which of the shop's users may sign in to your app. A client secret is also made — see §4 for why you must not rely on it.
2. The flow
- Your app makes a random verifier (43–128 characters) and its challenge = base64url(SHA-256(verifier)), and a random state. Keep both on your side for this sign-in.
- Send the person's browser to
https://<shop>/api/method/frappe.integrations.oauth2.authorizewithclient_id,response_type=code,redirect_uri,scope=openid all,state,code_challenge,code_challenge_method=S256. - The person signs in to the shop with their own account. A person not yet signed in is sent to the shop's own sign-in page first. The shop then shows an Allow / Deny page naming your app.
- On Allow, the shop redirects to your redirect address with
codeand the samestate. Refuse the callback ifstateis not the one you sent. - Your server exchanges the code at
…/frappe.integrations.oauth2.get_token(form fields:grant_type=authorization_code,code,redirect_uri,client_id,code_verifier). You get anaccess_token(and arefresh_token). - Every call then carries
Authorization: Bearer <access_token>and runs as that person.…/frappe.integrations.oauth2.openid_profilenames them and their roles.
3. What is refused, and why
The shop refuses each of these, so a replayed code or a revoked token gets nothing (section 4: always use PKCE):
- a code exchanged with a wrong verifier, or no verifier — even with the right client secret;
- a code used a second time (a code works once);
- a redirect address your app did not register (it gets no code);
- a token after it was revoked;
- a call for something the person may not read (a 403).
4. ⚠ Always use PKCE — the client secret is not checked
On this build the token endpoint does not check the client secret. So for a partner app, PKCE is the only thing that makes a stolen code useless. Use it for every sign-in, from every kind of app. Treat the client secret as an identifier, not a credential. Until the token endpoint checks it, rely on PKCE alone.
5. Your duties
- Keep tokens on your server, never in a page's storage; refresh them there.
- Ask for the person's consent on your side for anything you store about them.
- Revoke the token when the person signs out of your app (
…/frappe.integrations.oauth2.revoke_token, form fieldtoken). - Use the shop's address over HTTPS only, and follow no redirect with a token.
Technical notes
Proven on our test bench on 2026-10-04 (run pp-20261004T111824Z-95458): 15 checks, every one passing, and one measurement (§4). The table groups the 15 checks into nine rows.
What was proven on the bench
| Checks | Result | |
|---|---|---|
| A guest is sent to the shop's own sign-in page first, and signs in to their own account | 2 | ✅ |
| The Allow/Deny page is shown; Allow returns a code and the same state to the registered address | 2 | ✅ |
| Code + verifier → a token, no client secret sent | 1 | ✅ |
| The token acts as that person; the profile names them and their roles (a Retail Cashier) | 2 | ✅ |
| …reads what they may read (items) and is refused what they may not (the error log: 403) | 2 | ✅ |
| A revoked token works no more | 1 | ✅ |
| A code exchanged with a wrong verifier, with no verifier, or with no verifier and the right client secret, is refused | 3 | ✅ |
| A code works once | 1 | ✅ |
| A redirect address the app did not register gets no code | 1 | ✅ |
The measurement behind §4
On this build a code issued without PKCE was exchanged with a wrong secret and a token came back.
Bounds, stated
- The bench is reached over plain HTTP on the programme's own network; a real shop is HTTPS.
- The person's browser was played by a script (signing in with the persona's password, pressing Allow); the pages themselves were not looked at by a person.
- The refresh token was issued but not exercised. Allowed Roles was exercised later, by S13 (
recipes/portal.md): a user outside the roles is stopped at the shop's Allow step.