> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Standalone Authentication

> Sign merchants in from an app running on your own domain, and call the JTL Cloud API on their behalf.

An app that runs on your own domain signs merchants in itself, against the platform identity provider. It receives an access token, an ID token, and a refresh token, and uses the access token to call the JTL Cloud API.

This is the browser sign-in flow described in [Architecture Overview](/cloud/guides/cloud-apps/architecture-overview#browser-sign-in). Apps that render inside the Hub or Cloud ERP receive the same token through AppBridge instead. See [App Token Authentication](/cloud/guides/cloud-apps/app-token-authentication) for that flow.

An app with no frontend or fullstack authenticates as itself with a service account. See [Service Account Authentication](/cloud/guides/cloud-apps/service-account-authentication) for that flow.

### Adding Sign-In to an Existing App

To get started, install the JTL's auth library:

```bash theme={null}
npm install @jtl-software/cloud-apps-auth
```

## Create the Manifest and Register your App

You must register your app with JTL before it can access JTL resources.

Create an `app.json` file in your frontend project root:

```json theme={null}
{
	"version": "1.0.0",
	"technicalName": "standalone-app",
	"lifecycle": {},
	"authentication": {
		"publicClient": {
			"redirectUris": ["https://standalone-app.example.com/callback"],
			"postLogoutRedirectUris": ["https://standalone-app.example.com/"]
		}
	},
	"capabilities": {
		"hub": {
			"appLauncher": {}
		},
		"erp": {}
	}
}
```

The `publicClient` object tells JTL this is a browser-based authentication flow, where the user signs in through the JTL identity provider and is redirected back to your app after authentication.

`redirectUris` defines the allowed URLs where JTL can send the user after signing in. `postLogoutRedirectUris` defines where the user can be redirected after signing out.
See [App Manifest: Authentication](/cloud/guides/cloud-apps/app-manifest#authentication) for the full field list.

Next, create a `.env` file in your frontend project root:

```bash theme={null}
VITE_JTL_ISSUER=.... # the JTL identity provider
VITE_JTL_CLIENT_ID=.... # the public client ID for your app
```

The registration command reads your `app.json` and `.env` configuration and uses them to register the app with JTL.

Run:

```bash theme={null}
npx -y @jtl-software/create-cloud-app@latest register
```

<Tip>
  You can also use the registration wizard on [Partner Portal](https://partner.jtl-cloud.com) to register your app.
</Tip>

## Configure the Environment

Once your app is registered, copy the public client ID in the Partner Portal and add it to your environment variables:

```bash .env theme={null}
VITE_JTL_ISSUER=.... # the JTL identity provider
VITE_JTL_CLIENT_ID=.... # the public client ID for your app
```

If you're using the CLI to register the app, it will automatically generate a `.env` file and populate it with the required values.

## Wrap your App

The provider holds the session and makes it available to the rest of your component tree. Mount it above your router, passing the values from your environment.

```tsx main.tsx theme={null}
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { JtlAuthProvider } from '@jtl-software/cloud-apps-auth';
import App from './App';

createRoot(document.getElementById('root')!).render(
	<StrictMode>
		<JtlAuthProvider
			issuer={import.meta.env.VITE_JTL_ISSUER}
			clientId={import.meta.env.VITE_JTL_CLIENT_ID}
		>
			<App />
		</JtlAuthProvider>
	</StrictMode>,
);
```

Until your app is registered and `clientId` has a value, the provider renders its children unchanged.

## Protect a Route

`RequireJtlAuth` renders its children only when a merchant is signed in. Anyone reaching a protected route without a session is sent to the identity provider and returned to the same route afterwards.

```tsx App.tsx theme={null}
import { RequireJtlAuth } from '@jtl-software/cloud-apps-auth';
import DashboardPage from './pages/DashboardPage';

const App: React.FC = () => (
	<RequireJtlAuth>
		<DashboardPage />
	</RequireJtlAuth>
);

export default App;
```

Above the gated content, `RequireJtlAuth` renders a top bar showing the signed-in merchant and a sign-out control. To place that bar yourself, use the `JtlAuthBar` component directly.

Gate the routes that run on your own domain. Routes that render inside the Hub or Cloud ERP receive identity through AppBridge and are not gated this way.

## Read the Signed-In User

`useJtlAuth` returns the current session. The `profile` object carries the ID token claims, and `access_token` is the credential you send to the JTL Cloud API.

```tsx theme={null}
import { useJtlAuth } from '@jtl-software/cloud-apps-auth';

const DashboardPage: React.FC = () => {
	const { user } = useJtlAuth();

	if (!user) return null;

	const { profile } = user;
	const accessToken = user.access_token;

	return (
		<div>
			<h1>{profile.name ?? profile.email}</h1>
		</div>
	);
};
```

Inside `RequireJtlAuth` a session always exists, so the null check guards only the moment before the provider resolves.

## Fetch the User Profile

The ID token carries basic identity claims. The provider's userinfo endpoint returns the full set, including the tenant the merchant belongs to.

```tsx theme={null}
import { useEffect, useState } from 'react';
import { fetchUserInfo, useJtlAuth } from '@jtl-software/cloud-apps-auth';

const issuer = import.meta.env.VITE_JTL_ISSUER ?? '';

const useJtlUserInfo = () => {
	const { user } = useJtlAuth();
	const accessToken = user?.access_token;

	const [userInfo, setUserInfo] = useState<Record<string, unknown> | null>(
		null,
	);
	const [error, setError] = useState<string | null>(null);

	useEffect(() => {
		if (!accessToken || !issuer) return;
		let cancelled = false;

		setUserInfo(null);
		setError(null);

		fetchUserInfo(issuer, accessToken)
			.then((info) => !cancelled && setUserInfo(info))
			.catch(
				(err) =>
					!cancelled &&
					setError(err instanceof Error ? err.message : String(err)),
			);

		return () => {
			cancelled = true;
		};
	}, [accessToken]);

	return { userInfo, error };
};
```

The response contains the standard OpenID Connect claims alongside JTL-specific ones:

```json theme={null}
{
	"sub": "8f14e45f-ceea-4c2b-9b1e-3a7d2f6b0c11",
	"email": "merchant@example.com",
	"email_verified": true,
	"name": "Alex Beispiel",
	"given_name": "Alex",
	"family_name": "Beispiel",
	"preferred_username": "merchant@example.com",
	"locale": null,
	"updated_at": 1787157038,
	"urn:jtl:tenant_id": "3c9a1d70-52b8-4f6e-8d21-b4e7f09a5c33",
	"urn:jtl:kundencenter_id": "1000000"
}
```

| Claim                     | Type           | Description                                                       |
| ------------------------- | -------------- | ----------------------------------------------------------------- |
| `sub`                     | string (UUID)  | Stable identifier for the merchant across sessions                |
| `email`                   | string         | Merchant's email address                                          |
| `email_verified`          | boolean        | Whether the address has been confirmed                            |
| `name`                    | string         | Full display name                                                 |
| `given_name`              | string         | First name                                                        |
| `family_name`             | string         | Last name                                                         |
| `preferred_username`      | string         | Username the merchant signs in with                               |
| `locale`                  | string or null | Preferred locale, when set                                        |
| `updated_at`              | integer        | Unix timestamp of the last profile change                         |
| `urn:jtl:tenant_id`       | string (UUID)  | Tenant the merchant belongs to. Required for JTL Cloud API calls. |
| `urn:jtl:kundencenter_id` | string         | Merchant's JTL customer account number                            |

## Call the JTL-Wawi API

API calls carry the access token and the tenant the merchant belongs to. Read the tenant from `urn:jtl:tenant_id` in the userinfo response.

```tsx theme={null}
const fetchItems = async (accessToken: string, tenantId: string) => {
	const response = await fetch('https://api.jtl-cloud.com/erp/v2/graphql', {
		method: 'POST',
		headers: {
			Authorization: `Bearer ${accessToken}`,
			'X-Tenant-ID': tenantId,
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			query: `
        query Items($first: Int!) {
          items(first: $first) {
            nodes {
              id
              name
            }
          }
        }
      `,
			variables: { first: 20 },
		}),
	});

	if (!response.ok) {
		throw new Error(`Request failed with status ${response.status}`);
	}

	return response.json();
};
```

The library refreshes the access token as it approaches expiry, so read `user.access_token` at call time rather than holding a copy in component state.

Optionally, you can use the `getTokenInformation` function to get the token information.

```tsx theme={null}
import { getTokenInformation } from '@jtl-software/cloud-apps-auth';

const info = await getTokenInformation(token);
// info?.userId, info?.organizationId, info?.expiresAt
// info?.username, info?.name, info?.email
```

The `organizationId` in the token information is the same as the `tenantId`.

## Sign Out

`RequireJtlAuth` includes a sign-out control in its top bar. To sign out from elsewhere in your app, call `signoutRedirect` from the session hook.

```tsx theme={null}
import { useJtlAuth } from '@jtl-software/cloud-apps-auth';
import { Button } from '@jtl-software/platform-ui-react';

const SignOutButton: React.FC = () => {
	const { signoutRedirect } = useJtlAuth();

	return (
		<Button
			label='Sign out'
			variant='default'
			onClick={() => signoutRedirect()}
		/>
	);
};
```

Sign-out clears the local session and redirects to the identity provider, which returns the merchant to one of the URLs declared in `postLogoutRedirectUris`. Without an entry, sign-out ends on the identity provider rather than back in your app.

## Common Issues

Common failures and what causes them.

<AccordionGroup>
  <Accordion title="Sign-in fails with a redirect URI error">
    The identity provider matches redirect URIs exactly, so the URL your app
    sends must be present in your manifest character for character. A trailing
    slash, a different port, or `http` where the manifest declares `https` all
    count as a mismatch. Check the value in your manifest against the URL in the
    browser address bar when the error appears, and register a new app version
    if they differ.
  </Accordion>

  <Accordion title="The merchant returns to the sign-in page repeatedly">
    This happens when the session cannot be stored, usually because the app is
    served over `http` on a host other than localhost, or because browser
    storage is blocked. Serve the app over `https` in any environment other than
    local development, and confirm that third-party storage restrictions are not
    applying to your domain.
  </Accordion>

  <Accordion title="The userinfo request returns 401">
    The access token has expired or was rejected. Read `user.access_token` at
    the point of the call rather than capturing it once, so the value reflects
    the most recent refresh. If the token is current and the request still
    fails, confirm that `VITE_JTL_ISSUER` points at the same environment your
    app is registered in.
  </Accordion>

  <Accordion title="A protected route shows a configuration hint instead of signing in">
    The client ID is missing, so the library has nothing to authenticate
    against. Confirm that `VITE_JTL_CLIENT_ID` is set and that the provider
    receives it. A freshly scaffolded app has no client ID until it is
    registered, so run `npm run register` first.
  </Accordion>

  <Accordion title="The sign-in top bar renders without styling">
    Tailwind has not scanned the library's `dist` directory, so its utility
    classes were never generated. Add the `@source` directive for
    `@jtl-software/cloud-apps-auth/dist` to your CSS alongside the one for
    Platform UI, then restart the dev server.
  </Accordion>

  <Accordion title="API calls return a tenant error">
    The `X-Tenant-ID` header is missing or does not match a tenant the merchant
    belongs to. Read the value from `urn:jtl:tenant_id` in the userinfo
    response.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Using Platform APIs" icon="code" href="/cloud/guides/cloud-apps/using-platform-apis">
    Query the JTL Cloud and JTL-Wawi APIs with the right headers and scoping.
  </Card>

  <Card title="App Manifest" icon="file-braces" href="/cloud/guides/cloud-apps/app-manifest">
    Declare the public client, redirect URIs, and capabilities your app needs.
  </Card>

  <Card title="Architecture Overview" icon="git-fork" href="/cloud/guides/cloud-apps/architecture-overview">
    Compare the integration types and authentication flows available to your
    app.
  </Card>

  <Card title="Platform UI" icon="palette" href="/cloud/guides/cloud-apps/platform-ui">
    Build your interface with the JTL component library.
  </Card>
</CardGroup>
