@leancodepl/kratos
v10.1.0
Published
Headless React components library for building Ory Kratos authentication flows
Readme
@leancodepl/kratos
Headless React components library for building Ory Kratos authentication flows.
Installation
npm install @leancodepl/kratos
# or
yarn add @leancodepl/kratosAPI
mkKratos(queryClient, basePath, traits, SessionManager, oidcProviders)
Creates a Kratos client factory with authentication flows, session management, and React providers.
Parameters:
queryClient: QueryClient- React Query client instance for managing server statebasePath: string- Base URL for the Kratos API servertraits?: TTraitsConfig- Optional traits configuration object for user schema validationSessionManager?: new (props: BaseSessionManagerContructorProps) => TSessionManager- Optional session manager constructor, defaults to BaseSessionManageroidcProviders?: readonly OidcProviderConfig[]- Optional array of custom OIDC provider configurations. Each provider should have anid(matching Kratos provider ID)
Returns: Object with the following structure:
flows
Authentication flow components and hooks:
useLogout()- Hook providing logout functionality with optional returnTo parameterLoginFlow- Complete login flow component with multi-step authentication supportRecoveryFlow- Password recovery flow component with email verification and resetRegistrationFlow- User registration flow component with traits collection and verificationSettingsFlow- User settings flow component for account management and profile updatesVerificationFlow- Email verification flow component
providers
React context providers for the application:
KratosProviders- Composite provider that wraps your app with necessary Kratos context- Includes
KratosClientProviderfor API access - Includes
KratosSessionProviderfor session management
- Includes
session
Session management utilities:
sessionManager- Session manager instance with methods and hooks for:- Async Methods:
getSession(),getIdentity(),getUserId(),isLoggedIn(),checkIfLoggedIn() - React Hooks:
useSession(),useIdentity(),useUserId(),useIsLoggedIn(),useIsAal2Required() - Extensible: Can be extended to provide trait-specific methods like
useEmail(),useFirstName()for typed user data access
- Async Methods:
BaseSessionManager(queryClient, api)
Manages Ory Kratos session and identity state with React Query integration.
Parameters:
queryClient: QueryClient- React QueryQueryClientinstance for caching and fetching session dataapi: FrontendApi- Ory KratosFrontendApiinstance for session and identity requests
Usage Examples
Basic Setup
To use the library, you need to create new instance of Kratos client with mkKratos factory:
// traits.ts
export const traitsConfig = {
Email: { trait: "email", type: "string" },
GivenName: { trait: "given_name", type: "string" },
RegulationsAccepted: { trait: "regulations_accepted", type: "boolean" },
} as const
export type AuthTraitsConfig = typeof traitsConfig// kratosService.ts
import { environment } from "./environments"
import { queryClient } from "./queryService"
import { traitsConfig } from "./traits"
const {
session: { sessionManager },
providers: { KratosProviders },
flows: { LoginFlow, RegistrationFlow, SettingsFlow, VerificationFlow, RecoveryFlow, useLogout },
} = mkKratos({
queryClient,
basePath: environment.authUrl,
traits: traitsConfig,
})
// session
export { sessionManager }
// providers
export { KratosProviders }
// flows
export { LoginFlow, RecoveryFlow, RegistrationFlow, SettingsFlow, useLogout, VerificationFlow }And then wrap your app with KratosProviders from mkKratos:
// main.tsx
import { QueryClientProvider } from "@tanstack/react-query"
import { KratosProviders } from "./kratosService"
import { queryClient } from "./queryService"
function App() {
return (
<QueryClientProvider client={queryClient}>
<KratosProviders>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</KratosProviders>
</QueryClientProvider>
)
}Extending session manager
You can add new functionalities to the session manager by extending BaseSessionManager class:
// session.ts
import { BaseSessionManager } from "@leancodepl/kratos"
import { queryClient } from "./queryService"
import type { AuthTraitsConfig } from "./traits"
export class SessionManager extends BaseSessionManager<AuthTraitsConfig> {
getTraits = async () => {
const identity = await this.getIdentity()
return identity?.traits
}
getEmail = async () => {
const traits = await this.getTraits()
return traits?.email
}
// Hooks for React components
useTraits = () => {
const { identity, isLoading, error } = this.useIdentity()
return {
traits: identity?.traits,
isLoading,
error,
}
}
useEmail = () => {
const { traits, isLoading, error } = this.useTraits()
return {
email: traits?.email,
isLoading,
error,
}
}
}// kratosService.ts
import { environment } from "./environments"
import { queryClient } from "./queryService"
import { SessionManager } from "./session"
import { traitsConfig } from "./traits"
const {
session: { sessionManager },
providers: { KratosProviders },
flows: { LoginFlow, RegistrationFlow, SettingsFlow, VerificationFlow, RecoveryFlow, useLogout },
} = mkKratos({
queryClient,
basePath: environment.authUrl,
traits: traitsConfig,
SessionManager,
})Custom OIDC Providers
You can configure custom OIDC providers (like Microsoft, GitHub, etc.) in addition to the default providers (Apple, Google, Facebook):
// kratosService.ts
import { mkKratos } from "@leancodepl/kratos"
import { environment } from "./environments"
import { queryClient } from "./queryService"
import { traitsConfig } from "./traits"
const {
session: { sessionManager },
providers: { KratosProviders },
flows: { LoginFlow, RegistrationFlow, SettingsFlow },
} = mkKratos({
queryClient,
basePath: environment.authUrl,
traits: traitsConfig,
oidcProviders: [
{ id: "microsoft" },
{ id: "github" },
{ id: "reddit" },
],
})The OIDC providers are automatically made available in the flow forms as capitalized component properties. For example, a provider with id: "microsoft" will be available as oidcProviders.Microsoft.
With TypeScript, the library generates type-safe provider types from your configuration, ensuring you can only access providers you've configured:
// kratosService.ts
const oidcProvidersConfig = [
{ id: "microsoft" },
{ id: "github" },
{ id: "reddit" },
] as const
const {
session: { sessionManager },
providers: { KratosProviders },
flows: { LoginFlow, RegistrationFlow, SettingsFlow },
} = mkKratos({
queryClient,
basePath: environment.authUrl,
traits: traitsConfig,
oidcProviders: oidcProvidersConfig,
})
export type OidcProvidersConfig = typeof oidcProvidersConfig// loginPage.tsx
import { loginFlow } from "@leancodepl/kratos"
import type { OidcProvidersConfig } from "./kratosService"
// Note: Provider IDs are capitalized (first letter only) in the component names.
// For example, "github" becomes "Github", "microsoft" becomes "Microsoft".
function ChooseMethodForm(props: loginFlow.ChooseMethodFormProps<OidcProvidersConfig>) {
const { oidcProviders: { Microsoft, Github, Reddit }, isSubmitting, isValidating } = props
return (
<div>
{Microsoft && (
<Microsoft>
<button disabled={isSubmitting || isValidating}>Sign in with Microsoft</button>
</Microsoft>
)}
{Github && (
<Github>
<button disabled={isSubmitting || isValidating}>Sign in with GitHub</button>
</Github>
)}
{Reddit && (
<Reddit>
<button disabled={isSubmitting || isValidating}>Sign in with Reddit</button>
</Reddit>
)}
</div>
)
}The same pattern applies to registration and settings flows. Only providers configured in your Kratos instance will be available in the oidcProviders object, and TypeScript will provide autocomplete and type checking for the available providers.
Session Management
import { sessionManager } from "./kratosService"
function UserProfile() {
const { isLoggedIn, isLoading } = sessionManager.useIsLoggedIn()
const { userId } = sessionManager.useUserId()
const { email } = sessionManager.useEmail()
const { firstName } = sessionManager.useFirstName()
if (isLoading) return <div>Loading...</div>
if (!isLoggedIn) return <div>Not logged in</div>
return (
<div>
<h1>User Profile</h1>
<p>ID: {userId}</p>
<p>Email: {email}</p>
<p>Name: {firstName}</p>
</div>
)
}Login Flow
import { LoginFlow } from "./kratosService"
function LoginPage() {
return (
<LoginFlow
loaderComponent={Loader}
chooseMethodForm={ChooseMethodForm}
secondFactorForm={SecondFactorForm}
secondFactorEmailForm={SecondFactorEmailForm}
emailVerificationForm={EmailVerificationForm}
returnTo="/identity"
onLoginSuccess={() => {
console.log("Login successful")
}}
onSessionAlreadyAvailable={() => {
location.href = "/identity"
}}
onError={({ target, errors }) => {
console.error(`Error in ${target}:`, errors)
}}
/>
)
}Registration Flow
import { RegistrationFlow } from "./kratosService"
function RegisterPage() {
return (
<RegistrationFlow
chooseMethodForm={ChooseMethodForm}
emailVerificationForm={EmailVerificationForm}
traitsForm={TraitsForm}
returnTo="/welcome"
onRegistrationSuccess={() => {
console.log("Registration successful")
}}
onVerificationSuccess={() => {
console.log("Email verified")
}}
onError={({ target, errors }) => {
console.error(`Registration error in ${target}:`, errors)
}}
/>
)
}Settings Flow
import { SettingsFlow, settingsFlow } from "./kratosService"
function SettingsPage() {
const { isLoggedIn, isLoading } = sessionManager.useIsLoggedIn()
if (isLoading) return <div>Loading...</div>
if (!isLoggedIn) return <div>Access denied</div>
return (
<SettingsFlow
newPasswordForm={NewPasswordForm}
traitsForm={TraitsForm}
passkeysForm={PasskeysForm}
totpForm={TotpForm}
oidcForm={OidcForm}
settingsForm={SettingsForm}
onChangePasswordSuccess={() => {
console.log("Password updated")
}}
onChangeTraitsSuccess={() => {
console.log("Profile updated")
}}
/>
)
}
function SettingsForm({ isLoading, traitsForm, newPasswordForm, passkeysForm }: settingsFlow.SettingsFormProps) {
if (isLoading) {
return <div>...loading</div>
}
return (
<div>
{traitsForm}
{newPasswordForm}
{passkeysForm}
</div>
)
}Logout Functionality
import { useLogout } from "./kratosService"
function LogoutButton() {
const { logout } = useLogout()
const handleLogout = async () => {
const result = await logout({ returnTo: "/login" })
if (result.isSuccess) {
console.log("Logout successful")
} else {
console.error("Logout failed:", result.error)
}
}
return <button onClick={handleLogout}>Logout</button>
}Recovery Flow
import { RecoveryFlow } from "./kratosService"
function RecoveryPage() {
return (
<RecoveryFlow
emailForm={EmailForm}
codeForm={CodeForm}
newPasswordForm={NewPasswordForm}
onRecoverySuccess={() => {
console.log("Password recovery completed")
}}
onError={error => {
console.error("Recovery failed:", error)
}}
/>
)
}Custom Error Handling
import { AuthError } from "@leancodepl/kratos"
function getErrorMessage(error: AuthError) {
switch (error.id) {
case "Error_InvalidCredentials":
return "Invalid email or password"
case "Error_MissingProperty":
return "This field is required"
case "Error_TooShort":
return "Password is too short"
default:
return "An error occurred"
}
}
function handleError({ target, errors }) {
if (target === "root") {
console.error("Form errors:", errors.map(getErrorMessage))
} else {
console.error(`Field ${target} errors:`, errors.map(getErrorMessage))
}
}Implementing flow form
Each form in every flow exposes slot components for inputs
and buttons. When wrapping your custom components with these slots, relevant props (such as the onInput event handler)
are automatically applied. If you use a custom component as a child, you receive props from one of the following types:
CommonInputFieldProps, CommonCheckboxFieldProps, or CommonButtonProps.
Forms like TraitsForm have dynamic parameters determined by the traitsConfig you provide to the mkKratos factory.
To type these correctly, use a generic such as registrationFlow.TraitsFormProps<AuthTraitsConfig> and pass your traits
config.
Some forms are typed as discriminated unions with multiple variants.
Custom Input component with CommonInputFieldProps
import { CommonInputFieldProps } from "@leancodepl/kratos"
import { getErrorMessage } from "./kratosService"
type InputProps = CommonInputFieldProps & { placeholder?: string }
export const Input = ({ errors, ...props }: InputProps) => (
<div>
<input {...props} />
{errors && errors.length > 0 && (
<div>
{errors.map(error => (
<div key={error.id}>{getErrorMessage(error)}</div>
))}
</div>
)}
</div>
)ChooseMethodForm in login flow
import { loginFlow } from "@leancodepl/kratos"
import { LoginFlow, getErrorMessage } from "./kratosService"
import type { AuthTraitsConfig } from "./traits"
import type { OidcProvidersConfig } from "./kratosService"
function LoginPage() {
return (
<LoginFlow
chooseMethodForm={ChooseMethodForm}
// other props
/>
)
}
function ChooseMethodForm(props: loginFlow.ChooseMethodFormProps<OidcProvidersConfig>) {
const { errors, isSubmitting, isValidating, oidcProviders: { Google, Apple, Facebook } } = props
if (props.isRefresh) {
const { passwordFields, Passkey, identifier } = props
return (
<div>
{identifier && (
<h2>
Complete login process as <strong>{identifier}</strong>
</h2>
)}
{passwordFields && (
<>
<passwordFields.Password>
<input placeholder="Password" />
</passwordFields.Password>
<passwordFields.Submit>
<button>Login</button>
</passwordFields.Submit>
</>
)}
{Google && (
<Google>
<button>Sign in with Google</button>
</Google>
)}
{Apple && (
<Apple>
<button>Sign in with Apple</button>
</Apple>
)}
{Facebook && (
<Facebook>
<button>Sign in with Facebook</button>
</Facebook>
)}
{Passkey && (
<Passkey>
<button>Sign in with Passkey</button>
</Passkey>
)}
{errors && errors.length > 0 && (
<div>
{errors.map(error => (
<div key={error.id}>{getErrorMessage(error)}</div>
))}
</div>
)}
</div>
)
}
const {
passwordFields: { Identifier, Password, Submit },
Passkey,
} = props
return (
<div>
<Identifier>
<input placeholder="Identifier" />
</Identifier>
<Password>
<input placeholder="Password" />
</Password>
<Submit>
<button>Login</button>
</Submit>
{Google && (
<Google>
<button>Sign in with Google</button>
</Google>
)}
{Apple && (
<Apple>
<button>Sign in with Apple</button>
</Apple>
)}
{Facebook && (
<Facebook>
<button>Sign in with Facebook</button>
</Facebook>
)}
<Passkey>
<button>Sign in with Passkey</button>
</Passkey>
{errors && errors.length > 0 && (
<div>
{errors.map(error => (
<div key={error.id}>{getErrorMessage(error)}</div>
))}
</div>
)}
</div>
)
}TraitsForm in registration flow
import { registrationFlow } from "@leancodepl/kratos"
import { RegistrationFlow, getErrorMessage } from "./kratosService"
import type { AuthTraitsConfig, OidcProvidersConfig } from "./kratosService"
function RegisterPage() {
return (
<RegistrationFlow
traitsForm={TraitsForm}
onError={handleError}
// other props
/>
)
}
function TraitsForm({
errors,
oidcProviders: { Google, Apple, Facebook },
traitFields: { Email, GivenName, RegulationsAccepted, Submit },
isSubmitting,
isValidating,
}: registrationFlow.TraitsFormProps<AuthTraitsConfig, OidcProvidersConfig>) {
return (
<div>
<Email>
<input placeholder="Email" />
</Email>
<GivenName>
<input placeholder="First name" />
</GivenName>
<RegulationsAccepted>
<input placeholder="Regulations accepted" type="checkbox">
I accept the regulations
</input>
</RegulationsAccepted>
<Submit>
<button>Register</button>
</Submit>
{Google && (
<Google>
<button>Sign up with Google</button>
</Google>
)}
{Apple && (
<Apple>
<button>Sign up with Apple</button>
</Apple>
)}
{Facebook && (
<Facebook>
<button>Sign up with Facebook</button>
</Facebook>
)}
{errors && errors.length > 0 && (
<div>
{errors.map(error => (
<div key={error.id}>{getErrorMessage(error)}</div>
))}
</div>
)}
</div>
)
}
const handleError: registrationFlow.OnRegistrationFlowError<AuthTraitsConfig> = ({ target, errors }) => {
if (target === "root") {
console.error("Form errors:", errors.map(getErrorMessage))
} else {
console.error(`Field ${target} errors:`, errors.map(getErrorMessage))
}
}