Company sign-in
Sign customers in with your organization's OpenID Connect provider.
A brand's requester portal can sign customers in with their organization's OpenID Connect provider (Entra ID, Okta, or any provider with a discovery document). It is set per brand, under Requester portal in the brand's settings, or in the portal step of first-run setup. src/lib/portal/sso.ts holds the protocol; the routes are under src/app/sign-in/sso/.
Redirect URI
Register the redirect URI the portal settings show, exactly as shown. The portal is on APP_URL whatever addresses a brand has, so there is one redirect URI for every brand: APP_URL/sign-in/sso/callback, such as https://support.example.com/sign-in/sso/callback. Several brands can share one app registration at the provider, and it needs that one entry.
The redirect URI never has a query string. openid-client sends the token request the callback's URL with its query removed, and a provider rejects a redirect_uri that differs from the authorization request's (RFC 6749 §4.1.3).
The callback learns the brand from the sign-in's state cookie instead. Each brand's attempt has its own cookie, bouee_portal_sso_<brand id>, so sign-ins to several brands can run side by side in one browser. The callback uses the one whose recorded state matches the provider's answer. The cookie is signed, host-only and valid for ten minutes. It records the workspace, the brand and the exact redirect URI sent, and the exchange repeats that URI.
The callback refuses:
- any hostname but
APP_URL's (a brand's own hostname serves only its knowledge base), - a
brandparameter, which no provider sends.
With no matching cookie (expired, already used, tampered with, or started in another browser), it exchanges nothing: nothing says which brand it was, so it shows a page saying the sign-in did not complete.
Test sign-in
Saving checks only that the issuer's discovery document can be read. Once the issuer, client ID and client secret are saved, Test sign-in runs a customer's sign-in and comes back through the redirect URI. It then shows what came back on a page only the workspace's owner can open, or first-run setup while no agent exists. It creates no portal session, and it works before the portal's access is set to the identity provider.
It reports the provider's own answer:
- a wrong client ID or secret (
invalid_client), - a code the provider will not exchange (
invalid_grant, often a redirect URI mismatch), - a sign-in the provider refused,
- an account without a verified email address,
- an account outside the customer sign-in domains.
A redirect URI the provider does not have registered is refused on the provider's own page, before anything returns to Bouée.
Customer sign-in domains
The optional domain list restricts who company sign-in lets in. Left empty, it admits anyone the provider signs in. An entry that is not a domain name, an email address included, refuses the save and names the entry, and the form keeps what was typed.
Where Bouée connects
Bouée fetches the provider itself: its discovery document when the settings are saved and whenever the cached copy is more than ten minutes old, and its token, userinfo and key endpoints during sign-in. A workspace owner chooses the issuer, so each of those requests looks the provider's host name up once, checks every address it resolves to, and connects only to those addresses. A name that answers differently a moment later (DNS rebinding) changes nothing, and TLS and the Host header still use the name. Redirects are not followed. The rules are in src/lib/http/outbound.ts.
| Address | Company sign-in |
|---|---|
| Public | Allowed. |
Loopback, private and other non-public ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, ::1, fc00::/7, and the documentation, benchmarking, 6to4 and Teredo ranges | Refused, unless OUTBOUND_ALLOW_PRIVATE allows it. Allowed in development. |
Link-local and cloud metadata: 169.254.0.0/16 (169.254.169.254 included), fe80::/10, fd00:ec2::/64, fd20:ce::254, 100.100.100.200, 168.63.129.16, 192.0.0.192. Unspecified (0.0.0.0/8, ::), multicast and broadcast | Always refused, in development too. |
A name is refused when any address it resolves to is refused. An IPv4 address written as IPv6 (::ffff:10.0.0.5, or behind the NAT64 prefix 64:ff9b::/96) counts as the IPv4 address. The issuer must also be a host name: an IP address, localhost, a single-label name, or a name under .local or .internal is refused from the name alone, before anything is looked up.
Saving checks the issuer and the endpoints its discovery document names, so a provider that sign-in would refuse is refused at saving, with the same message. Development skips those checks at saving, for a test provider on the developer's machine, and accepts a plain http issuer; each request is still checked.
Requests go straight to the provider's address. They never go through an HTTP proxy set in the environment, because the proxy would look the name up again.
A provider on your own network
A self-hosted installation whose provider runs on its own network lists the provider in OUTBOUND_ALLOW_PRIVATE, comma-separated, and restarts:
OUTBOUND_ALLOW_PRIVATE="sso.corp.example,10.20.0.0/16,fd12:3456::/48"- A host name lets that provider resolve to loopback and private addresses. Its issuer may then also be a single-label name or a name under
.internalor.local. - An address (
10.20.0.5) or a network (10.20.0.0/16) lets any provider resolve there, and lets an issuer be written as an address inside it.
Link-local and metadata addresses stay refused whatever the list says. An entry that is not a host name, an address or a network makes the environment invalid: the server log names the entry at startup, and requests fail until it is fixed. The provider's certificate still has to be valid for its name; for an internal certificate authority, give Node that authority's certificate with NODE_EXTRA_CA_CERTS.
Every workspace owner can point company sign-in at anything the list allows. An installation that hosts other people's workspaces, like Bouée Cloud, leaves it empty.