Skip to content

Alberta · prescription r.kaur@clinic.ca · owner

Patient
D. Whitecalf · 1987-04-02
Drug
Amoxicillin 500 mg · capsule · 21
Directions
One capsule by mouth three times daily for seven days

Prescriber signature

clinical.write refused — no linked provider profile

Security

She owns this clinic. The software will not let her sign.

Owner is the highest role there is here. It bypasses every permission check in the product — except one. Issuing a prescription and signing an encounter are not granted by a role at all; they are granted by a linked provider profile, and that test is written above the owner bypass, so it answers first and the bypass is never reached. An owner who is also the physician links their profile and signs. An owner who runs the business and does not see patients never can.

Nobody can turn that off, because it is not a setting. It is four lines near the top of one function, and everything else on this page has the same shape.

What the menu does not do

Front desk cannot see four of these. They can still type the address.

Sidebar · front desk

  • Today’s snapshot/Todays-Snapshot.aspx
  • Calendar/Calendar_View.aspx
  • Patients/Patient_List.aspx
  • Revenue report/Revenue_Report.aspx
  • Messages/Messages.aspx
  • Payroll/Payroll.aspx
  • Patient insurance/Patient_Insurance.aspx
  • Users & roles/Users_Roles.aspx
  • Activity log/Activity_Log.aspx

Four of those are never rendered for this role. That is the whole of what the menu contributes, and it is presentation.

So a front-desk session types the last one into the address bar, because addresses are not a secret. Here is everything that happens next.

  1. 01 GET /Activity_Log.aspx The page loads. Nothing is hidden any more — the reader is on it.
  2. 02 RequirePermissionForPage(audit.view) First statement in Page_Load, before a single control renders. Front desk does not hold it.
  3. 03 auth.permission_denied · Page A row is written first: Denied ‘audit.view’ at /Activity_Log.aspx. Refusing quietly would leave nothing for you to find later.
  4. 04 302 → /Todays-Snapshot.aspx?denied=1 Redirected to their own landing page, and the response is ended there. Not hidden. Refused.

Then the interesting one. Every page in this product talks to its own server methods over AJAX, and those have addresses too — so guarding the page and leaving the endpoint open is the classic way to build a lock with no back panel. Each of those methods carries its own gate, CheckWebMethodAccess, and it writes the same audit row with one word changed: WebMethod instead of Page. Which door somebody tried is a thing you can read afterwards.

Hiding a sidebar link is presentation; these calls are the security. — ClinicAuthService.vb, in its own header

The front door

Two locks are counting. Which one catches you depends on what you are doing.

Every clinic account has a lockout after five wrong passwords. That is the obvious lock, and on its own it is close to useless against the attack people actually run: one common password tried once against a hundred different accounts, which never reaches five on any of them. So there is a second count, and it is not counting accounts.

  1. A One account, guessed again and again

    Five failed sign-ins against a single account.

    Per account
    5 of 5 — locked, 5 minutes
    Per address
    5 of 15 — still open
  2. B Fifteen accounts, one guess each

    Fifteen failed sign-ins, each against a different account, from one address.

    Per account
    1 of 5, fifteen times over — never trips
    Per address
    15 of 15 — blocked, 15 minutes

Fifteen failures from one public address inside ten minutes closes that address for fifteen minutes, whoever it was aiming at. Neither lock covers the other’s case, which is why both are there and why the thresholds are different numbers rather than one number used twice.

Every one of those failures is also a row. auth.login_failed while the count is climbing, auth.login_lockout at the moment it trips — so the pattern is readable afterwards instead of being something you infer from a quiet afternoon.

What a failure tells you

Three different mistakes. One answer, word for word.

  1. 01 That address has never been used here.
  2. 02 The address exists. The password is wrong.
  3. 03 The address exists, and it is a patient account rather than a clinic one.

“Invalid login attempt.”

The software knows which of the three it was. It writes that down. It does not say it.

An answer that told them apart would be a free account checker. Walk a list of addresses through a login form, read which ones come back differently, and you have learned who works at the clinic without guessing a single password. That is worth more to somebody than the passwords are.

The third one is the interesting fix. “This account is not a clinic account” used to be decided before the password was checked, which made the login form answer a question nobody had earned the right to ask. It now waits until the password has actually succeeded — and then signs you straight back out. You have to already know the password to learn anything at all.

One answer is deliberately different

A locked account is told it is locked, in its own words. Somebody guessing did the locking, so it tells them nothing they did not already do — and the person it matters to is the one staring at a form that has stopped working. Uniformity is worth a lot; it is not worth more than that.

And when something breaks rather than being refused, the same rule holds. There is no exception text on the screen, no stack, no provider message — the whole thing goes to the log and what comes back is one sentence and an eight-character reference like 4F2A9C71. Quote it to us and we can find the exact request. Nobody else learns anything from it, which is the entire idea.

When you take somebody’s access away

The next thing they click is refused. The cookie takes up to half an hour.

14:02 — you set their membership to disabled 14:32

Their next request
The page guard reads dbo.ClinicAdmins on every request. A disabled membership fails every permission check, is written to the audit log, and is signed out. Nothing is cached across requests to make that faster.
The identity cookie
Re-validated on a thirty-minute interval, so the underlying sign-in can survive that long. It buys nothing — every page it reaches has already refused it — but half an hour is the honest number and we would rather print it than round it to zero.

The reason those are different numbers is that they are different mechanisms. One is a question asked of the database every time a page loads; the other is a signature on a cookie with an interval on it. Most products describe only the second and let you assume the first.

And what this page does not claim

Two-factor sign-in
Not available. The framework’s two-factor middleware is wired up and the login flow has a branch for it — and that branch redirects to a page that has not been built. Plumbing without a tap is not a feature, and this is the largest single gap on this page.
Certification
None. SOC 2 is in preparation and that is the whole of the status; it is what the trust strip at the foot of every page says and this page does not improve on it. Nobody has audited us against anything yet.
Encryption beyond the platform
Records are held in Azure SQL in Canada Central, with the encryption that platform applies at rest and in transit. There is no column-level or application-layer encryption in the source, so there is none claimed here. If that matters to your risk assessment, ask before you sign, not after.
Anything you cannot check
Every number on this page — five, five minutes, fifteen, ten minutes, fifteen minutes, thirty minutes, ten characters — is a constant in a file, not a target. Ask us to show you the line.

Security on a clinic system is not a badge and it is not a page like this one. It is whether the rule survives the afternoon somebody is in a hurry — which is why the ones described here live in code that runs before anything renders, and not in a setting anyone can be talked into changing.