OAuth 2.0 / OpenID Connect Setup
Mango has two independent OAuth 2.0 / OpenID Connect features. They share the oauth2. property prefix and are easy to confuse:
- Browser login (
authentication.oauth2.enabled,oauth2.client.*) — a person signs in to the Mango web interface with an identity provider such as Google Workspace, Microsoft Entra ID or Keycloak. Mango is the OAuth2 client. It creates and updates a Mango user from the provider's claims and maps groups or roles to Mango roles. - API bearer tokens (
authentication.token.enabled,oauth2.resourceserver.*) — a program calls Mango's REST API with anAuthorization: Bearerheader carrying a JWT issued by an identity provider. Mango is the OAuth2 resource server. It validates the token and derives roles from it. No Mango user is created.
The two sections below cover each feature in full. They can be used together with the same identity provider, but neither reads the other's properties.
All properties are set in mango.properties (or env.properties), or as mango_-prefixed environment variables with dots replaced by underscores. Both features require a restart of Mango after changes.
Browser login (oauth2.client)
How a login works
- The user clicks the provider's button on the login page. Mango redirects the browser to the provider's authorization endpoint. The URL Mango uses to start a login is
/oauth2/authorization/{registrationId}. - The user authenticates at the provider and is redirected back to
/oauth2/callback/{registrationId}with a one-time code. This callback URL, with Mango's public scheme and host, must be registered at the provider as the redirect URI. - Mango exchanges the code at the provider's token endpoint for an access token and, for OpenID Connect, an ID token. The ID token's signature, issuer, audience, expiry and nonce are verified.
- When the provider has a user info endpoint, Mango requests it with the access token. The user info claims are combined with the ID token claims; the ID token wins on conflicts.
- When extra claims are configured, Mango requests the configured endpoints with the access token and adds the results to the claims. Fetched claims take precedence over both the ID token and user info.
- The
userMappingproperties map the combined claims to a Mango user. The user is found by the linked account, which is the issuer and subject claim pair. A new user is created on first login; on later logins email, name, phone, locale, timezone and (by default) roles are updated.
Users created this way have a locked password and can only log in through the provider.
Enabling
| Property | Default | Description |
|---|---|---|
authentication.oauth2.enabled | false | Enables the login flow and the provider buttons on the login page. |
oauth2.client.registrationIds | empty | Comma-separated list of registration IDs to enable. Each ID gets its own button and its own oauth2.client.registration.{id}.* properties. |
oauth2.client.connectTimeout | 30 seconds | Connect timeout for every HTTP request Mango makes during a login (token endpoint, user info, extra claims). |
oauth2.client.readTimeout | 30 seconds | Read timeout for the same requests. |
How properties are resolved
Every property below the registration level is looked up in three places; the first match wins:
oauth2.client.registration.{registrationId}.{property}— the registrationoauth2.client.provider.{providerId}.{property}— the provider, where the provider ID isoauth2.client.registration.{registrationId}.providerand defaults to the registration IDoauth2.client.default.{property}— the default
This lets several registrations share provider settings, and lets Mango ship presets for well-known providers.
Where the endpoints come from
A registration needs the provider's authorization, token, JWK set, issuer and user info URIs. There are three ways to supply them. They can be combined, and a value set directly on the registration always wins.
- A preset. When the provider ID (the
providerproperty, or the registration ID when it is not set) matches a provider in the table below, its known values are inherited. - Discovery.
issuerLocationmakes Mango fetch{issuer}/.well-known/openid-configurationat startup and take every URI from it. This works with any OpenID Connect provider and is the recommended way for providers whose endpoints are specific to your tenant. - Individual properties.
authorizationUri,tokenUri,jwkSetUri,issuerUrianduserInfoUri, set one by one. Needed only for providers without discovery.
The first column shows each provider's issuer. Where the issuer is not a template it links to the discovery document at {issuer}/.well-known/openid-configuration, which you can open in a browser to see the endpoints and claims the provider publishes.
| Provider ID and issuer | The preset provides | Configure at the provider | Configure in Mango |
|---|---|---|---|
google — https://accounts.google.com | All endpoints; scopes openid,profile,email; username from the email claim | An OAuth consent screen with the openid, email and profile scopes; a Web application OAuth client with https://mango.example.com/oauth2/callback/{registrationId} as an authorised redirect URI | clientId, clientSecret. To offer only one Workspace domain in the account chooser: authorizationUri=https://accounts.google.com/o/oauth2/v2/auth?hd=example.com. Google treats hd as a hint, not a restriction; enforce membership through roles (for example Cloud Identity groups with allowEmpty=false). |
microsoft — https://login.microsoftonline.com/{tenantId}/v2.0 | User info endpoint (Microsoft Graph); client settings; scopes; button label | An app registration with the redirect URI (type Web); a client secret, noting its expiry date; for roles either a groups claim under Token configuration, or app roles assigned under Enterprise applications (app roles accept users and security groups, not Microsoft 365 groups); admin consent removes the per-user consent prompt | issuerLocation set to the issuer with your directory (tenant) ID, clientId, clientSecret, userMapping.roles=groups or roles |
github — no discovery, plain OAuth2 | All endpoints; claim mapping (id as subject, login as username, [email protected] as email) | An OAuth App with the callback URL | clientId, clientSecret; add the scope read:org for organisation membership |
okta — https://{yourOktaDomain}/oauth2/default | Scopes and button label | An OIDC Web application with the sign-in redirect URI; a groups claim on the authorization server if roles are mapped from groups | issuerLocation set to the authorization server issuer, clientId, clientSecret |
onelogin — https://{subdomain}.onelogin.com/oidc/2 | Client settings; scopes including groups; roles from the groups claim | An OIDC application with the redirect URI | issuerLocation, clientId, clientSecret |
facebook — https://www.facebook.com | All endpoints | An app with the redirect URI | clientId, clientSecret |
any other ID, e.g. keycloak — https://{host}/realms/{realm} | nothing | A confidential OIDC client with the redirect URI; a mapper which puts groups or roles into a claim | issuerLocation, clientId, clientSecret, userMapping.roles |
issuerLocation is ignored for google, github and facebook, whose endpoints are built in.
The issuer value in the discovery document must be exactly the URL configured as issuerLocation. For Entra ID, use the directory (tenant) ID: the common and organizations endpoints publish a templated issuer which never matches.
The discovery document is fetched while Mango starts. When it cannot be fetched, or its issuer does not match, Mango exits (and is restarted by the service manager where one is configured). A provider outage at startup therefore stops Mango, whereas the individual URI properties are only used when a user logs in.
Registration properties
| Property | Description |
|---|---|
issuerLocation | Loads the authorization, token, JWK set, user info and issuer URIs from {issuerLocation}/.well-known/openid-configuration at startup, see above. |
provider | Provider ID for the second lookup tier, see above. Defaults to the registration ID. |
clientId, clientSecret | Issued by the provider when the application is registered. Required. |
scope | Comma-separated scopes to request, default openid,profile,email for most presets. Add provider scopes here when extra claims need them. |
clientName | Label of the button on the login page. |
logoUri | Image shown on the button on the login page. |
authorizationUri, tokenUri, jwkSetUri, issuerUri, userInfoUri | The provider endpoints, when not using issuerLocation or a preset. |
userInfoAuthenticationMethod | header (default) or form: how the access token is sent to the user info endpoint. |
clientAuthenticationMethod | client_secret_basic (default), client_secret_post or none: how the client authenticates at the token endpoint. |
authorizationGrantType | authorization_code. Browser login supports only this grant. |
redirectUri | Defaults to {baseUrl}/oauth2/callback/{registrationId}. Only change it when the provider requires a different callback path. |
userNameAttributeName | The claim Spring Security uses as the principal name of the OAuth2 user object. Defaults to sub. It is only required for plain OAuth2 providers, which have no sub, and it is not the Mango username (see userMapping.username). |
The base URL used for {baseUrl} is derived from the request. Behind a reverse proxy, forwarded headers must be trusted (web.forwardedHeaders.*) so the callback URI has the public scheme and host. See Reverse Proxy Configuration.
User mapping
The userMapping.* properties name the claim whose value is used for each attribute of the Mango user. Defaults are the OpenID Connect standard claim names.
| Property | Default | Description |
|---|---|---|
userMapping.issuer | iss | Claim holding the issuer. Together with the subject it identifies the linked account. |
userMapping.issuer.fixed | none | A fixed issuer value instead of a claim, for plain OAuth2 providers without an iss claim (the GitHub preset uses this). |
userMapping.subject | sub | Claim holding the subject. Do not change issuer or subject for an OpenID Connect provider. |
userMapping.username | preferred_username | Claim which becomes the Mango username when the user is created. When the claim is missing the user gets a generated username. |
userMapping.username.prefix, .suffix | empty | Added around the username. |
userMapping.username.sync | false | Update the username from the claim on every login instead of only on creation. The login fails when the new username is already in use by another Mango user. Since Mango 5.7.6. |
userMapping.email | email | Claim holding the email address. Required: a user without an email fails validation. |
userMapping.email.prefix, .suffix | empty | Added around the email. |
userMapping.name | name | Display name. |
userMapping.phone | phone_number | Phone number. |
userMapping.locale | locale | Locale, e.g. en-AU. |
userMapping.timezone | zoneinfo | Time zone ID, e.g. Australia/Melbourne. |
Email, name, phone, locale and timezone are updated on every login. A claim that is absent leaves the attribute unchanged, except email, which is required.
Roles
| Property | Default | Description |
|---|---|---|
userMapping.roles | roles | Claim holding the user's roles or groups. A JSON array, or a single value. |
userMapping.roles.sync | true | Replace the user's Mango roles from the claim on every login. When false the provider only authenticates; roles are managed in Mango and a new user gets only the user role. |
userMapping.roles.ignore | empty | Comma-separated claim values to ignore. |
userMapping.roles.map.{value} | none | Map one claim value to a Mango role XID, e.g. userMapping.roles.map.mango-admins=superadmin. |
userMapping.roles.prefix, .suffix | empty | Added around claim values which are not mapped explicitly, to form the role XID. |
userMapping.roles.add | empty | Comma-separated Mango roles added to every user from this registration. |
userMapping.roles.insert | true | When true, roles which do not exist in Mango are created. When false they are dropped and logged at DEBUG, so only roles an administrator has created in Mango can be granted. Since Mango 5.7.6. |
The user role is always added.
With roles.sync=true the claim is authoritative: every role the user had that is not in the claim is removed at login, including superadmin. Check the provider's claim contents (for example by decoding the ID token at jwt.io) before enabling a registration for production users.
Extra claims
Some providers cannot put the required information into their tokens. Google Workspace has a fixed ID token claim set with no custom claims, so group membership is only available through Google APIs. Microsoft Entra ID omits the groups claim when a user is in more than 200 groups. GitHub returns no claims at all.
Extra claims close this gap. After the user is loaded and before it is mapped, Mango requests one or more configured HTTP endpoints with the user's own access token, evaluates a JSONPath expression against the JSON response and stores the result as a claim. Every userMapping property then works against the fetched claim like any other.
| Property | Default | Description |
|---|---|---|
extraClaims | empty | Comma-separated entry names, fetched in order. Empty disables the feature. |
extraClaims.{name}.uri | required | URI for the GET request. {claim} placeholders such as {email} or {sub} are replaced with the URL-encoded claim value. Entries with an identical URI and headers share a single request per login. |
extraClaims.{name}.path | required | JSONPath expression. A definite path (no wildcards, filters or deep scans) matching a single value stores that value as-is, which is what single-valued mappings such as userMapping.phone need. Any other result is stored as a list; JSON null values are dropped. A response which is a bare JSON value (a string or number) rather than an object or array is read with the path $. |
extraClaims.{name}.claim | entry name | Claim name to store the result under. Fetched claims take precedence over ID token and user info claims of the same name. The subject and issuer claims cannot be fetched. |
extraClaims.{name}.required | true | When true, a failed request (connection error, HTTP error, non-JSON response) or a {claim} placeholder in the URI which this user does not have fails the login. When false the entry is skipped with a warning in the log and the claim is left unset. |
extraClaims.{name}.allowEmpty | false | When false, an empty result (JSON null, an empty string, list or object) fails the login, so an endpoint which silently returns nothing (for example because of missing permissions) can never remove a user's roles. When true the claim is left unset. When that claim is the one named by userMapping.roles and roles.sync is true, the user is reset to the user role plus roles.add. |
extraClaims.{name}.skipIfPresent | false | Skip the request when the claim is already present and not empty. |
extraClaims.{name}.headers | empty | Comma-separated additional request header names, each with a headers.{Header-Name} value property. They may override the default Accept: application/json and Authorization headers. |
Only the first page of a paginated API response is read. Request a page size large enough for all values of a user, for example pageSize=500 for Google APIs or $top=999 for Microsoft Graph. Configuration errors, such as an invalid JSONPath expression, always fail the login.
The user's access token is sent to every configured URI. Only configure trusted HTTPS endpoints. Fetched values are mapped to Mango roles by name, including superadmin, so control them with roles.map, roles.ignore or a prefix.
Google Workspace groups via the Cloud Identity API
Enable the Cloud Identity API in the Google Cloud project of the OAuth client and add the scope https://www.googleapis.com/auth/cloud-identity.groups.readonly. Values are group email addresses.
oauth2.client.registration.google.scope=openid,profile,email,https://www.googleapis.com/auth/cloud-identity.groups.readonly
oauth2.client.registration.google.extraClaims=groups
oauth2.client.registration.google.extraClaims.groups.uri=https://cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups?query=member_key_id=='{email}'&pageSize=500
oauth2.client.registration.google.extraClaims.groups.path=$.memberships[*].groupKey.id
oauth2.client.registration.google.userMapping.roles=groups
oauth2.client.registration.google.userMapping.roles.map.mango-admins@example.com=superadmin
A user without any groups receives an empty response, which looks the same as an API that filtered everything out for lack of permission. Set allowEmpty=true only when users without groups are expected. Verified with a non-admin Workspace user: the user's own direct memberships are returned with the user's access token; no admin privilege or service account is required.
Google Workspace roles from a custom user schema
Roles can be stored per user in a custom schema in the Google Admin console, for example a schema Mango with a multi-valued field roles. Enable the Admin SDK API and add the scope https://www.googleapis.com/auth/admin.directory.user.readonly. With viewType=domain_public users can read their own custom fields when the field is visible to the domain. Verified with a non-admin Workspace user: a multi-valued field is returned as a list with the user's access token; no admin privilege or service account is required.
oauth2.client.registration.google.extraClaims=roles
oauth2.client.registration.google.extraClaims.roles.uri=https://admin.googleapis.com/admin/directory/v1/users/{email}?projection=custom&customFieldMask=Mango&viewType=domain_public
oauth2.client.registration.google.extraClaims.roles.path=$.customSchemas.Mango.roles[*].value
Name and phone number from the Google People API
Two entries with the same URI are served from one request. Enable the People API; phone numbers need the scope https://www.googleapis.com/auth/user.phonenumbers.read.
oauth2.client.registration.google.extraClaims=name,phone
oauth2.client.registration.google.extraClaims.name.uri=https://people.googleapis.com/v1/people/me?personFields=names,phoneNumbers
oauth2.client.registration.google.extraClaims.name.path=$.names[0].displayName
oauth2.client.registration.google.extraClaims.name.allowEmpty=true
oauth2.client.registration.google.extraClaims.phone.uri=https://people.googleapis.com/v1/people/me?personFields=names,phoneNumbers
oauth2.client.registration.google.extraClaims.phone.path=$.phoneNumbers[0].value
oauth2.client.registration.google.extraClaims.phone.allowEmpty=true
oauth2.client.registration.google.userMapping.phone=phone
Microsoft Entra ID groups via Microsoft Graph
Prefer Entra ID app roles: assign them to users or groups in the enterprise application and they arrive in the token's roles claim with no extra request. When groups are used instead, the token omits the groups claim above 200 groups. This entry only runs for those users because of skipIfPresent. It needs the scope GroupMember.Read.All (admin consent) and the ConsistencyLevel: eventual header Graph requires for this query.
oauth2.client.registration.entra.provider=microsoft
oauth2.client.registration.entra.scope=openid,profile,email,GroupMember.Read.All
oauth2.client.registration.entra.extraClaims=groups
oauth2.client.registration.entra.extraClaims.groups.uri=https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true&$select=displayName&$top=999
oauth2.client.registration.entra.extraClaims.groups.path=$.value[*].displayName
oauth2.client.registration.entra.extraClaims.groups.skipIfPresent=true
oauth2.client.registration.entra.extraClaims.groups.headers=ConsistencyLevel
oauth2.client.registration.entra.extraClaims.groups.headers.ConsistencyLevel=eventual
oauth2.client.registration.entra.userMapping.roles=groups
Do not add scopes for other resources (such as a custom API) to the same registration: the access token would then be issued for that resource and rejected by Graph. Graph returns null display names for groups the user cannot read; use $select=id and path=$.value[*].id to map by object ID instead.
GitHub organisations
GitHub is plain OAuth2. Add the scope read:org. Organisations that restrict third-party access return an empty list until the application is approved.
oauth2.client.registration.github.scope=read:user,user:email,read:org
oauth2.client.registration.github.extraClaims=orgs
oauth2.client.registration.github.extraClaims.orgs.uri=https://api.github.com/user/orgs?per_page=100
oauth2.client.registration.github.extraClaims.orgs.path=$[*].login
oauth2.client.registration.github.userMapping.roles=orgs
Complete example: Keycloak with groups
authentication.oauth2.enabled=true
oauth2.client.registrationIds=keycloak
oauth2.client.registration.keycloak.issuerLocation=https://sso.example.com/realms/mango
oauth2.client.registration.keycloak.clientId=mango
oauth2.client.registration.keycloak.clientSecret=...
oauth2.client.registration.keycloak.scope=openid,profile,email,groups
oauth2.client.registration.keycloak.clientName=Example SSO
oauth2.client.registration.keycloak.userMapping.roles=groups
oauth2.client.registration.keycloak.userMapping.roles.map.mango-admins=superadmin
oauth2.client.registration.keycloak.userMapping.roles.map.mango-users=user
The redirect URI to register in Keycloak is https://mango.example.com/oauth2/callback/keycloak.
Provider walkthrough: Google Workspace
- Open the Google Cloud console and create an OAuth consent screen with the
openid,emailandprofilescopes. - On the Credentials page click Create Credentials > OAuth client ID and choose Web application.
- Under Authorized JavaScript origins enter your Mango URL, e.g.
https://mango.example.com. Under Authorized redirect URIs enterhttps://mango.example.com/oauth2/callback/google. - Add the following to
mango.properties; thegooglepreset supplies the endpoints:
authentication.oauth2.enabled=true
oauth2.client.registrationIds=google
oauth2.client.registration.google.clientId=YOUR_CLIENT_ID
oauth2.client.registration.google.clientSecret=YOUR_CLIENT_SECRET
# Optional: show only one Workspace domain in the account chooser (a hint, not a restriction)
#oauth2.client.registration.google.authorizationUri=https://accounts.google.com/o/oauth2/v2/auth?hd=example.com
Google's ID token carries no groups, so map roles with one of the extra claims recipes above, or leave roles.sync=false and manage roles in Mango.
Provider walkthrough: Microsoft Entra ID
- In the Azure portal open Microsoft Entra ID > App registrations > New registration. Enter a name, choose Accounts in this organizational directory only (Single tenant) and add a Web redirect URI of
https://mango.example.com/oauth2/callback/entra. - From the app overview record the Application (client) ID and the Directory (tenant) ID.
- Under Certificates & secrets > Client secrets create a new secret and copy its Value immediately; note the expiry date.
- For roles, either add a Groups claim under Token configuration, or create App roles and assign them to users or security groups under Enterprise applications > Users and groups. App roles accept users and security groups, not Microsoft 365 groups.
- Optionally, under API permissions click Grant admin consent to remove the per-user consent prompt.
- Add the following to
mango.properties:
authentication.oauth2.enabled=true
oauth2.client.registrationIds=entra
# The microsoft preset supplies the Graph user info endpoint, scopes and button label
oauth2.client.registration.entra.provider=microsoft
oauth2.client.registration.entra.issuerLocation=https://login.microsoftonline.com/<TENANT_ID>/v2.0
oauth2.client.registration.entra.clientId=<CLIENT_ID>
oauth2.client.registration.entra.clientSecret=<CLIENT_SECRET>
# "groups" when using a groups claim, "roles" when using app roles
oauth2.client.registration.entra.userMapping.roles=roles
oauth2.client.registration.entra.userMapping.roles.map.MangoAdmin=superadmin
The registration ID in the redirect URI (entra in /oauth2/callback/entra) must exactly match the value in oauth2.client.registrationIds and the registration prefix oauth2.client.registration.entra.*. Use the directory (tenant) ID in issuerLocation; the common and organizations endpoints publish a templated issuer that never matches.
Testing and troubleshooting
After saving mango.properties, restart Mango, open the login page and check that a button for the registration appears. Sign in with a test user from the provider and confirm the Mango user's roles under Users.
| Symptom | Check |
|---|---|
| No provider button on the login page | authentication.oauth2.enabled=true, the registration ID is listed in oauth2.client.registrationIds, and Mango was restarted. |
| The provider reports a redirect URI mismatch | The redirect URI registered at the provider must equal {public Mango URL}/oauth2/callback/{registrationId} exactly, including https, the host and the registration ID. Behind a reverse proxy, web.forwardedHeaders.enabled=true with the proxy's address in web.forwardedHeaders.trustedIpRanges, otherwise Mango builds the callback from the internal host. |
| The user logs in with the wrong roles or none | Decode the ID token (for example at jwt.io) and check which claim carries the groups or roles and what its values are, then compare with userMapping.roles and roles.map.*. Enable debug logging (below) to see the claims Mango received. |
| Roles assigned in Mango disappear after login | userMapping.roles.sync is true, so the claim replaces the user's roles. Set it to false to manage roles in Mango. |
| Mango exits at startup | The issuerLocation discovery document could not be fetched or its issuer does not match. Check outbound connectivity and the exact issuer URL. |
A failed login shows an error message on the login page. Messages from Mango's own mapping start with a code in square brackets.
| Message | Meaning |
|---|---|
[extra_claims_config] Extra claim "groups" for registration "google" is misconfigured: ... | An extraClaims property is missing or invalid. The reason is in the message. |
[extra_claims_error] Extra claim "groups" for registration "google" could not be fetched | The request to the endpoint failed. The HTTP status or connection error is logged at WARN by DefaultExtraClaimsService. Check the scopes, whether the API is enabled for the client, and whether the user may call it. |
[extra_claims_empty] Extra claim "groups" for registration "google" is empty | The request succeeded but the path matched nothing. |
Error synchronizing OpenID Connect user: ... | The Mango user failed validation, for example a missing email claim or a username already in use. |
[invalid_id_token] ..., [invalid_nonce] ..., [invalid_user_info_response] ... | Spring Security rejected the provider's response. Check the endpoint URIs and the clock on the Mango host. |
Debug logging for com.serotonin.m2m2.web.mvc.spring.security.oauth2 logs the claims of every user being synchronised and the URI of every extra claims request.
API bearer tokens (oauth2.resourceserver)
How a request is authenticated
A client obtains a JWT from an identity provider, typically with the client credentials grant for machine-to-machine access or the authorization code grant for a user, and sends it with every request:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Mango selects the provider configuration whose issuer-uri matches the token's iss claim, verifies the signature against the provider's JWK set, checks the issuer, expiry and (when configured) audience, and derives roles from the token. The request runs as a principal named JWT {sub} ({iss}). No Mango user is created or updated; the principal exists only for the request.
Both authentication.token.enabled and a non-empty provider list are required.
Properties
Provider names in oauth2.resourceserver.providers are labels chosen by the administrator and form the prefix of that provider's properties. They are unrelated to the login registration IDs.
| Property | Default | Description |
|---|---|---|
authentication.token.enabled | true | Enables bearer token authentication (Mango's own tokens and OAuth2 tokens). |
oauth2.resourceserver.providers | empty | Comma-separated provider names. Empty disables OAuth2 bearer tokens. |
oauth2.resourceserver.{provider}.jwt.issuer-uri | required | The token issuer, exactly as it appears in the iss claim. Also used for discovery of the JWK set through {issuer-uri}/.well-known/openid-configuration. |
oauth2.resourceserver.{provider}.jwt.jwk-set-uri | discovered | The JWK set URL, when the issuer does not support discovery. |
oauth2.resourceserver.{provider}.jwt.audiences | any | Comma-separated accepted values of the aud claim. Set this to the identifier the provider assigns to Mango so tokens issued for other APIs are rejected. |
oauth2.resourceserver.{provider}.jwt.roles | empty | Comma-separated Mango roles granted to every token from this provider, for example anonymous,user. |
oauth2.resourceserver.{provider}.jwt.map-scope.{scope} | none | Grants the listed Mango roles when the token's scope (or scp) claim contains {scope}. Sound for client credentials tokens, where the provider issues only the scopes the client is allowed. Not sound for user tokens from providers such as Keycloak, which grant any optional scope the client requests without consulting the user's privileges. |
oauth2.resourceserver.{provider}.jwt.roles-claim | none | Claim holding the roles or groups of a user token, such as groups or roles. A JSON array or a space-separated string. Each value maps to the Mango role with the same XID unless remapped below. Since Mango 5.7.4. |
oauth2.resourceserver.{provider}.jwt.roles-claim.map.{value} | none | Maps one claim value to a comma-separated list of Mango roles. |
oauth2.resourceserver.{provider}.jwt.roles-claim.ignore | empty | Comma-separated claim values to ignore. |
oauth2.resourceserver.{provider}.jwt.roles-claim.prefix, .suffix | empty | Added around unmapped claim values to form the role XID. |
oauth2.resourceserver.{provider}.jwt.insert-roles | false | When true, roles which do not exist in Mango are created. When false they are dropped and logged at DEBUG. |
The roles from roles, map-scope and roles-claim are combined. Unlike browser login, the user role is not added automatically; grant it through roles when the API client should have it.
Example: machine-to-machine access with Auth0
authentication.token.enabled=true
oauth2.resourceserver.providers=auth0
oauth2.resourceserver.auth0.jwt.issuer-uri=https://example.us.auth0.com/
oauth2.resourceserver.auth0.jwt.audiences=https://mango.example.com/api
oauth2.resourceserver.auth0.jwt.roles=anonymous,user
oauth2.resourceserver.auth0.jwt.map-scope.mango:admin=superadmin
The client requests a token with grant_type=client_credentials, audience=https://mango.example.com/api and its client ID and secret, then calls Mango with the returned access token.
Example: user tokens from Keycloak with a groups claim
authentication.token.enabled=true
oauth2.resourceserver.providers=keycloak
oauth2.resourceserver.keycloak.jwt.issuer-uri=https://sso.example.com/realms/mango
oauth2.resourceserver.keycloak.jwt.audiences=mango
oauth2.resourceserver.keycloak.jwt.roles=user
oauth2.resourceserver.keycloak.jwt.roles-claim=groups
oauth2.resourceserver.keycloak.jwt.roles-claim.map.mango-admins=superadmin
Troubleshooting
| Symptom | Check |
|---|---|
401 with invalid_token | The signature, issuer, expiry or audience check failed. Decode the token at jwt.io and compare iss with issuer-uri and aud with audiences. Clock skew on the Mango host also fails expiry checks. |
401 and the token is fine | authentication.token.enabled is false, or the provider is not listed in oauth2.resourceserver.providers, or its issuer-uri does not match iss exactly (trailing slash included). |
403 on every endpoint | The token authenticated but carries no useful roles. Check roles, map-scope and roles-claim, and enable DEBUG logging for com.infiniteautomation.mango.spring.authentication to see dropped roles. |
Failed to load provider in the log at startup, then 401 for every token | The issuer's discovery document could not be fetched, or issuer-uri is missing. The provider is skipped until the properties are fixed. Set jwk-set-uri explicitly or check outbound connectivity from the Mango host. Two providers with the same issuer-uri are also refused; the second is skipped with a warning. |
Using both together
The same identity provider can serve both features. Register Mango once as an application at the provider, use the same clientId for browser login, and accept tokens for the API with the provider's issuer. The two configurations remain independent: a registration ID in oauth2.client.registrationIds and a provider name in oauth2.resourceserver.providers may be the same word for readability, but nothing connects them, and oauth2.client.* properties have no effect on bearer token authentication.
Related Pages
- LDAP Configuration — Alternative SSO using LDAP or Active Directory
- Users and Permissions — Manage Mango user accounts and role assignments
- Mango Properties Reference — Full reference for
mango.properties - Reverse Proxy Configuration — Configure forwarded headers when OAuth callbacks pass through a proxy