Skip to main content

OAuth 2.0 / OpenID Connect Setup

Since Mango 5.2

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 an Authorization: Bearer header 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​

  1. 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}.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. The userMapping properties 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​

PropertyDefaultDescription
authentication.oauth2.enabledfalseEnables the login flow and the provider buttons on the login page.
oauth2.client.registrationIdsemptyComma-separated list of registration IDs to enable. Each ID gets its own button and its own oauth2.client.registration.{id}.* properties.
oauth2.client.connectTimeout30 secondsConnect timeout for every HTTP request Mango makes during a login (token endpoint, user info, extra claims).
oauth2.client.readTimeout30 secondsRead 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:

  1. oauth2.client.registration.{registrationId}.{property} — the registration
  2. oauth2.client.provider.{providerId}.{property} — the provider, where the provider ID is oauth2.client.registration.{registrationId}.provider and defaults to the registration ID
  3. oauth2.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.

  1. A preset. When the provider ID (the provider property, or the registration ID when it is not set) matches a provider in the table below, its known values are inherited.
  2. Discovery. issuerLocation makes Mango fetch {issuer}/.well-known/openid-configuration at 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.
  3. Individual properties. authorizationUri, tokenUri, jwkSetUri, issuerUri and userInfoUri, 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 issuerThe preset providesConfigure at the providerConfigure in Mango
google — https://accounts.google.comAll endpoints; scopes openid,profile,email; username from the email claimAn 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 URIclientId, 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.0User info endpoint (Microsoft Graph); client settings; scopes; button labelAn 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 promptissuerLocation set to the issuer with your directory (tenant) ID, clientId, clientSecret, userMapping.roles=groups or roles
github — no discovery, plain OAuth2All endpoints; claim mapping (id as subject, login as username, [email protected] as email)An OAuth App with the callback URLclientId, clientSecret; add the scope read:org for organisation membership
okta — https://{yourOktaDomain}/oauth2/defaultScopes and button labelAn OIDC Web application with the sign-in redirect URI; a groups claim on the authorization server if roles are mapped from groupsissuerLocation set to the authorization server issuer, clientId, clientSecret
onelogin — https://{subdomain}.onelogin.com/oidc/2Client settings; scopes including groups; roles from the groups claimAn OIDC application with the redirect URIissuerLocation, clientId, clientSecret
facebook — https://www.facebook.comAll endpointsAn app with the redirect URIclientId, clientSecret
any other ID, e.g. keycloak — https://{host}/realms/{realm}nothingA confidential OIDC client with the redirect URI; a mapper which puts groups or roles into a claimissuerLocation, 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.

caution

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​

PropertyDescription
issuerLocationLoads the authorization, token, JWK set, user info and issuer URIs from {issuerLocation}/.well-known/openid-configuration at startup, see above.
providerProvider ID for the second lookup tier, see above. Defaults to the registration ID.
clientId, clientSecretIssued by the provider when the application is registered. Required.
scopeComma-separated scopes to request, default openid,profile,email for most presets. Add provider scopes here when extra claims need them.
clientNameLabel of the button on the login page.
logoUriImage shown on the button on the login page.
authorizationUri, tokenUri, jwkSetUri, issuerUri, userInfoUriThe provider endpoints, when not using issuerLocation or a preset.
userInfoAuthenticationMethodheader (default) or form: how the access token is sent to the user info endpoint.
clientAuthenticationMethodclient_secret_basic (default), client_secret_post or none: how the client authenticates at the token endpoint.
authorizationGrantTypeauthorization_code. Browser login supports only this grant.
redirectUriDefaults to {baseUrl}/oauth2/callback/{registrationId}. Only change it when the provider requires a different callback path.
userNameAttributeNameThe 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.

PropertyDefaultDescription
userMapping.issuerissClaim holding the issuer. Together with the subject it identifies the linked account.
userMapping.issuer.fixednoneA fixed issuer value instead of a claim, for plain OAuth2 providers without an iss claim (the GitHub preset uses this).
userMapping.subjectsubClaim holding the subject. Do not change issuer or subject for an OpenID Connect provider.
userMapping.usernamepreferred_usernameClaim which becomes the Mango username when the user is created. When the claim is missing the user gets a generated username.
userMapping.username.prefix, .suffixemptyAdded around the username.
userMapping.username.syncfalseUpdate 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.emailemailClaim holding the email address. Required: a user without an email fails validation.
userMapping.email.prefix, .suffixemptyAdded around the email.
userMapping.namenameDisplay name.
userMapping.phonephone_numberPhone number.
userMapping.localelocaleLocale, e.g. en-AU.
userMapping.timezonezoneinfoTime 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​

PropertyDefaultDescription
userMapping.rolesrolesClaim holding the user's roles or groups. A JSON array, or a single value.
userMapping.roles.synctrueReplace 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.ignoreemptyComma-separated claim values to ignore.
userMapping.roles.map.{value}noneMap one claim value to a Mango role XID, e.g. userMapping.roles.map.mango-admins=superadmin.
userMapping.roles.prefix, .suffixemptyAdded around claim values which are not mapped explicitly, to form the role XID.
userMapping.roles.addemptyComma-separated Mango roles added to every user from this registration.
userMapping.roles.inserttrueWhen 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.

PropertyDefaultDescription
extraClaimsemptyComma-separated entry names, fetched in order. Empty disables the feature.
extraClaims.{name}.urirequiredURI 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}.pathrequiredJSONPath 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}.claimentry nameClaim 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}.requiredtrueWhen 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}.allowEmptyfalseWhen 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}.skipIfPresentfalseSkip the request when the claim is already present and not empty.
extraClaims.{name}.headersemptyComma-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.

caution

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​

  1. Open the Google Cloud console and create an OAuth consent screen with the openid, email and profile scopes.
  2. On the Credentials page click Create Credentials > OAuth client ID and choose Web application.
  3. Under Authorized JavaScript origins enter your Mango URL, e.g. https://mango.example.com. Under Authorized redirect URIs enter https://mango.example.com/oauth2/callback/google.
  4. Add the following to mango.properties; the google preset 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​

  1. 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.
  2. From the app overview record the Application (client) ID and the Directory (tenant) ID.
  3. Under Certificates & secrets > Client secrets create a new secret and copy its Value immediately; note the expiry date.
  4. 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.
  5. Optionally, under API permissions click Grant admin consent to remove the per-user consent prompt.
  6. 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
important

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.

SymptomCheck
No provider button on the login pageauthentication.oauth2.enabled=true, the registration ID is listed in oauth2.client.registrationIds, and Mango was restarted.
The provider reports a redirect URI mismatchThe 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 noneDecode 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 loginuserMapping.roles.sync is true, so the claim replaces the user's roles. Set it to false to manage roles in Mango.
Mango exits at startupThe 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.

MessageMeaning
[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 fetchedThe 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 emptyThe 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.

PropertyDefaultDescription
authentication.token.enabledtrueEnables bearer token authentication (Mango's own tokens and OAuth2 tokens).
oauth2.resourceserver.providersemptyComma-separated provider names. Empty disables OAuth2 bearer tokens.
oauth2.resourceserver.{provider}.jwt.issuer-urirequiredThe 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-uridiscoveredThe JWK set URL, when the issuer does not support discovery.
oauth2.resourceserver.{provider}.jwt.audiencesanyComma-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.rolesemptyComma-separated Mango roles granted to every token from this provider, for example anonymous,user.
oauth2.resourceserver.{provider}.jwt.map-scope.{scope}noneGrants 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-claimnoneClaim 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}noneMaps one claim value to a comma-separated list of Mango roles.
oauth2.resourceserver.{provider}.jwt.roles-claim.ignoreemptyComma-separated claim values to ignore.
oauth2.resourceserver.{provider}.jwt.roles-claim.prefix, .suffixemptyAdded around unmapped claim values to form the role XID.
oauth2.resourceserver.{provider}.jwt.insert-rolesfalseWhen 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​

SymptomCheck
401 with invalid_tokenThe 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 fineauthentication.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 endpointThe 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 tokenThe 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.