Auto-detection
What happens when you leave the mode unset — the precedence rules, the ambient probe, and how an ambiguous session is resolved.
unknown is the default, and it is not a placeholder: it is a request to work the mode out. Two mechanisms do
that, at two different moments, and confusing them makes the behaviour look erratic.
| Mechanism | When | How |
|---|---|---|
| Detection | initializeConfig() | Pure, no I/O: config plus the server's pre-login response |
| Ambient probe | login() | A real request, to see whether the transport authenticates |
Detection: the precedence
detectAuthMode(config, preLogin) decides, in this order:
Two things follow. Declaring a mode is absolute: an explicit sso, bearer, oauth or saml is never
overridden by what the server announces. And OAuth wins over SAML when both are configured.
This is a fixed regression, worth knowing if you are on 2.2 or earlier
detectAuthMode used to recognise credentials only. Any other explicit mode was discarded and replaced by
unknown unless the matching legacy key happened to be set too — so an application that had declared exactly
how it authenticates got a username and password prompt.
The probe: resolving unknown at sign-in
With the mode still unknown, login() runs the ambient handshake — a request to a protected endpoint that
lets the server challenge it:
- It succeeds → the mode becomes
sso, and the session is recorded. - It fails → the mode becomes
credentials, and a form is what your interface should render.
That fallback is announced in the logs rather than done silently, because the reason is almost always a configuration the client never received: no token for a token-authenticated server, or a provider that neither the configuration nor the pre-login response declared.
import { createAtomicClient, isExternalAuthMode } from "@sinequa/atomic";
// No authMode: the default is unknown, which means "work it out".
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
// Reads the pre-login response and applies the precedence above.
await client.initializeConfig();
console.log("detected:", client.config.authMode?.kind);
const signedIn = await client.auth.login();
if (signedIn) {
start();
} else if (isExternalAuthMode(client.config.authMode)) {
// oauth and saml navigate away, so reaching here means the redirect did not
// happen or came back empty — not a case for a password form.
showError("Sign-in did not complete.");
} else {
// Resolved to credentials: the form owns the sign-in from here.
showForm();
}
function start() {}
function showForm() {}
function showError(message: string) {
console.error(message);
}import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core";
import { isExternalAuthMode } from "@sinequa/atomic";
import { SINEQUA } from "./sinequa";
/**
* Renders from the mode the initializer resolved. `config` is a live view, so by
* the time this component exists the value reflects detection.
*/
@Component({
selector: "app-auth-gate",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (signedIn()) {
<router-outlet />
} @else if (external()) {
<p>Signing you in…</p>
} @else {
<app-sign-in />
}
`,
})
export class AuthGateComponent {
private readonly client = inject(SINEQUA);
readonly signedIn = computed(() => this.client.auth.isAuthenticated());
/** True for sso, oauth, saml and bearer — every mode with no form to show. */
readonly external = computed(() => isExternalAuthMode(this.client.config.authMode));
}import { isExternalAuthMode } from "@sinequa/atomic";
import type { ReactNode } from "react";
import { sinequa } from "./sinequa";
import { SignIn } from "./SignIn";
/**
* Renders from the mode resolved during start-up. `config` is a live view, so this
* reads the detected value rather than the one passed at creation.
*/
export function AuthGate({ children }: { children: ReactNode }) {
if (sinequa.auth.isAuthenticated()) return <>{children}</>;
// True for sso, oauth, saml and bearer — every mode with no form to show.
if (isExternalAuthMode(sinequa.config.authMode)) return <p>Signing you in…</p>;
return <SignIn onSignedIn={() => window.location.reload()} />;
}When a session already exists under unknown
A subtler case: login() finds a valid session, but nothing on the wire says whether it came from a form or
from the host. It is resolved like this, in order:
- A configured transport — an SPFx
AadHttpClient, any injectedhttpAdapter— settles it assso. Such a transport authenticates every request itself, so the session cannot have come from a form. This is a fact about the current page. - Otherwise, a marker in
localStoragewritten by the last credentials sign-in settles it ascredentials. - Otherwise,
sso.
A real session found by the ambient probe also takes precedence over the marker: if the handshake succeeds, the
mode is sso even when the marker says otherwise.
The marker is shared per origin
localStorage is per origin, not per application. Serving an SPFx build from the same host as a
credentials-based application — an ng serve on localhost, typically — used to make it label the
host-authenticated session credentials, and then offer a login form for an application its host had already
authenticated.
Rule 1 exists for that reason. An adapter that does not authenticate (one added for logging or retries)
should declare authMode explicitly rather than rely on this resolution.