Skip to main content

Configure authentication

Configure sign-in on the shell: backend in shellui.config.json, built-in routes at /login and /login/callback, and navigation guards. The shell stores the session, refreshes tokens, and shares the signed-in user with trusted iframe apps. Choose a provider on Connect a backend first.

For token storage hardening, optional HttpOnly refresh (BFF), and shell CSP, see Security hardening.

Enable authentication​

Add a backend block. Without it, useAuth() reports signed out and login actions are unavailable.

{
"backend": {
"type": "shellui",
"url": "http://localhost:8000",
"companyId": 1,
"login": {
"methods": ["oauth", "magic_link"],
"oauthProviders": ["github", "google"]
}
}
}

backend.login.methods lists what the login page may show. At runtime the shell intersects that list with backend settings so disabled providers stay hidden. When methods is missing or empty, the login page defaults to magic_link, plus oauth if oauthProviders is set.

After a magic link is sent, the login page replaces the email field with a "Check your email" message showing the address. Use a different email unlocks after 60 seconds so you can't resend links in a loop.

Staff accounts cannot use a magic link. If you request one, or open an old email link, identity-service returns magic_link_staff_disabled. The login page and /login/callback then show: "Staff accounts can't sign in with an email link. Sign in with your usual sign-in method instead."

MethodLogin UINotes
oauthProvider buttonsNeeds oauthProviders and a backend-enabled provider
magic_linkEmail fieldSupabase email auth; identity-service when advertised. Staff accounts are refused
web3Ethereum walletWhen the backend enables it
passwordnoneTyped and forwarded; the stock login view does not render a password form

oauthProviders is an array of ids (github, google, microsoft, apple). Shellui deduplicates and lowercases them. For identity-service, per-company OAuth clients from /api/v1/settings appear as separate labeled buttons.

identity-service needs url and companyId for OAuth. Optional adminPathname / adminUrl add Administration in the account menu for staff/owners - see Administration. Supabase needs url and publishableKey.

Login routes​

The shell registers these paths (urls in @shellui/core/constants/urls). You do not implement them in microfrontends.

PathPurpose
/loginOAuth, magic link, Web3, legal links, next redirect
/login/callbackOAuth callback (authorization code exchange)

Protected routes redirect to /login?next=<encoded-path>. After sign-in, the shell navigates to next as an in-app path. Example: /billing → /login?next=%2Fbilling.

Login left panel​

On desktop, /login shows a full-height left panel. Both branding fields are optional.

FieldBehavior
panelUrlFull-bleed iframe. Wins when both fields are set. Must be a same-origin relative path, a configured backend/login/storage origin, or (development only) a loopback URL; untrusted URLs are ignored
panelImageCentered object-contain image
neitherMuted panel with clickable appIcon top left (links home)

On mobile, the square appIcon is pinned top-left and the form stays centered. Language (when multiple languages are configured) and light/dark controls sit at the top right of the form column. The form uses .shellui-safe-pad for notches. Relative image paths are served from static/. The left panel is hidden on mobile and when login is embedded in a modal iframe.

login: {
methods: ["oauth"],
oauthProviders: ["github"],
panelUrl: "http://localhost:5176/login-branding/",
// panelImage: "/login-panel.jpg",
}

Optional login nav item​

The shell always exposes /login. Add a nav entry to open it in the main area, a modal, or a drawer:

import type { ShellUIConfig } from '@shellui/core';
import urls from '@shellui/core/constants/urls';

const config: ShellUIConfig = {
navigation: [
{
label: 'Login',
path: 'login',
url: urls.login,
openIn: 'modal',
position: 'end',
},
],
};

When a session exists, login entries whose URL matches the shell login route are hidden from the sidebar. The header account control (avatar or Login) remains in supported layouts.

Layouts with a sidebar, app bar, or Windows taskbar render that control: signed-out users go to /login; signed-in users get profile, settings, optional administration, and logout. Logout from a requiresAuth route navigates to / first so you are not sent straight back to login.

Loading state on reload​

After a page load with a stored session, the shell exchanges the refresh token before it knows who is signed in. During that first restore, the account control shows a skeleton instead of Login, login nav entries stay hidden, and Admin shows a "Restoring your session…" spinner instead of "Access forbidden". Visitors without a stored session see Login right away. Later token refreshes run in the background and never show a loading state.

Guard routes​

See Navigation for the full item shape.

requiresAuth: direct navigation checks the session. While auth is loading, the route shows a fallback; when signed out, the shell redirects to /login?next=.... The iframe URL does not load until you sign in.

{
"label": "Billing",
"path": "billing",
"url": "https://app.example.com/billing",
"requiresAuth": true
}

hideWhenLoggedOut: omit the item from the sidebar and 404 suggestions while signed out. Combine with requiresAuth to hide and protect.

filterNavigationForAuthState removes hideWhenLoggedOut items when signed out and hides login nav URLs when signed in. requiresDevMode items are unchanged by auth.

Session in the host​

import { useAuth } from "@shellui/core";

function Example() {
const { user, isAuthenticated, isLoading, logout, startOAuth } = useAuth();
void startOAuth;

if (isLoading) return null;
if (!isAuthenticated) return <p>Not signed in</p>;

return (
<div>
<p>{user?.email}</p>
<button type="button" onClick={() => void logout()}>
Sign out
</button>
</div>
);
}

isLoading is true only during the first session restore after a page load. Background token refreshes leave it false, so it is safe to drive loading placeholders.

Sessions persist in browser storage. The shell refreshes access tokens before expiry while the tab is open and persists rotated refresh tokens from POST /api/v1/token.

With identity-service 0.5.0+, the default OAuth delivery is a one-time shellui_auth_code query param on /login/callback. The shell exchanges it via POST /api/v1/oauth/session and stores the returned JSON tokens. Legacy fragment delivery (#access_token=…&refresh_token=…) still works when identity is configured with OAUTH_TOKEN_DELIVERY=fragment or token_delivery=fragment on authorize. Older shells that receive provider ?code= on the frontend continue to use POST /api/v1/oauth/exchange.

Logout sends the refresh token in the request body when available so identity can revoke the server-side session.

Register the identity callback URL on GitHub/Google/Microsoft (not each shell URL). Add every browser shell origin to the company OAuth redirect allowlist so redirect_to is accepted.

Company access pending​

When identity-service uses a non-public company join mode (domain or invite), OAuth may create an account but not issue tokens for that company. Access is stored per company (CompanyMembership.is_enabled).

The backend returns error_code access_pending or access_denied (also as shellui_oauth_error_code on bounce redirects). The shell then shows a pending-access screen. After an admin enables membership, sign in again. Configure modes in admin Organization or PATCH /api/v1/companies/<id>/.

Iframe apps​

Iframes do not read storage directly. They receive user through SDK settings after the shell initializes auth. The session access token is included only when the companion navigation item sets safeForAuthToken: true (opt-in). The refresh token stays in the shell and is never sent to iframe apps.

Migration (breaking change)​

If your companion relied on the previous default (token shared without config), add safeForAuthToken: true on its navigation item after upgrading:

{
label: 'My app',
path: 'my-app',
url: 'http://localhost:5173/',
safeForAuthToken: true,
}

From 0.5.1 onward, postMessage origin and trusted-frame policy is on by default. The Shellui host derives companion origins from navigation, storage, and admin URLs in shellui.config.json. Add preview or staging hosts with security.allowedMessageOrigins. See SDK messaging (security.allowedMessageOrigins). Token sharing still requires safeForAuthToken: true. Allowed origins are enforced separately for postMessage traffic.

import { shellui } from '@shellui/sdk';

shellui.login({
method: 'oauth',
provider: 'github',
redirectPath: '/login',
});

Supported method values: oauth (with provider), web3. Nested frames send SHELLUI_LOGIN to the root. When backend.type is shellui, settings include authBackendBaseUrl so admin tools call the same identity base URL.

Signed-in users open Settings for account fields, theme, language, and region. Preferences sync with the backend when configured. Legal links on the login page come from legalDocuments - see Legal documents.

Account management​

With the identity-service backend (backend.type: "shellui"), Settings → user account lets users manage their own account:

  • Edit name: an edit button next to the name opens an inline field. The shell saves it with PATCH /api/v1/user and confirms with a toast.
  • Delete account: a Delete account button next to logout asks for confirmation, then calls DELETE /api/v1/user. It removes the account for the current company only. A stale sign-in prompts the user to sign in again, and the only owner of a company must make another member an owner first.

Both requests are accepted only from the shell window or its own Settings frame, never from embedded apps. In host code, useAuth() exposes supportsProfileUpdate / updateProfile and supportsAccountDeletion / deleteAccount. Supabase backends hide both controls.

Checklist​

  1. Run identity-service or Supabase and set backend.type, url, and provider keys
  2. Set backend.login.methods and oauthProviders to match the backend
  3. Mark sensitive nav items with requiresAuth and optionally hideWhenLoggedOut
  4. Test a signed-out deep link (/billing → login → return)
  5. In embedded apps, read the signed-in profile from SDK settings and call shellui.login() for OAuth