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.
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_TOKENcookie 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:
| Parameter | Description |
|---|---|
tenantId | Your tenant ID. |
environmentId | Your environment ID. |
applicationId | Your application ID. |
idpConnectionId | The idpConnectionId of the chosen provider from step 1. |
originUrl | Where to send the user after sign-in completes. Must be a registered origin for the app. |
userAuthProxyUrl | Your 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. |
encryptionKey | Native 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.
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.netdirectly, 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
- @axinom/mosaic-user-auth - the React library that wraps all of the above.
- Sign in with External IDPs and Sign in with AxAuth IDP - the same flows shown with the React library.
- Sign In on Smart TVs and Other Devices - the concept, roles, and configuration for the Device Authorization flow (shown with the React library; the HTTP contract is in Option 2 above).
- Generate a Delegated Access Token - issue a Mosaic access token when authentication is handled entirely by a third party.