Add Sign-In
Configure DefaultAuthProvider and DefaultAuthService in an application configuration class, declare the sign-in endpoint in noAuthPaths, and supply a login page.
import {
type BaseArdorApplication as TArdorApplication,
configuration,
CoreBindings,
DefaultAuthProvider,
DefaultAuthService,
DefaultRestDataProvider,
inject,
provide,
} from '@venizia/ardor';
@configuration()
export class AuthConfiguration {
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
private readonly application: TArdorApplication,
) {}
@provide({ key: CoreBindings.REST_DATA_PROVIDER_OPTIONS })
restDataProviderOptions() {
return { url: '/api', noAuthPaths: ['/auth/sign-in'] };
}
@provide({ key: CoreBindings.AUTH_PROVIDER_OPTIONS })
authProviderOptions() {
return { paths: { afterLogin: '/dashboard' } };
}
@provide({ key: CoreBindings.DEFAULT_REST_DATA_PROVIDER })
restDataProvider() {
return this.application.instantiate(DefaultRestDataProvider).value(this.application);
}
@provide({ key: CoreBindings.DEFAULT_AUTH_SERVICE })
authService() {
return this.application.instantiate(DefaultAuthService);
}
@provide({ key: CoreBindings.DEFAULT_AUTH_PROVIDER })
authProvider() {
return this.application.instantiate(DefaultAuthProvider).value(this.application);
}
}1. What default sign-in expects from IGNIS
DefaultAuthProvider.login (packages/admin/src/providers/auth.ts) sends an HTTP POST request to paths.signIn (default /auth/sign-in) using restDataProvider.send.
Request body generated by buildSignInBody:
{
"identifier": { "scheme": "username", "value": "admin" },
"credential": { "scheme": "basic", "value": "password" }
}Response payload read by readSignInSession:
{
"userId": 1,
"roles": ["admin", "editor"],
"token": { "value": "eyJhbGciOiJIUzI1NiIsIn...", "type": "Bearer" }
}readSignInSession also accepts { token: "<jwt>" } (bare JWT string), decoding claims without signature verification to extract userId from userId or sub, defaulting type to Bearer. If no token or user identifier exists, login throws.
DefaultAuthProvider.saveSession delegates to DefaultAuthService.saveAuth (packages/kernel/src/base/services/auth.ts), writing to localStorage:
LocalStorageKeys.KEY_AUTH_TOKEN(@app/auth/token):JSON.stringify({ value, type, provider }).LocalStorageKeys.KEY_AUTH_IDENTITY(@app/auth/identity):JSON.stringify({ userId, username, referenceId, provider }).LocalStorageKeys.KEY_AUTH_PERMISSION(@app/auth/permission):JSON.stringify(roles).
readRoleNames preserves string entries as role names, and reads identifier or name on object entries.
2. Authentication container bindings
AuthConfiguration establishes the authentication pipeline:
CoreBindings.REST_DATA_PROVIDER_OPTIONS: providesIRestDataProviderOptions(packages/kernel/src/common/types.ts). List/auth/sign-ininnoAuthPaths; omitting it causesDefaultNetworkRequestServiceto throw 401 before sending because no token exists in storage.CoreBindings.AUTH_PROVIDER_OPTIONS: providesIAuthProviderOptionswith post-login redirectpaths.afterLogin(default/).CoreBindings.DEFAULT_AUTH_SERVICE: providesDefaultAuthService, managing tokens and identity inlocalStorage.CoreBindings.DEFAULT_AUTH_PROVIDER: providesDefaultAuthProviderfor react-admin.CoreBindings.DEFAULT_REST_DATA_PROVIDER: provides the data provider executing network calls.
See Configuration.
3. The login page
The login page submits credentials using useLogin from ra-core:
import { useState, type FormEvent } from 'react';
import { useLogin, useNotify } from 'ra-core';
export const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const submit = (event: FormEvent) => {
event.preventDefault();
login({ username, password }).catch(() => {
notify('ra.auth.sign_in_error', { type: 'error' });
});
};
return (
<form onSubmit={submit}>
<h1>Sign in</h1>
<label>
Username <input value={username} onChange={(e) => setUsername(e.target.value)} />
</label>
<label>
Password <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</label>
<button type="submit">Sign in</button>
</form>
);
};Pass LoginPage to the loginPage prop of ArdorApplication (Quickstart). When an unauthenticated user accesses a protected route, DefaultAuthProvider.checkAuth() checks storage via authService.getAuth(). If no valid token exists, checkAuth rejects with { redirectTo: '/login' }.
4. Protecting pages and components by role
DefaultAuthProvider exposes inspection methods:
checkAuth: resolves if a token exists inlocalStorage, or issuesGETtopaths.checkAuthif configured.getRoles: resolves withSet<string>of stored roles.getIdentity: reads@app/auth/identitythroughauthService.getUser()({ userId, username, referenceId, provider }).getPermissions: resolves withPromise.resolve().
To authorize UI components and resources through react-admin's canAccess contract, bind IPermissionProvider under CoreBindings.PERMISSION_PROVIDER. Unbound, canAccess remains undefined and react-admin permits access to all actions.
import {
configuration,
CoreBindings,
DefaultAuthService,
inject,
type IPermissionProvider,
provide,
} from '@venizia/ardor';
@configuration()
export class PermissionConfiguration {
constructor(
@inject({ key: CoreBindings.DEFAULT_AUTH_SERVICE })
private readonly authService: DefaultAuthService,
) {}
@provide({ key: CoreBindings.PERMISSION_PROVIDER })
permissionProvider(): IPermissionProvider {
return {
canAccess: async ({ resource, action }) => {
const roles = this.authService.getRoles();
return roles.has('admin') || (roles.has('editor') && action !== 'delete');
},
};
}
}Components check permissions with useCanAccess from ra-core. See Permission provider.
5. Token recovery and refresh
When a request returns HTTP 401, DefaultNetworkRequestService asks AuthRecovery.canRecover({ paths }): the endpoint must not be a no-auth path (isNoAuthPath) or match authRecovery.refreshTokenPath.
If both pass, refresh() batches concurrent 401 failures into a single in-flight promise and calls refreshToken(). Retried once if refreshToken() resolves true. If refreshToken() throws, onAuthFailure() runs once, the refresh resolves false, and the 401 error is raised.
Set authRecovery: { refreshTokenPath: '/auth/token/refresh' } in REST_DATA_PROVIDER_OPTIONS. The default refreshToken() POSTs to that path with no body, reads the new token from the answer and saves it through the auth service, so the session hooks run and the kept identity and roles stay. It needs no subclass. The expired token is attached to the refresh request, which is what an endpoint that reads the session from the token needs. Add the path to noAuthPaths only when the server identifies the session by other means, such as a cookie: that keeps the token off the request.
Write a DefaultNetworkRequestService subclass only for a refresh the default does not cover: a request body, another answer shape, or a token store that is not the auth service. See The default refresh.
useRefreshToken from @venizia/ardor calls DefaultAuthProvider.refreshToken(), which delegates to refresh() on the network service and shares the same coalesced promise. See No-auth paths and token recovery.
6. Sign out and error handling
To log out, call useLogout from ra-core. DefaultAuthProvider.logout() invokes DefaultAuthService.cleanUp(), deleting all keys starting with @app/auth/ from localStorage.
DefaultAuthProvider.checkError evaluates HTTP errors raised by the data provider or react-admin:
- Status 401: calls
authService.cleanUp()and rejects with{ redirectTo: '/login' }. - Status 403: rejects with
{ redirectTo: '/unauthorized', logoutUser: false }.
7. Custom sign-in route and body
Set paths.signIn in IAuthProviderOptions and add the matching path to noAuthPaths in IRestDataProviderOptions. To modify the request body format, subclass DefaultAuthProvider and override buildSignInBody:
import { type AnyType, DefaultAuthProvider } from '@venizia/ardor';
export class CustomAuthProvider extends DefaultAuthProvider {
protected override buildSignInBody(opts: { params: AnyType }) {
return {
identifier: { scheme: 'email', value: opts.params?.email },
credential: { scheme: 'basic', value: opts.params?.password },
};
}
}To send the login form directly without schema wrapping, return opts.params.
NOTE
If an application requires no authentication at all, leave CoreBindings.DEFAULT_AUTH_PROVIDER unbound, omit loginPage on ArdorApplication, and configure noAuthPathRegex: /./ on REST_DATA_PROVIDER_OPTIONS.