HTTP: sign-in (reference)
All sign-in methods of the service with their rules and responses, self-service for your own keys, and the sign-in policy with the second factor. The model behind it is explained in Sign-in and identity pass-through.
Syntax
| Item | Meaning |
|---|---|
X-Api-Key: <service key or personal key> | first factor; header name per Security:ApiKeyHeader |
Authorization: Negotiate … | Windows account |
Authorization: Basic … | SQL Server login, or personal key as password |
X-Wkl-Session: <session> | second factor, in addition to the first |
X-Wkl-Role: <identity> | choice of the database identity |
POST /auth/v1/totp[/{alias}] | code for session |
POST /auth/v1/enrol[/{alias}] | set up the second factor at first sign-in |
GET /self/v1/[{alias}/]keys | self-service: own keys |
POST /self/v1/[{alias}/]issue-key | self-service: issue a key |
POST /self/v1/[{alias}/]revoke-key | self-service: revoke a key |
Parameters
Sign-in with the Windows account
Besides the service key, a caller can sign in with their Windows account, via HTTP Negotiate—Kerberos or, where Kerberos isn't available, NTLM. This is intended for people: Access via the ODBC driver, and Excel or Power BI, sign in as the user who runs them. No key or password is stored anywhere, and the audit names the person instead of a key name.
"Security": {
"AuthMode": "ApiKey, Windows",
"ApiKeys": [ { "Client": "fachanwendung", "Key": "env:FACHANWENDUNG_SCHLUESSEL" } ],
"WindowsAccounts": [
{ "Account": "CORP\\BusinessData-Readers", "Client": "fachdaten-lesen" },
{ "Account": "CORP\\BusinessData-Editors", "Client": "fachdaten-pflege" },
{ "Account": "S-1-5-32-545", "Client": "auskunft" }
],
"Clients": [
{ "Id": "fachanwendung", "Databases": [ "fachdaten" ], "Resources": [ "*" ], "Verbs": [ "*" ] },
{ "Id": "fachdaten-lesen", "Databases": [ "fachdaten" ], "Resources": [ "views" ], "Verbs": [ "GET" ], "ReadOnly": true },
{ "Id": "fachdaten-pflege", "Databases": [ "fachdaten" ], "Resources": [ "*" ], "Verbs": [ "*" ] },
{ "Id": "auskunft", "Databases": [ "fachdaten" ], "Resources": [ "views" ], "Verbs": [ "GET" ], "ReadOnly": true }
]
}
| Rule | Reason |
|---|---|
AuthMode lists the methods, comma-separated: ApiKey, Windows, SqlLogin, PersonalKey in any combination | Services keep their key, people sign in with Windows or with a SQL Server login |
An entry under WindowsAccounts gives an account or a group the rules of a caller under Clients | Rights are attached to groups, as elsewhere in Windows; a person gets them through their membership |
| The rights of several entries add up; a read-only rule doesn't restrict another one | That's how groups work everywhere in Windows. Anyone who should only read doesn't belong in a group that may write |
| Comparison is by SID; a name is translated into it at startup | A renamed group keeps its rights. A name that doesn't exist prevents startup instead of silently granting nothing |
Specify built-in groups by their SID (S-1-5-32-545 instead of VORDEFINIERT\Benutzer, the German name of BUILTIN\Users) | Their names depend on the language of the Windows installation. A SID is checked only for its form |
| A service key sent along decides alone | An explicit proof isn't overruled by an implicit one; a wrong key results in 401, even if a Windows sign-in comes along |
| A signed-in person without a matching entry reaches nothing, like a key without a rule | The audit names who tried it. A 401 would make Office ask again for credentials that the person has long since presented |
| Groups that Windows lists only for denying don't count | For example, the Administrators group of a non-elevated user; counting it would grant what Windows withholds |
Without sign-in, the service responds with 401 and WWW-Authenticate: Negotiate; only then does a
client sign in with Windows. Every new connection therefore begins with a request without sign-in,
which appears in the audit as anonym … UNAUTHENTICATED (401). That's the first step of the method, not an
error. After that, the sign-in applies to the whole connection.
Kerberos requires an SPN HTTP/<host-name> for the account under which the service runs; if it's missing,
Windows falls back to NTLM.
Sign-in with a SQL Server login (HTTP Basic)
If you have been reaching SQL Server with a name and password, you sign in to the service with the same
login: via HTTP Basic, that is, with name and password in the Authorization header. The service doesn't
check the password itself and doesn't store one. It opens a connection to the server of the addressed alias
with the name and password, and SQL Server decides, as with a direct connection. All statements of the
request then run over connections of this login: permissions, column defaults and triggers see the login
itself, including ORIGINAL_LOGIN().
"Databases": [
{ "Name": "fachdaten", "Provider": "sqlserver", "ConnectionString": "…",
"AllowSqlLogins": true }
],
"Security": {
"AuthMode": "ApiKey, Windows, SqlLogin",
"SqlLogins": [
{ "Login": "*", "Client": "fachdaten-lesen" },
{ "Login": "pflege1", "Client": "fachdaten-pflege" }
],
"TrustedProxies": [ "10.0.0.5" ]
}
| Rule | Reason |
|---|---|
AuthMode contains SqlLogin, and an alias accepts SQL Server logins only with AllowSqlLogins | Which database checks the name and password is a decision per alias. An alias without the switch responds to a SQL Server login as to an unknown one (401); the response doesn't reveal which aliases exist |
| Server and database come from the alias's connection string; name and password only replace its login | The caller doesn't choose the database, as everywhere in the service |
An entry under SqlLogins gives a login (Login: its name, or * for every login) the rules of a caller under Clients; the rights of several entries add up | The rules remain the outer barrier: which aliases, resource types and verbs can be reached over HTTP. Inside, the login's rights in SQL Server decide |
| The check happens before rules and catalog are consulted | Otherwise someone who knows a name but not the password could tell from the responses which objects exist |
Only over a protected connection: HTTPS, a proxy from TrustedProxies that reports X-Forwarded-Proto: https, or a request from the same computer. Otherwise 403, without trying the password | Basic sends the password with every request. The X-Forwarded-Proto of an arbitrary client doesn't count, and a trusted proxy on the same computer doesn't turn a request from outside into a local one |
At most 5 failed attempts per login and alias, and 20 per origin, within one minute; then 429, even with the correct password. An attempt the server gives no readable answer to (503) also counts as failed | SQL Server counts failed attempts only for logins with a password policy, and the service's rate limiting applies only after sign-in. Even without a readable answer, the server may have checked the password |
| The password appears neither in the log nor in the text form of the caller | It's held only in the request's memory and in the connection pool; SqlClient keeps a separate pool per login |
A service key sent along takes precedence over Basic, Basic over Windows; a rejected password results in 401 without falling back to Windows | An explicit proof isn't overruled by an implicit one |
Without sign-in, the 401 response names every enabled method: WWW-Authenticate: Negotiate and WWW-Authenticate: Basic realm="Wkl.Data.Gateway", charset="UTF-8" | A client picks what it can do; charset tells it that name and password are read as UTF-8 |
| Response | Meaning |
|---|---|
401 “Es wurde keine gültige Anmeldung vorgelegt.” (No valid sign-in was presented.) | Name or password wrong, login unknown or disabled, or the alias doesn't accept SQL Server logins. The response deliberately doesn't distinguish these; the log names the SQL Server error number (such as 18456) |
403 “SQL-Anmeldungen nimmt der Dienst nur über eine verschlüsselte Verbindung an.” (The service accepts SQL Server logins only over an encrypted connection.) | The password came unprotected from another computer |
403 “Diese Anmeldung hat keinen Zugang zur Datenbank des Alias.” (This login has no access to the alias's database.) | SQL Server accepted the login but didn't open the alias's database (4060) |
403 “Die Datenbank verweigert dieser Anmeldung den Vorgang.” (The database refuses this operation for this login.) | The login lacks a permission (error 229, 230, 262, 297, 300 or 916) |
429 “Zu viele fehlgeschlagene Anmeldungen; bitte später erneut versuchen.” (Too many failed sign-ins; please try again later.) | The limit for failed attempts has been reached |
Personal keys
A personal key belongs to a person, not to a service. It requires no domain, no SQL Server password
and no infrastructure, and so it works everywhere the service can be reached. People and keys are kept in
the administration store; the method needs it
and is turned on with PersonalKey:
"Security": { "AuthMode": "ApiKey, PersonalKey" },
"Store": { "Path": "store/dbgwsvc.db", "MasterKey": "env:WKL_GW_MASTER_KEY", "MaxKeyLifetimeDays": 365 }
The key goes into the same header as a service key (X-Api-Key, see ApiKeyHeader). Every client
that can send a service key—the ODBC driver with APIKEY/APIKEYENV, a script, a reporting tool—sends
a personal one without any change. The prefix distinguishes the two. A personal key also works as the
password of HTTP Basic (the name isn't checked; pkuser is recommended, see below): Excel and Power BI
store such credentials (authentication kind “Basic”) in their own credential management, whereas a header
would have to be written into the query—and therefore into every workbook. The form of the key distinguishes it from the password of a SQL
Server login.
<a id=“user-name-pkuser”></a>User name pkuser (convention). If you enter a personal key as the
password, enter pkuser as the user name (personal key user). This isn't required: the service
recognizes the key by its form (wkl_…), not by the name, and accepts any name. A uniform name still has
advantages:
- In the credential managers of Excel, Power BI and Windows, you can see at a glance that this entry holds a personal key and not a SQL Server login.
- On the phone, “user name
pkuser, password your key” is enough, without explaining which name is meant. - Nobody enters the name of another system and then mistakes it for part of the access.
Don't create a pkuser login in SQL Server: the service checks a password that doesn't have the form of a
key as a SQL Server login of this name.
| Property | Definition | Reason |
|---|---|---|
| Form | wkl_ + 43 random characters (256 bits, letters and digits) + 6 check characters, 53 characters in total | A secret scanner finds a fixed prefix and length in source code and logs; the check characters detect a typo without a lookup |
| Storage | only the SHA-256 of the key, a public ID (k + 8 characters) and the last 4 characters | A copy of the file opens no request. With 256 bits of randomness, SHA-256 is enough; a slow password hashing method would cost time on every request and protect nothing |
| Display | once, when issued | After that, only the holder has it |
| Rights | the rules (Clients) assigned to the person; the key restricts and never grants anything itself: aliases, read-only, networks (From/ExceptFrom) | A key for Excel on someone else's computer gets only what it needs, without its own rule |
| Lifetime | expiry date per key; if not specified, the maximum lifetime Gateway:Store:MaxKeyLifetimeDays (default 365, 0 = no expiry); longer is rejected | A forgotten key on an old computer stops working on its own |
| Revocation | revoking a key or disabling a person takes effect from the next request, without a reload and without a restart | The key is looked up in the store on every request (an index lookup on the local file); there's no cache from which a revoked key could continue to work |
| Several per person | yes, per device or purpose, with a label | A lost laptop costs one key, not all of them |
| Evidence | audit: Person (Kennung) (person (ID)); “last used” with time and origin per key | Who acted and which key to revoke |
| Identity in SQL Server | If the person has their own login (--login) and the alias has Persons: Impersonate (which requires Windows in AuthMode), the request runs under that login (EXECUTE AS LOGIN, DenyImpersonate applies). Otherwise under the alias login. In both cases the person is in the session context, read-only: SESSION_CONTEXT(N'wkl.person'), SESSION_CONTEXT(N'wkl.key') | Triggers, column defaults and views can ask who acted, even without a login of their own. The connection pool resets the context before the next request |
| Service keys | never switch identity | A service isn't a person |
“Last used” is collected and written every 60 seconds and on shutdown, not per request: every write to the file would make the service read it again. A crash costs at most that minute of statistics, never an authorization. The service doesn't treat writes that don't change the operating data (usage, keys, people) as a new version and doesn't report them.
Manage people and keys—via the administration API, or on the service's
computer with the commands save-person, issue-key, revoke-key and list-keys; they run with the
configuration and environment of the service, even while it's running. Invocation, options and return codes
are listed under Server commands: people and keys. These commands
don't write to the change log (whoever runs them has access to the file anyway); the recovery commands
init-admin, reset-totp and remove-sign-in-rule, however, do, with the actor “Konsole (Rechner, Konto)”
(console (computer, account)).
| Response | Meaning |
|---|---|
401 “Der persönliche Schlüssel ist unbekannt.” (The personal key is unknown.) | No such key, or a typo |
401 “Persönliche Schlüssel sind an diesem Dienst nicht eingerichtet.” (Personal keys aren't set up on this service.) | PersonalKey isn't turned on in AuthMode |
403 “Die Person zu dieser Anmeldung ist gesperrt.” (The person for this sign-in is blocked.) | The Windows account or SQL Server login belongs to a blocked person. If the same account belongs to several people, the service doesn't check this |
401 “… ist abgelaufen.” (… has expired.) / “… ist gesperrt.” (… is revoked.) / “… ruht; er gilt nur, solange die Verwaltung ihn freigeschaltet hat.” (… is dormant; it's valid only while an administrator has activated it.) / “Die Person zu diesem Schlüssel ist gesperrt.” (The person for this key is blocked.) | State of the key or of the person. Only someone who has the key learns this distinction; guessing a key is hopeless |
403 “Dieser persönliche Schlüssel gilt nicht für diese Herkunft.” (This personal key isn't valid for this origin.) | The key names networks, and the request comes from outside them—or its origin can't be determined |
403 “Der Zugriff auf diese Ressource ist nicht freigegeben.” (Access to this resource isn't permitted.) | Rules of the person or restriction of the key (alias, read-only) |
403 “Die SQL-Server-Anmeldung dieser Person gibt es nicht.” (This person's SQL Server login doesn't exist.) | --login names a login that SQL Server doesn't know; no fallback to the alias login |
On-demand keys
A key can be issued so that it's dormant until an administrator activates it for a period—for example a read-only key without a second factor that a person stores in Power BI for presentations to the board. The key itself stays the same across all activations, so Power BI doesn't need to be set up again. Outside an activation it opens nothing, even if it has been left behind in a report, on a laptop or in a mail.
| Step | How |
|---|---|
| Issue | issue-key with "onDemand": true; the key is dormant from the moment it's issued |
| Activate | activate-key { "id", "hours" }; ends on its own. A new activation replaces the running one |
| End early | deactivate-key { "id" }; takes effect with the next request |
| Maximum duration | max_activation_hours (set Settings), otherwise Gateway:Store:MaxActivationHours, default 12, 1 to 168. Never beyond the key's expiry date |
Only an administrator may activate it, not the person themselves; both are recorded in the change log with
the actor and the end. The maximum lifetime of the key (MaxKeyLifetimeDays) still applies: after it, a new
key is needed.
Self-service
A person who can sign in another way—with their Windows account or their SQL Server login—issues personal keys to themselves, for example for Excel on a laptop, and revokes them again. Administrators don't need to do anything for this except turn self-service on once.
| Address | Body | Response |
|---|---|---|
GET /self/v1/keys | — | 200 { person, keys: [...], maximumDays }; per key ID, label, last 4 characters, read-only, aliases, issue date, expiry, revocation, last use—never the key |
POST /self/v1/issue-key | { "label", "expiresAt"?, "readOnly"?, "databases"? } | 201 with key, once; Cache-Control: no-store |
POST /self/v1/revoke-key | { "id" } | 200; the service answers someone else's key or an already revoked one with 404, without revealing whether it exists |
…/self/v1/{alias}/… | as above | the same for a SQL Server login: SQL Server checks its password at a data source, so the address names it |
| Rule | Reason |
|---|---|
Only with a Windows account or SQL Server login, assigned via the person's own account or own login (Persons.WindowsAccount, Persons.SqlLogin); a group isn't a person | A personal key doesn't issue keys: otherwise a stolen one could renew itself beyond its expiry and revocation. A service key belongs to no person |
Off until an administrator sets self_service_key_days (set Settings) or Gateway:Store:SelfServiceKeyDays to more than 0 | Whether people may create access for themselves is decided by administrators |
A self-issued key always expires: if not specified, after self_service_key_days, at most after the shorter of this value and MaxKeyLifetimeDays | Only administrators issue keys without expiry |
Can be restricted to read-only and to aliases; networks and the exemption from the second factor can't (400) | Both are decisions for administrators |
| The rights are the person's rules, as with every personal key | Self-service doesn't extend anything |
The sign-in policy treats self-service as administration (scope Admin); a new store requires a second factor there | Whoever issues access is administering—their own access. With the default rule, the person needs a second factor that has been set up and a session (/auth/v1/totp, for a SQL Server login /auth/v1/totp/{alias}) |
Only over a protected connection, like the administration; issuing and revoking are recorded in the change log as Person (Selbstbedienung) (person (self-service)) | The response carries a key |
“WklGatewayClient: VBA class for the data service” (documentation of the administration app) provides
OwnKeys, IssueOwnKey and RevokeOwnKey for this.
Sign-in policy and second factor
The sign-in policy defines which sign-in method needs a second factor or is refused for which part
of the service from which networks or countries—for example “personal keys from the corporate
network without, from outside only with TOTP”, “SQL Server logins never from outside” or “data access only
from Germany”. It's kept in the
administration store and maintained via the sets
SignInRules, SignInRuleItems, SignInRuleExceptions, SignInRuleExceptionItems and Settings of the
administration API; without a store, only denials are possible
(Gateway:Security:SignInRules in the configuration file).
| Field of a rule | Values | Meaning |
|---|---|---|
Scope | All (default), Data, Admin | part: data interfaces or administration |
Method | Any (default), ApiKey, PersonalKey, Windows, SqlLogin | sign-in method of the caller |
Effect | RequireTotp (default), Deny | second factor required, or denial |
From / ExceptFrom | networks such as 10.0.0.0/8, or the name of one of your own networks from `Gateway:SignInLog:Networks` | origin; empty = any. A name stands for every network listed under it there; an unknown name stops startup. An origin that can't be determined falls within the scope of every rule |
Persons / ExceptPersons | people (name; in the store the Id, attribute person/except_person) | only for these people, or not for them; empty = every caller. Requires the administration store |
WindowsGroups / ExceptWindowsGroups | Windows groups (name or SID) | only for members, or not for them; known only with sign-in by Windows account. An unknown group stops startup |
Countries / ExceptCountries | ISO 3166-1 alpha-2 country codes such as DE, TH (not case-sensitive) | only for requests from these countries, or not for them; empty = any country. A country that can't be determined falls within the scope of the rule. A code that isn't two letters stops startup |
Operators / ExceptOperators | network operators as autonomous systems, such as AS3320 (also 3320) | only for requests from the networks of these operators, or not for them; empty = any. An operator that can't be determined falls within the scope of the rule (see By network operator, program and time) |
Clients / ExceptClients | kind of program, such as Power Query, WinHTTP, Browser, PowerShell | only for requests that state this program, or not for them; empty = any. Claimed by the client; a request without a User-Agent falls within the scope of the rule |
Hours / ExceptHours | time windows such as Mon-Fri 06:00-20:00, Sat,Sun, 22:00-06:00 | only at these times, or not at them; empty = at any time. The service's clock, in the zone of Gateway:SignInLog:TimeZone. A window that can't be read stops startup |
Exceptions | list of exceptions (see below) | requests that meet all attributes of an exception at once are spared from the rule, optionally until an end |
Label | text | note, no effect |
Mode | Enforce (default), Report | enforce or only try. A rule on trial is judged at every sign-in but changes no decision; the sign-in event records what it would have done (see Try a rule) |
For individual people or groups. A rule can be restricted to people or Windows groups, or can exclude
them—for example “nobody from outside, except John Doe”:
{ Scope: Data, Effect: Deny, ExceptFrom: [10.0.0.0/8], ExceptPersons: [John Doe] }. The service
recognizes a person by their personal key, their own Windows account or their SQL Server login; a group only
with a Windows sign-in. A caller who isn't assigned to a person (service key) falls under no such rule and no
such exception. explain-sign-in of the administration API shows what a rule means
for a person.
By country. The service reads the country of a request from the geolocation database of the sign-in log—in memory, without network access, and only when a rule names countries. A request has no country when no geolocation database is loaded, when it comes from this machine, from one of your own networks (`Gateway:SignInLog:Networks`) or from a private network, or when the database doesn't know the address. Such an unknown country falls within the scope of every rule: “data access only from Germany” refuses it. So exclude your own networks by their addresses, not by a country:
{ "Scope": "Data", "Effect": "Deny", "Label": "Germany only",
"ExceptCountries": [ "DE" ], "ExceptFrom": [ "10.0.0.0/8" ] }
Important: A country derived from an address is a hint, not proof. A VPN, a proxy or a mobile network moves the origin to another country, and geolocation databases are wrong for some networks. Rules by country add a layer to keys, networks and the second factor; they don't replace any of them.
By network operator, program and time
A rule can also ask for the network operator of the origin, for the program and for the time of day. The three attributes differ in how far you can rely on them:
| Attribute | Where the value comes from | Reliability | Unknown when |
|---|---|---|---|
| Network operator | ASN file of the geolocation database of the sign-in log, read only when a rule names operators | observed: the service derives it from the address; mostly reliable, a VPN moves it | no ASN file loaded, own or private address, address unknown |
| Program | User-Agent of the request, grouped as in the sign-in log (Power Query, WinHTTP, Browser, PowerShell, otherwise the first word) | claimed: any program can send any text; Excel and Power BI Desktop send the same one | no User-Agent (the ODBC driver sends none) |
| Time | the service's clock, in the zone of Gateway:SignInLog:TimeZone (empty: the server's zone) | observed | never |
Like an unknown country, an unknown operator or a missing program falls within the scope of the rule and meets no attribute of an exception. A claimed program is therefore fit for narrowing (“the administration only from WinHTTP” refuses a browser). As the only attribute of an exception, it spares anyone who sends the text; combine it there with people, networks or an end.
Write a time window as [days] [hh:mm-hh:mm]:
- Days
Mon,Tue,Wed,Thu,Fri,Sat,Sunas a list (Sat,Sun) or a range (Mon-Fri, alsoFri-Mon); without days, every day applies. - Times with an exclusive end;
24:00is the end of the day; without times, the whole day applies. - A window across midnight (
Mon-Fri 22:00-06:00) belongs to the day it begins on: Friday 10 p.m. to Saturday 6 a.m. is inside it, Monday 3 a.m. isn't.
“SQL Server logins only Monday to Friday, 6 a.m. to 8 p.m.”:
{ "Method": "SqlLogin", "Effect": "Deny", "Label": "SQL office hours only",
"ExceptHours": [ "Mon-Fri 06:00-20:00" ] }
“From other network operators only with a second factor, never from Plant 3”:
{ "Scope": "Data", "Effect": "RequireTotp", "Label": "code from other networks",
"ExceptOperators": [ "AS3320" ], "ExceptFrom": [ "Plant 3" ] }
Your own and private networks have no network operator. Without ExceptFrom, the rule would therefore
require the code in the plant as well.
The sign-in event names in Signals the network operator it judged by (operator), the program
(client) and, under trust, how reliable each signal is (observed, claimed, verified).
Exceptions with an end. The lists ExceptPersons, ExceptCountries and ExceptFrom each exclude on
their own and permanently: ExceptPersons: [John Doe] spares John from every country. An exception in
Exceptions, by contrast, spares only when all of its attributes apply at once—for example “John Doe from
Thailand until November 2”:
{ "Scope": "Data", "Effect": "Deny", "Label": "Germany only",
"ExceptCountries": [ "DE" ], "ExceptFrom": [ "10.0.0.0/8" ],
"Exceptions": [ { "Persons": [ "John Doe" ], "Countries": [ "TH" ],
"Until": "2026-11-02T00:00:00Z", "Label": "vacation" } ] }
| Field of an exception | Values | Meaning |
|---|---|---|
Persons | people (name; in the store the Id) | the caller is one of them; empty = anyone |
WindowsGroups | Windows groups (name or SID) | the caller is a member of one of them; empty = anyone |
Countries | country codes as above | the request comes from one of them; empty = any country |
From | networks such as 203.0.113.0/24, or names of your own networks | the origin lies in one of them; empty = any |
Operators | network operators such as AS3320 | the origin belongs to one of them; empty = any |
Clients | kind of program as above | the request states one of them; empty = any. Claimed |
Hours | time windows as above | the request falls into one of them; empty = at any time |
Until | point in time in UTC, such as 2026-11-02T00:00:00Z | from this point on, the exception spares nobody; empty = no end. A value without a time zone is read as UTC |
Label | text | note, no effect |
- At least one attribute or an end. An exception with neither would spare everyone permanently; the startup check and the administration API reject it.
- Not knowing never spares. An unknown country, an unknown origin, an unknown network operator, a missing program or a caller who isn't assigned to a person meets no attribute that asks for it. Without a geolocation database, an exception by country therefore closes instead of opening the rule.
- The end takes effect immediately. The service compares it with its clock on every request; no change is needed. When it notices the end, it writes it once into the change log (
Entityexception-lapsed, actorDienst (Ablauf), before: rule, label and end). An exception that has ended stays in place until it's removed.
Caution when creating a denial. A rule takes effect as soon as it's saved. If you first create a denial
for all scopes without exceptions and add the attributes afterward, you lock yourself out at that moment.
So create denials with save-sign-in-rule of the administration API,
which saves the rule and exceptions in one change; if it has happened, remove-sign-in-rule on the server
helps.
Every matching rule counts, a denial before anything else; the order means nothing. Exception: the
service judges the exchange of a code and the setup of the factor (/auth/v1/totp, /auth/v1/enrol) only by
rules with scope All; a denial only for Data or Admin doesn't prevent an attempt there. And SQL Server
checks a SQL Server login before the policy judges: from a denied origin, a wrong password results in
401, a correct one in 403—so an attacker learns whether a password is correct, limited only by the
failed-attempt limit. If no rule matches, the rights alone decide. Unauthenticated operation in the
development environment isn't judged; a policy that has locked out the administration is repaired with the
server commands. A new store starts with the rule
“administration only with a second factor” (Scope: Admin, Effect: RequireTotp); it can be changed or
deleted like any other. Independently of this, the fixed lower bound Gateway:Store:RequireAdminTotp
(default true) requires the second factor for the administration and self-service, for every sign-in
method and every origin; not even a key with TotpExempt lifts it. It can only be turned off in the
configuration file. The startup check rejects RequireTotp for service keys—a service isn't a person and
has no second factor; write Deny instead.
Second factor (TOTP). Time-based codes according to RFC 6238 (6 digits, 30 seconds), as generated by
every authenticator app. The factor belongs to a person: a personal key names them; a Windows account and
a SQL Server login find them via their own account (WindowsAccount) or their own login (SqlLogin); only
the login name is compared, case-insensitively, across all aliases and servers, and a session opened this way
applies not only to the alias where the code was exchanged. A group isn't a person; a sign-in that matches no
person or two people gets 403 if a second factor is required. An administrator sets it up
(enroll-totp), ideally as a one-time link: the page shows the otpauth:// address as a QR code and next to
it the key for manual entry. The secret is stored sealed in the store (its own purpose, its own master key
change via reseal-store).
Set up at first sign-in. If a person doesn't have a second factor yet but the policy requires one, they may set it up themselves for a limited time—with their first factor, which until then is valid only for this:
POST /auth/v1/enrol[/{alias}] first factor as when exchanging a code
Body: { } 200: { "person", "secret", "uri", "qrPng", "openUntil" }
Body: { "code": "123456" } 201: as for the exchange - the factor is set up, a session opened
- When. Within
totp_enrol_hours(setSettings, otherwiseGateway:Security:TotpEnrolHours, default 24, at most 168,0turns off setup at sign-in) after the key with which the request comes was issued; for a person without a key (Windows account), until the time set byinit-adminorreset-totpon the server. - Why a deadline. Until the factor is set up, the key alone stands for the person. Anyone who intercepts it during this time could bind their own authenticator app. The deadline keeps this time short; a key that has waited too long must be reissued.
- Once. The confirmation ends the period and is recorded in the change log; a person who has a factor is no longer offered setup. If someone else set it up first, the rightful holder notices it this way. The administration then sets up a new factor.
- Two steps. Without a code, the service generates a secret and responds with the address, the key for typing in and a QR code as PNG; it's stored sealed only with the first matching code. A new call without a code replaces the generated secret.
- Until then, the service answers every other request of the person with
401andWWW-Authenticate: Wkl-Totp realm="Wkl.Data.Gateway", enrol="/auth/v1/enrol". The administration application then shows the setup card; the VBA classWklGatewayClientreportsgwErrEnrolmentRequiredand offersBeginTotpEnrolment/ConfirmTotpEnrolment.
Code for session. A code lives for 30 seconds; a driver paging through a table can't ask for one with
every request. The code is therefore exchanged once for a session, which then accompanies every request
in the X-Wkl-Session header:
POST /auth/v1/totp first factor: personal key or Windows account
POST /auth/v1/totp/{alias} first factor: SQL Server login (Basic), checked at this alias
Body: { "code": "123456" } optionally with "role": "vertrieb"
201: { "session": "…", "id": "t…", "person": "…", "header": "X-Wkl-Session", "expiresAt": "…" }
If the body names role, the session is bound to this
database identity: a request without its own choice
runs under it. The choice is checked on every request, not at the exchange.
- The session isn't a sign-in. Every request still carries its first factor; the session applies only to the person, the method and—with a key—the key with which it was opened. A stolen token alone opens nothing. That's why it's a separate header instead of
Authorization: Bearer: with Windows and Basic, that header already carries the first factor. - Lifetime
totp_session_hours(setSettings, otherwiseGateway:Security:TotpSessionHours), default 10 hours, 1 to 168. - End: expiry;
RevokedinSessionTokens; revoking the key; disabling the person; setting up again or removing their second factor. This is looked up on every request. - A code is valid once (its time step must be later than the last one accepted) and one step (30 seconds) before or after the service's clock; this tolerance is fixed and can't be configured. Five wrong codes per person or twenty per origin within 15 minutes lead to
429. The counters are kept in memory: a restart resets them, and anyone who knows a name can lock the person out for a while with wrong attempts. - The exchange only over a protected connection (HTTPS, trusted proxy with
https, same computer).
Clients without a dialog—Power BI, Excel, scripts—can't ask for a code. For them there are keys with
TotpExempt: exempt from a required second factor, not from a denial, and permitted only as a read-only
key. With rights that restrict the person to released views, and optionally a network binding, this is the
narrowly limited access for reports.
| Response | Meaning |
|---|---|
401 with WWW-Authenticate: Wkl-Totp realm="Wkl.Data.Gateway", endpoint="/auth/v1/totp" | Second factor required: no session or no valid session. With a SQL Server login, endpoint names the alias (/auth/v1/totp/{alias}). The ODBC driver asks for the code in a dialog and keeps the session for the process |
401 “Der Code ist falsch oder wurde schon benutzt.” (The code is wrong or has already been used.) | exchange refused |
403 “Die Anmelderichtlinie lässt dieses Anmeldeverfahren von dieser Herkunft nicht zu.” (The sign-in policy doesn't allow this sign-in method from this origin.) | rule Deny. The response names no country; the sign-in log records a denial that rests on rules with countries alone with the reason deniedByCountry, otherwise denied |
401 with WWW-Authenticate: Wkl-Totp realm="Wkl.Data.Gateway", enrol="/auth/v1/enrol" | Second factor required but not set up yet; the person may set it up now |
403 “… gehört zu keiner Person” (… doesn't belong to any person) / “… ist keiner eingerichtet, und die Frist … ist abgelaufen.” (… none is set up, and the period … has expired.) | second factor required but not possible |
403 “Ein zweiter Faktor wird nur über HTTPS oder vom eigenen Rechner angenommen.” (A second factor is accepted only via HTTPS or from the same computer.) | exchange over an unprotected connection |
429 | too many wrong codes |
Try a rule
Try a new rule—above all a denial—first with Mode = Report. The rule is then judged at every sign-in
like an enforced one but changes nothing: whoever would be admitted without it stays admitted. The
sign-in event names it in Rules with "mode": "report"; SignInRuleReports counts, per rule and
version, how many sign-ins it would have hit, from how many people and origins, and which ones an
exception would have spared (see administration).
explain-sign-in shows it separately under tried.
If the result fits, enforce the rule with save-sign-in-rule and "mode": "Enforce"; from then on it
takes effect. The switch is in the change log and changes the rule's version, so events from the
trial and from operation stay distinguishable. The administration's lower bound
(RequireAdminTotp) is no rule and can't be tried.