OAuth and SAML
Redirecting to an identity provider, coming back correctly, and the loop guard that stops a misconfigured callback from cycling forever.
Both modes work the same way: the server hands you a URL, the browser leaves for the identity provider, and it comes back with a session cookie already set. Your interface renders a loader and nothing else — there is no form, and no token for you to handle.
import { AuthMode } from "@sinequa/atomic";
client.configure({ authMode: AuthMode.oauth("my-provider") });
// This may never return: the page is left.
await client.auth.login();The round trip
The critical part is the second boot. The application restarts with a session it did not establish in this page load, so it has to check rather than sign in again.
Coming back
import { AuthMode, createAtomicClient } from "@sinequa/atomic";
const client = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
authMode: AuthMode.oauth("my-provider"),
});
await client.initializeConfig();
// First: is there already a session? This is the return-from-provider case, and it
// never redirects — unlike login(), which would navigate away again.
if (await client.auth.hasSession()) {
start();
} else if (client.auth.isRedirectPending()) {
// We came back from the provider without a usable session. Redirecting again
// would loop, so say what happened instead.
showError("Sign-in completed at the provider, but no session reached this application.");
} else {
// A genuine first visit: this navigates away and does not return.
await client.auth.login();
}
function start() {
console.log("signed in");
}
function showError(message: string) {
console.error(message);
}import { InjectionToken, inject, provideAppInitializer } from "@angular/core";
import { AuthMode, type AtomicClient, createAtomicClient } from "@sinequa/atomic";
export const SINEQUA = new InjectionToken<AtomicClient>("Sinequa client", {
factory: () =>
createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
authMode: AuthMode.oauth("my-provider"),
}),
});
/** Set when the provider sent us back without a session, so the UI can explain. */
export const redirectFailed = { value: false };
export function provideSinequa() {
return provideAppInitializer(async () => {
const client = inject(SINEQUA);
await client.initializeConfig();
// hasSession() never redirects — the right probe on the way back.
if (await client.auth.hasSession()) return;
if (client.auth.isRedirectPending()) {
redirectFailed.value = true;
return;
}
// Navigates away; nothing after this line runs on a first visit.
await client.auth.login();
});
}import type { ApplicationConfig } from "@angular/core";
import { provideSinequa } from "./sinequa";
export const appConfig: ApplicationConfig = {
providers: [provideSinequa()],
};import { AuthMode, createAtomicClient } from "@sinequa/atomic";
export const sinequa = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
authMode: AuthMode.oauth("my-provider"),
});
export type AuthState = "signed-in" | "redirect-failed" | "redirecting";
/**
* Resolved before rendering. Note the order: probe first, because login() would
* navigate away again on the way back from the provider.
*/
export const ready: Promise<AuthState> = (async () => {
await sinequa.initializeConfig();
if (await sinequa.auth.hasSession()) return "signed-in";
if (sinequa.auth.isRedirectPending()) return "redirect-failed";
await sinequa.auth.login(); // navigates away
return "redirecting";
})();The loop guard
Before asking the server for a sign-in URL, the library checks a one-shot flag in sessionStorage. If it is
already set — meaning we redirected once and came back without a session — it throws instead of redirecting
again:
OAuth sign-in with provider "my-provider" did not establish a session — its callback
is likely not authorized for this application.Two details of its design are deliberate, and both fixed real loops:
- It is checked before the server call, not after. A provider we already know sends us back empty-handed should not be asked for another URL.
- It is not consumed when it fires. An earlier version removed the flag while raising the error, so the next
401— an application writing user settings while unauthenticated, say — triggered a recovery that signed in again, found no guard, and redirected once more. The application cycled through the provider about once a second.
It is cleared by logout(), by any session actually being established, and by a successful hasSession().
A failed probe leaves it alone, so the protection stays intact.
Configuring the provider
Two ways, and the mode wins:
client.configure({ authMode: AuthMode.oauth("identity") }); // preferred
client.configure({ autoOAuthProvider: "identity" }); // legacy key, still readThe server's pre-login response may also announce one, which is what makes auto-detection work without any client configuration at all.
With neither, login() throws and names both ways to fix it. A client created with createAtomicClient and no
autoOAuthProvider must carry the provider in the mode.