Sign in with Azure AD groups¶
Microsoft Entra ID, formerly Azure Active Directory, works with Mewbo over either OpenID Connect or SAML 2.0. Both paths are covered here.
Sign in tends to work first time. Authorization is where the two surprises live, and both concern group membership.
- Group claims carry object UUIDs, not group names. Your mapping rules must match UUIDs, or you should switch to app roles.
- Entra drops the groups claim entirely once a user is in too many groups, replacing it with a pointer to the Graph API. On the OIDC path Mewbo fails loud rather than silently granting nothing.
What this unlocks¶
- Single sign on for the Mewbo console and REST API with Entra ID accounts.
- Conditional Access, multi factor and device compliance policies apply to Mewbo without Mewbo implementing any of them.
- Entra security groups or app roles drive Mewbo roles.
The login flow¶
sequenceDiagram
autonumber
participant B as Browser
participant M as Mewbo API
participant E as Microsoft Entra ID
B->>M: GET /api/auth/login
M-->>B: 302 to login.microsoftonline.com + signed state cookie
B->>E: Authorization request
E->>E: Sign-in, multi-factor, Conditional Access
E-->>B: 302 back with ?code=...&state=...
B->>M: GET /api/auth/callback?code&state
M->>E: Exchange code for tokens (back channel)
E-->>M: ID token
M->>M: Verify signature, iss, aud, exp, nonce
alt Groups claim present inline
M->>M: Map groups, provision, resolve roles
M-->>B: 302 to return_to + session cookie
else Groups overage pointer instead of groups
M-->>B: 302 to /?auth_error=groups_overage
end
Path A: OpenID Connect¶
Configure Entra¶
In the Entra admin centre, register an application.
| Entra setting | Value |
|---|---|
| Redirect URI, type Web | https://console.example.com/api/auth/callback |
| Front-channel logout URL | https://console.example.com/ |
| Client secret | Create one under Certificates and secrets, and copy the value, not the id |
| Token configuration | Add the optional groups claim, or define app roles. See below |
| Value | |
|---|---|
| Issuer | https://login.microsoftonline.com/<tenant-id>/v2.0 |
| Discovery URL | https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration |
Use the v2.0 endpoints. The v1.0 endpoint issues tokens whose iss does not match the v2.0 issuer string, and Mewbo checks the discovery document's issuer against your configured issuer before it will verify anything.
Configure Mewbo¶
{
"api": {
"auth": {
"enabled": true,
"authenticators": [
{
"name": "entra",
"kind": "oidc",
"issuer": "https://login.microsoftonline.com/00000000-1111-2222-3333-444444444444/v2.0",
"discovery_url": "https://login.microsoftonline.com/00000000-1111-2222-3333-444444444444/v2.0/.well-known/openid-configuration",
"client_id": "55555555-6666-7777-8888-999999999999",
"client_secret": "the-secret-value-from-the-portal",
"scopes": ["openid", "email", "profile"],
"groups_claim": "roles"
}
],
"session": { "secret": "generate-a-long-random-string" },
"role_mappings": {
"default_role": "viewer",
"rules": [
{ "match": "MewboAdmin", "target": "admin" },
{ "match": "MewboOperator", "target": "operator" }
]
},
"bootstrap": { "admin_group": "MewboAdmin" }
}
}
}
The oidc extra is required. Run pip install mewbo-iam[oidc].
Trap 1: group claims are object UUIDs¶
Add the optional groups claim in Token configuration and Entra emits an array of group object ids, not display names.
{ "groups": ["aaaaaaaa-1111-2222-3333-444444444444", "bbbbbbbb-5555-6666-7777-888888888888"] }
Three ways to live with this, best first.
App roles emit the names you choose, so your configuration stays readable and survives a group being recreated.
In the app registration, under App roles, create a role with a value such as MewboAdmin. Then, under Enterprise applications, assign users or groups to that role. Entra places the assigned role values in a roles claim.
"groups_claim": "roles"
The token then carries what you wrote.
{ "roles": ["MewboAdmin", "MewboOperator"] }
This is the configuration shown above. It also sidesteps Trap 2, since app-role assignments are not subject to the group-count limit.
Keep the groups claim and write the object ids into your rules. It works, and it is unreadable.
"groups_claim": "groups",
"role_mappings": {
"default_role": "viewer",
"rules": [
{ "match": "aaaaaaaa-1111-2222-3333-444444444444", "target": "admin" }
]
}
Matching is case-insensitive, so Entra's casing for the UUID does not matter. Record which group each id refers to somewhere, because nothing in the file will tell you six months later.
For groups synchronised from on-premises Active Directory only, the app registration manifest can emit sAMAccountName instead of the object id. Set optionalClaims.idToken to include groups with additionalProperties of sam_account_name.
Cloud-only groups have no sAMAccountName, so a mixed estate emits names for synced groups and nothing usable for the rest. That is worse than either consistent option. Use this only if all your groups are synced.
Trap 2: the groups overage¶
Entra refuses to put an unbounded number of groups in a token. Once a user belongs to too many, it drops the groups claim completely and substitutes a reference to the Microsoft Graph API.
{
"_claim_names": { "groups": "src1" },
"_claim_sources": { "src1": { "endpoint": "https://graph.microsoft.com/v1.0/users/.../getMemberObjects" } }
}
Microsoft's documentation puts that boundary in the low hundreds and the figure has moved between token types and over time. Treat it as a user in many groups rather than a number you can safely sit just under.
Mewbo refuses the login rather than admitting the user with no groups. The browser is redirected to /?auth_error=groups_overage and the server log carries the explanation at error level. The check runs immediately after the signature is verified, before issuer, audience and expiry, so an overage is reported as an overage rather than masked by another claim complaint. Treating it as this user has no groups would strip every group derived role from the users in the most groups, usually the most senior ones.
The check lives in the JWT verifier, so it covers the login callback and the bearer token path and nothing else. Introspection responses, userinfo responses, SAML attribute statements and LDAP directory entries are never overage checked. That matters most for the SAML path below.
Fixing an overage¶
| Fix | How |
|---|---|
| Switch to app roles | The recommended fix. App-role assignments are not subject to the group limit, so the overage cannot recur. Set groups_claim to roles |
| Emit only assigned groups | In Token configuration, set the groups claim to Groups assigned to the application rather than all groups. Only groups actually assigned to the Mewbo enterprise application are emitted, which is almost always a handful |
| Reduce group membership | Rarely practical, and it does not prevent recurrence |
| Call Graph | Mewbo does not follow the _claim_sources pointer. There is no Graph client and no configuration field that would enable one. Resolve Graph-derived membership outside Mewbo and assign roles through the IAM admin API or over SCIM |
Groups assigned to the application needs no change to your role mappings, so it is usually the fastest way out of a live incident.
Path B: SAML 2.0¶
Entra can also act as a SAML identity provider. Configure Mewbo as a non-gallery Enterprise application.
Prerequisites, and one that will surprise your users¶
Check these before you build the integration. Two of them are not configuration problems you can solve later.
- Sign-in is service-provider initiated only. Users must start at
/api/auth/saml/login, since the assertion consumer requires a signed state cookie only that route issues and refuses any assertion arriving without one. Clicking the Mewbo tile in the Entra My Apps dashboard will not sign anyone in. Link the tile tohttps://console.example.com/api/auth/saml/logininstead. - Mewbo does not sign its AuthnRequests and cannot decrypt encrypted assertions. There is no service-provider private key. An unsigned AuthnRequest is spec-legal and Entra accepts one by default, but a policy requiring a signed request, or enabled assertion encryption, will break the integration. Leave assertion encryption off.
- There is no Single Logout. No SLO endpoint is served and none appears in the SP metadata. Signing out of Mewbo clears the Mewbo session. It does not end the Entra session.
- Assertion signatures are always required, and nothing in the identity provider's metadata can relax that. The NameID format requested is
unspecified, and the assertion arrives over an HTTP POST binding.
If either of the first two is a blocker, the reverse-proxy alternative below sidesteps both.
| Entra SAML setting | Value |
|---|---|
| Identifier, Entity ID | https://console.example.com |
| Reply URL, ACS | https://console.example.com/api/auth/saml/acs |
| Sign-on URL | https://console.example.com/api/auth/saml/login |
{
"name": "entra-saml",
"kind": "saml",
"issuer": "https://sts.windows.net/00000000-1111-2222-3333-444444444444/",
"sp_entity_id": "https://console.example.com",
"idp_metadata_url": "https://login.microsoftonline.com/00000000-1111-2222-3333-444444444444/federationmetadata/2007-06/federationmetadata.xml?appid=55555555-6666-7777-8888-999999999999",
"subject_attribute": "NameID",
"email_attribute": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"name_attribute": "http://schemas.microsoft.com/identity/claims/displayname",
"groups_attribute": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
}
Entra sends attributes under their full claim-type URIs rather than short names. Copy them from the Attributes and Claims blade rather than typing them.
Exactly one of idp_metadata_url or idp_metadata_xml is required, and both or neither is rejected when the configuration is parsed. Mewbo publishes its own SP metadata at GET /api/auth/saml/metadata, so hand that URL to whoever administers Entra instead of transcribing values.
The saml extra is required. Run pip install mewbo-iam[saml].
The overage check does not cover the SAML path
The fail-loud detection above lives in the JWT verifier, so it protects the OIDC and bearer paths only. If an Entra SAML user exceeds the group limit, Entra substitutes a groups.link attribute for the groups attribute, Mewbo finds no groups, and the user is silently assigned only default_role.
On the SAML path with users in many groups, prefer emitting only application-assigned groups, and treat an unexpected drop to default_role as a suspected overage rather than a mapping error.
Replay protection is per worker¶
Consumed assertion ids are remembered so a captured, still valid SAMLResponse cannot be replayed. That memory is process local and gunicorn workers do not share it, so a replay routed to a different worker inside the assertion's validity window is not caught. See Known limits.
Alternative: terminate SAML at a reverse proxy¶
If the service provider initiated restriction or the lack of request signing blocks you, let something that already speaks SAML do the handshake and assert the result to Mewbo through the trusted header authenticator. For a shop with existing SAML infrastructure this is often the better recommendation, not a fallback.
A Shibboleth SP, SimpleSAMLphp or an auth proxy sidecar terminates SAML at the proxy. Mewbo never runs the SAML code path, so there is no saml extra and no xmlsec dependency, and neither limitation above applies because the proxy handles both.
Configure it exactly as in the Authelia guide, substituting your proxy's identity header names. The trusted header security requirements apply in full, and they are strict. This is an authentication bypass class of feature if the trusted_proxies allowlist is wrong.
Verify it works¶
- Sign in through the console, then call
GET /api/auth/me. Expectauthenticated: true, your Entra object id as the subject, the resolvedroles, the fullpermissionsset, and anauth_methodobject reporting your issuer. - If
rolescontains onlyviewer, decode the ID token and look forrolesorgroups. Use the Entra token inspection tooling rather than guessing. An absent claim and a claim at the wrong path need different fixes. - To confirm overage handling before it bites in production, add a test account to enough groups to cross the limit. The login should be refused with
?auth_error=groups_overagerather than succeeding with no roles.
Failure modes¶
These are Entra specific. Boot failures and generic callback errors are in the shared Troubleshooting table.
| Symptom | Cause | Fix |
|---|---|---|
?auth_error=groups_overage |
The user is in more groups than the token can carry | Switch to app roles, or emit only application-assigned groups |
Everyone lands on viewer, OIDC path |
The groups or roles claim was never added in Token configuration | Add the optional claim or define app roles, then set groups_claim to match |
Everyone lands on viewer, SAML path |
Possibly an unreported overage, since the SAML path has no overage check | Emit only application-assigned groups, and verify the assertion's attributes |
| Rules never match, and the claim is present | The claim carries UUIDs while the rules carry names | Match the UUIDs, or move to app roles |
| Login fails with an issuer mismatch in the log | The v1.0 endpoint was configured, so the token's iss disagrees with the discovery document |
Use the v2.0 issuer and discovery URL |
?auth_error=provider_error |
Entra returned an error to the callback, commonly a Conditional Access block or a missing admin consent | Check the Entra sign-in logs for that user |
Token verification, for the security review¶
The algorithm allowlist and the double issuer pin are described in What a browser login actually does. These four numbers are the ones a reviewer asks for, verified against jwks.py.
- Audience is essential, resolved as the explicit audience, then the authenticator's configured
audience, then theclient_id. - Expiry is essential, with 45 seconds of clock-skew tolerance.
- Nonce is checked on the ID token from the login callback, as replay defence.
- Signing keys are cached for 6 hours, and an unrecognised key id forces one out-of-band refresh, so a provider rotating keys does not cause an outage.
For the concepts behind this page, including roles, the permission catalogue, how a request resolves to a principal, and what is and is not enforced, see Authentication and Access. This guide connects one provider. It deliberately does not restate the model.