Skip to main content

Integrate the User Service Without the React Library

Introduction

Most of the User Service guides show how to integrate authentication using the @axinom/mosaic-user-auth React library. The library is the recommended path for web and React Native applications because it takes care of the token caching, cookie handling, and renewal timing for you.

There are cases where the React library is not an option, for example a native mobile app on iOS or Android, a game console, a server-side backend, or any client built on a stack the library does not support. This article describes the underlying HTTP and GraphQL contract that the library talks to, so you can integrate the same flows from any platform.

Everything below is a plain HTTP interaction. The examples use generic requests (shown with curl and raw GraphQL) instead of any specific language or framework.

What you need before you start

To talk to the User Service you need the identifiers of your application, which an administrator configures in the Admin Portal:

  • Tenant ID - a UUID identifying your tenant.
  • Environment ID - a UUID identifying the environment.
  • Application ID - a UUID identifying the end-user application.

You also need a few base URLs. The User Service exposes two kinds of surfaces:

  • Authentication endpoints (https://user-auth.service.eu.axinom.net) - the OAuth redirect flow, the token endpoint, and the sign-in/sign-out endpoints. These use browser cookies to keep a session.
  • GraphQL APIs (https://user.service.eu.axinom.net) - the End-User API (/graphql) for profile operations, and the Management API (/graphql-management) for application-level operations. An additional AxAuth Management GraphQL API handles sign-up and password reset.

The authentication endpoints and the GraphQL APIs are served from different hosts. Browser applications do not call the authentication host directly: because the session cookie is same-site, they reach the authentication endpoints through a reverse proxy on their own domain (see Set Up an Authentication Proxy). Server-side and native clients, which are not subject to browser cookie rules, may call the authentication host https://user-auth.service.eu.axinom.net directly.

Example hosts used in this guide

The browser-facing examples send authentication requests to https://auth.example.com, a placeholder for your own proxy domain. Replace it with your actual proxy DNS. GraphQL examples use the real host https://user.service.eu.axinom.net.

Rather than hard-coding the GraphQL URLs, discover them from the well-known document described next.

Discover the endpoints

Send a GET request to the /.well-known endpoint of the authentication base URL. No authentication is required.

curl https://auth.example.com/.well-known

The response lists the endpoints you will use in the rest of this guide:

{
"userServiceManagementGQL": "https://user.service.eu.axinom.net/graphql-management",
"userServiceEndUserGQL": "https://user.service.eu.axinom.net/graphql",
"axAuthManagementGQL": "https://<axauth-service>/graphql",
"axAuthEndpoint": "https://<axauth-service>/"
}
  • userServiceEndUserGQL - the End-User GraphQL API, used with an end-user access token to manage profiles.
  • userServiceManagementGQL - the Management GraphQL API, used for operations such as authenticating an end-user application.
  • axAuthManagementGQL - the AxAuth Management GraphQL API, used for the standalone sign-up and password reset flows.

The axAuth* values point at a separate AxAuth host and can differ per environment, so always read them from this response rather than hard-coding them.

The authentication model

The authentication endpoints are grouped under a base URL you register for your application (referred to below as the auth base URL). When a user signs in, the service stores a long-lived refresh token and returns it to the browser as an HttpOnly cookie named AX_REFRESH_TOKEN. The cookie is scoped to the path /<tenantId>/<environmentId>/<applicationId>, so the same browser can hold separate sessions for several applications.

You never read the refresh cookie directly. Instead, you call the token endpoint, which reads the cookie and returns a short-lived access token. That access token is the credential you attach to all Mosaic end-user API calls.

Two rules apply to the browser requests:

  • Send requests to these endpoints with credentials (cookies) included, so the AX_REFRESH_TOKEN cookie travels with them.
  • The auth base URL and the URLs your application redirects to must be registered for the application (as allowed proxy URLs and allowed origins). Requests from unregistered URLs are rejected.

Sign in with an external identity provider

This is the redirect based flow used for Google, Apple, Facebook, and any custom OAuth 2.0 / OpenID Connect provider.

1. List the configured identity providers

curl "https://auth.example.com/<tenantId>/<environmentId>/<applicationId>/get-user-auth-idp-config"
{
"code": "SUCCESS",
"availableIdentityProviders": [
{
"idpConnectionId": "1d0b...",
"providerId": "AX_GOOGLE",
"title": "Google",
"providerIconUrl": "https://...",
"sortOrder": 1,
"clientId": "..."
}
]
}

Render a sign-in button per provider. The code can also be NO_ACTIVE_IDPS when no providers are configured for the application.

2. Redirect the user to the authorization endpoint

When the user picks a provider, send the browser to the /oauth endpoint with the following query parameters:

ParameterDescription
tenantIdYour tenant ID.
environmentIdYour environment ID.
applicationIdYour application ID.
idpConnectionIdThe idpConnectionId of the chosen provider from step 1.
originUrlWhere to send the user after sign-in completes. Must be a registered origin for the app.
userAuthProxyUrlYour proxy's base URL, on your own domain (this guide uses https://auth.example.com). Must be registered as an allowed proxy URL for the app.
encryptionKeyNative apps only (see Native applications). Omit for web apps.
https://auth.example.com/oauth?tenantId=<tenantId>&environmentId=<environmentId>&applicationId=<applicationId>&idpConnectionId=<idpConnectionId>&originUrl=<originUrl>&userAuthProxyUrl=https://auth.example.com

The service redirects the browser to the identity provider, using the OAuth 2.0 Authorization Code flow with PKCE.

3. The callback completes the sign-in

After the user authenticates with the provider, the provider redirects back to the service's callback endpoint. The service exchanges the authorization code for tokens, creates or updates the user record, sets the AX_REFRESH_TOKEN cookie, and finally redirects the browser to the originUrl you supplied. At that point the user has a session and you can request an access token.

Sign in with email and password

For applications configured with an AxAuth IDP (the standalone user store), users can sign in with a username and password directly, without a redirect.

POST to the sign-in-with-credentials endpoint. The connectionId is the idpConnectionId of the AxAuth provider, which you obtain the same way as any other provider from the well-known listing. Send the request with credentials included so the response cookie is stored.

curl -X POST "https://auth.example.com/<tenantId>/<environmentId>/<applicationId>/sign-in-with-credentials" \
-H "Content-Type: application/json" \
--cookie-jar cookies.txt \
-d '{
"connectionId": "<axAuthIdpConnectionId>",
"email": "user@example.com",
"password": "the-password"
}'
{ "code": "SUCCESS" }

On success the AX_REFRESH_TOKEN cookie is set, exactly as in the redirect flow. Then retrieve the access token from the token endpoint (next section). Possible non-success codes include INVALID_CREDENTIALS, INVALID_IDP_CONNECTION, and APPLICATION_NOT_ACTIVE.

Get and renew the access token

Call the token endpoint to obtain an access token. This is the same call whether the user signed in through an external provider or with credentials.

The request must carry the AX_REFRESH_TOKEN cookie that was set during sign-in; that cookie is what identifies the session. In a browser it is sent automatically, provided the request goes to your proxy domain with credentials enabled. A native client must attach the cookie it stored from the sign-in handoff (see Native applications).

curl "https://auth.example.com/<tenantId>/<environmentId>/<applicationId>/token" \
--cookie cookies.txt
{
"code": "SUCCESS",
"tenantId": "...",
"environmentId": "...",
"applicationId": "...",
"user": {
"id": "...",
"name": "Jane Doe",
"email": "user@example.com",
"profileId": "...",
"token": {
"accessToken": "eyJ...",
"expiresInSeconds": 3600,
"expiresAt": "2026-07-13T12:00:00.000Z"
}
},
"extensions": {}
}

Use user.token.accessToken as a Bearer token for Mosaic end-user API calls.

The access token is short-lived. To renew it, simply call the token endpoint again before expiresAt; as long as the refresh cookie is still valid, a fresh access token is returned. If the refresh cookie is missing or expired, the response code is NEEDS_LOGIN and the user must sign in again. Other non-success codes include ACCOUNT_NOT_ACTIVE and USER_NOT_FOUND.

tip

Cache a valid access token and reuse it until shortly before it expires, then request a new one. Requesting a token on every API call is unnecessary and slower.

Sign out

Call the sign-out endpoint with the cookie. It invalidates the session and clears the AX_REFRESH_TOKEN cookie.

Sign-out is only needed when the user explicitly chooses to stop being signed in (for example, a "Log out" action). You do not need to call it to end a session when the user simply closes the app or the browser: the access token expires on its own, and the session stays valid until it expires (the refresh session lifetime) or the user signs out. Calling sign-out is what deliberately ends the "stay signed in" state.

curl "https://auth.example.com/<tenantId>/<environmentId>/<applicationId>/sign-out" \
--cookie cookies.txt
{ "code": "SUCCESS", "message": "User signed out." }

Native applications

Native apps (mobile, smart TV, consoles, and similar) have two ways to sign a user in. Choose based on the device:

Option 1: Sign in on the device (external IDP or email and password)

Use this when the device can present a sign-in UI: a browser or WebView for an external IDP, or an email and password form for AxAuth. It is the same flow as on the web, with one difference: a native app usually cannot rely on the browser to store the HttpOnly session cookie, so the sign-in returns the cookie in an encrypted form for the app to store and replay.

When starting the redirect flow, add an encryptionKey query parameter to the /oauth request: a 32-byte key encoded as a hex string. After a successful sign-in, instead of setting a cookie the service appends a COOKIE_OPTIONS query parameter to the originUrl redirect. That value is the encrypted cookie.

The app decrypts COOKIE_OPTIONS using the same key by calling the decryptWithKeyAes mutation on the Management GraphQL API, then stores the resulting cookie and attaches it to subsequent token and sign-out requests as the AX_REFRESH_TOKEN cookie.

The application must be configured as a Native application, and the originUrl must match one of its registered origins.

Option 2: Device Authorization flow (keyboard-less or shared devices)

Use this for devices where typing credentials is awkward or impossible, such as smart TVs, consoles, and streaming sticks. The device shows a short code that the user approves from a phone or laptop; the device never handles credentials or cookies itself.

For the concept, the roles involved, and the Admin Portal configuration (enabling the flow, the verification URL, code lifetime and poll interval), see Sign In on Smart TVs and Other Devices. That guide shows the flow with the React library; this section describes the same flow as plain HTTP.

Two parties take part, and they are two different applications in the same tenant and environment (so they share one user store). Each calls the endpoints under its own application-scoped path, /<tenantId>/<environmentId>/<applicationId>/…, using its own application ID:

  • The device app is the Native application (with the Device Authorization Flow enabled). It starts the flow, polls, and ends up signed in. Being native, it can call the authentication host https://user-auth.service.eu.axinom.net directly, and it never uses the session cookie: its refresh token is carried explicitly in the request instead. Its path uses the <deviceApplicationId> (the Native app).
  • The activation page is a separate Web application, where the user signs in and approves the device. Being browser-based, it reaches the endpoints through its own proxy domain (this guide uses https://auth.example.com) with a signed-in user's access token. Its path uses the <activationApplicationId> (the Web app).

The two IDs are not interchangeable: device/authorize on the Web app ID returns DEVICE_FLOW_NOT_ENABLED (only the Native app has the flow), and the user's access token is issued for the Web app, so the activation-page calls must use the Web app ID. The <tenantId> and <environmentId> are the same for both. The short user code is what ties the two sides together across the shared user store.

On the device

1. Start the authorization. POST to the device/authorize endpoint. The body is optional; deviceLabel is a display-only string (max 100 characters) shown to the user on the approval screen.

curl -X POST "https://user-auth.service.eu.axinom.net/<tenantId>/<environmentId>/<deviceApplicationId>/device/authorize" \
-H "Content-Type: application/json" \
-d '{ "deviceLabel": "Living Room TV" }'
{
"code": "SUCCESS",
"deviceCode": "GmRh...secret",
"userCode": "WDJB-MJHT",
"verificationUri": "https://app.example.com/activate",
"verificationUriComplete": "https://app.example.com/activate?userCode=WDJB-MJHT",
"expiresIn": 900,
"interval": 5
}

The deviceCode is a secret the device keeps and polls with. Show the userCode and verificationUri to the user; you can render verificationUriComplete (the same URL with the code embedded) as a QR code so they do not have to type the code.

verificationUri is your own activation page URL (https://app.example.com/activate in this example), not a User Service URL: the User Service is headless and owns none of the user journey. An administrator configures this URL as the application's Device Verification URL (see Configure the application), and the service returns it here verbatim with the userCode appended.

Non-success codes are DEVICE_FLOW_NOT_ENABLED (the application is not a Native app with the flow enabled) and APPLICATION_NOT_ACTIVE.

2. Poll until the user approves. POST to the device/poll endpoint with the deviceCode. This call always returns HTTP 200; the state is in the code field, so the "still waiting" states are not treated as errors.

curl -X POST "https://user-auth.service.eu.axinom.net/<tenantId>/<environmentId>/<deviceApplicationId>/device/poll" \
-H "Content-Type: application/json" \
-d '{ "deviceCode": "GmRh...secret" }'

While the user has not acted yet, the response is:

{ "code": "AUTHORIZATION_PENDING" }

Wait at least interval seconds between polls. If the response code is SLOW_DOWN, you are polling too fast; increase your interval and continue (the React library adds 5 seconds each time this happens). Once the user approves, the poll returns the device's refresh token:

{ "code": "SUCCESS", "refreshToken": "eyJ...refresh" }

The other terminal codes are ACCESS_DENIED (the user denied it, or the account is not active) and EXPIRED (the code timed out; start over). Store the refreshToken in secure device storage: it is the device's long-lived credential.

3. Exchange the refresh token for an access token. The device cannot use the AX_REFRESH_TOKEN cookie, so it calls the same token endpoint as the web flow but presents its refresh token as a Bearer header instead of a cookie.

curl "https://user-auth.service.eu.axinom.net/<tenantId>/<environmentId>/<deviceApplicationId>/token" \
-H "Authorization: Bearer eyJ...refresh"

The response is the same token payload documented in Get and renew the access token; use user.token.accessToken as a Bearer token for Mosaic end-user API calls. Renew the same way: call this endpoint again with the same refresh token before the access token expires. The refresh token is not rotated, so you reuse it every time. When it eventually expires the response code is NEEDS_LOGIN, and the device runs the flow again from step 1.

On the activation page

The activation page acts on behalf of a signed-in user, so sign the user in first with any of the flows above (external IDP or credentials) on the activation page's own Web application, and obtain their access token from the token endpoint. Because that sign-in runs against the Web app, the resulting access token, and every endpoint below, uses the <activationApplicationId> (not the device's Native app ID). The lookup below is anonymous, but the approve and deny calls require that access token.

4. Look up the code. GET the device/verify endpoint with the userCode the user typed, so you can show them what they are about to authorize.

curl "https://auth.example.com/<tenantId>/<environmentId>/<activationApplicationId>/device/verify?userCode=WDJB-MJHT"
{ "code": "SUCCESS", "deviceLabel": "Living Room TV", "status": "PENDING" }

The deviceLabel is the display-only string the device supplied; the real safeguard is the explicit approval combined with the code shown on the device, so always show the user what they are approving. Non-success codes are NOT_FOUND (unknown code) and EXPIRED.

5. Approve or deny. POST to device/approve (or device/deny) with the userCode in the body and the signed-in user's access token as a Bearer header.

curl -X POST "https://auth.example.com/<tenantId>/<environmentId>/<activationApplicationId>/device/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <accessToken>" \
-d '{ "userCode": "WDJB-MJHT" }'
{ "code": "SUCCESS" }

On SUCCESS, the user who approved becomes the user the device is signed in as, and the device's next poll returns its refresh token. Other codes are NOT_FOUND, EXPIRED, ALREADY_USED (the code was already approved or denied), and, when the caller is not authenticated, ACCESS_TOKEN_EXPIRED (refresh the access token from the token endpoint and retry) versus NEEDS_LOGIN (the user must sign in again).

For the flow as a whole, with both parties in one picture, see the sequence diagram under How it works.

Sign up and reset passwords (AxAuth)

Standalone sign-up and password reset are handled by the AxAuth Management GraphQL API (axAuthManagementGQL from the well-known document). These mutations do not require an access token. The oAuthClientId in the inputs is the clientId of the AxAuth provider from the provider listing.

Both flows are two-step and rely on a one-time password (OTP). The service sends the OTP to a webhook you configure on the AxAuth user store, and your backend forwards it to the user (typically by email). See Sign in with AxAuth IDP for the configuration details.

Initiate sign-up

mutation InitiateEndUserSignUp($input: InitiateEndUserSignUpInput!) {
initiateEndUserSignUp(input: $input) {
idpUserId
}
}
{
"input": {
"oAuthClientId": "<axAuthClientId>",
"email": "user@example.com",
"password": "the-password",
"firstName": "Jane",
"lastName": "Doe"
}
}

The password can be supplied here or deferred to the complete step, but it must be provided in exactly one of the two.

Complete sign-up

mutation CompleteEndUserSignUp($input: CompleteEndUserSignUpInput!) {
completeEndUserSignUp(input: $input) {
idpUserId
}
}
{ "input": { "signUpOtp": "123456", "password": "the-password" } }

Only verified users can sign in, so the sign-up must be completed before the first credential sign-in.

Reset a password

Initiate the reset, then complete it with the OTP and the new password:

mutation InitiateEndUserPasswordReset($input: InitiateEndUserPasswordResetInput!) {
initiateEndUserPasswordReset(input: $input) {
idpUserId
}
}
{ "input": { "oAuthClientId": "<axAuthClientId>", "email": "user@example.com" } }
mutation CompleteEndUserPasswordReset($input: CompleteEndUserPasswordResetInput!) {
completeEndUserPasswordReset(input: $input) {
idpUserId
}
}
{ "input": { "resetOtp": "123456", "newPassword": "the-new-password" } }

To check whether an OTP is still valid before submitting the full form, use the checkEndUserSignUpOtp and checkEndUserPasswordResetOtp mutations, which return { isOtpValid }.

Manage user profiles

A user can have multiple profiles. Profile operations use the End-User GraphQL API (userServiceEndUserGQL) and require the access token as a Bearer token in the Authorization header.

curl -X POST "https://user.service.eu.axinom.net/graphql" \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{ "query": "query { userProfiles { nodes { id displayName defaultProfile profilePictureUrl } } }" }'

The operations available:

# List all profiles
query GetUserProfiles {
userProfiles {
nodes { id displayName defaultProfile profilePictureUrl }
}
}

# Get one profile
query GetUserProfile($profileId: UUID!) {
userProfile(id: $profileId) { id displayName defaultProfile profilePictureUrl }
}

# Create a profile
mutation CreateUserProfile($input: CreateUserProfileInput!) {
createUserProfile(input: $input) {
userProfile { id displayName defaultProfile profilePictureUrl }
}
}

# Update a profile
mutation UpdateUserProfile($input: UpdateUserProfileInput!) {
updateUserProfile(input: $input) {
userProfile { id displayName defaultProfile profilePictureUrl }
}
}

# Delete a profile
mutation DeleteUserProfile($input: DeleteUserProfileInput!) {
deleteUserProfile(input: $input) {
userProfile { id }
}
}

# Set the active profile
mutation SetActiveProfile($profileId: UUID!) {
setActiveProfile(profileId: $profileId) {
id displayName defaultProfile profilePictureUrl
}
}

Setting the active profile changes which profile the next access token represents. After calling setActiveProfile, request a new access token so the updated profileId is reflected in the token payload.

For the full explanation of what profiles are and how they behave, see Manage User Profiles.

Authenticate an end-user application

Some scenarios need an application token (a token representing the application itself, not a signed-in user). Request one from the Management GraphQL API (userServiceManagementGQL) with the application key configured in the Admin Portal.

mutation AuthenticateEndUserApplication($input: AuthenticateEndUserApplicationInput!) {
authenticateEndUserApplication(input: $input) {
accessToken
expiresInSeconds
tokenType
}
}
{
"input": {
"tenantId": "...",
"environmentId": "...",
"applicationId": "...",
"applicationKey": "..."
}
}

Using the access token

The access token returned by the token endpoint (or a delegated or application token) is a JWT. Attach it as a Bearer token in the Authorization header when calling any Mosaic end-user facing service, such as Personalization or Entitlement.

Authorization: Bearer <accessToken>

See also

Was this page helpful?