Skip to content

Mailbox Authentication

Ed Mozley edited this page Jun 28, 2026 · 8 revisions

Mailbox Authentication

How FreeITSM connects to a Microsoft 365 (or Google Workspace) mailbox to read inbound email into tickets and send replies β€” the two authentication modes, when to use each, the safeguards that make sure you're reading the right inbox, and a plain-English primer on OAuth scopes and permissions.

Configure all of this under Tickets β†’ Settings β†’ Mailboxes.


The two modes (Microsoft 365)

A Microsoft mailbox can authenticate in one of two ways. You choose with the Authentication dropdown in the mailbox modal.

Delegated (sign in as the mailbox) App-only (client credentials)
Who is FreeITSM acting as? A user β€” whoever signed in The application itself (no user)
How you connect You click Authenticate and sign in through Microsoft once No sign-in β€” the app uses its own client ID + secret
Which inbox it reads The signed-in user's inbox (Graph /me) The target mailbox you typed (Graph /users/{address})
Azure permission type Delegated permissions Application permissions
Needs admin consent? Usually not (a user can consent for themselves) Yes β€” an admin must grant consent
Token lifetime Short-lived + refresh token (stays signed in) Short-lived, re-fetched automatically (no refresh token)
Survives the person leaving? ❌ If you signed in as a person whose account is later disabled, it breaks βœ… Not tied to any person

When to use which

Use Delegated when:

  • You're setting up quickly and the mailbox account credentials are to hand.
  • The target mailbox is an account you can sign in as (a real licensed mailbox with a password / MFA you control).
  • You don't have a Global Admin handy to grant application consent.

Use App-only when:

  • The target mailbox is a shared mailbox or a service mailbox that nobody "logs in as".
  • You don't want the connection tied to a specific person's sign-in (no breakage when staff leave).
  • You want it to be impossible to accidentally read the wrong inbox β€” app-only always reads exactly the address you configured, regardless of who's signed into Microsoft on the browser.
  • You're happy to get a Global Admin to grant consent once.

Rule of thumb: for a long-lived, hands-off, "set it and forget it" service desk mailbox, app-only is the cleaner choice. For a quick start, delegated is fine β€” just make sure you sign in as the mailbox, not as yourself.


"Reading from the right inbox" safeguards (delegated)

Delegated mode has a sharp edge: the token belongs to whoever signed in, and it reads their inbox via /me. If you sign in as the wrong account β€” or if you sign in as support@, then later change the mailbox's target to helpdesk@ without re-authenticating β€” FreeITSM would happily keep reading the original account's mail while the label says something else. (This was the subject of GitHub issue #26.)

FreeITSM guards against that:

  1. It records who actually signed in. On authentication, the signed-in identity is captured into authenticated_as. It's read from the access token itself (the token is a JWT whose upn / preferred_username claim is the user) β€” so it works without any extra Graph permission. (See Scopes 101 below for why we don't use /me for this.)
  2. It checks before every read and send. If the recorded identity doesn't match the configured target mailbox, the operation is blocked with a clear message rather than silently reading the wrong inbox.
  3. Changing the address invalidates the sign-in. Edit a mailbox's target address (or switch its auth mode) and the stored identity is cleared, forcing a fresh sign-in. A stale token can't keep reading the old inbox.
  4. The list shows you the truth. Each mailbox row carries a plain-language status:
Badge Meaning
βœ“ Reading from addr Signed-in account matches the target β€” all good
App-only Reads the target directly via client credentials
Unverified Authenticated, but we haven't confirmed the account yet (e.g. authenticated under an older version) β€” clears itself on the next email check
⚠ Wrong account The token belongs to a different account than the target β€” blocked until you re-authenticate or switch to app-only

"Unverified" is harmless and self-healing. It just means authenticated_as hasn't been populated yet. Click the Check emails (envelope) icon once; the identity is back-filled from the token and the badge settles to green βœ“ or red ⚠.


Scopes & permissions 101 (for dummies)

If "scopes", "delegated vs application permissions", and "admin consent" make your eyes glaze over, read this once and it'll click.

What's a scope?

A scope (a.k.a. a permission) is a single capability you're asking Microsoft for, like Mail.Read ("read mail") or Mail.Send ("send mail as"). When FreeITSM connects, it asks for a list of scopes; Microsoft issues a token that is stamped with exactly those capabilities and nothing more. The token is like a backstage pass that lists which doors it opens.

FreeITSM asks for: Mail.Read, Mail.ReadWrite, Mail.Send, plus openid, email, offline_access (the last three are sign-in plumbing β€” identity + "keep me signed in").

Delegated permission vs Application permission

This is the part everyone trips on. Same-sounding permission, two completely different flavours:

  • Delegated permission = "the app, acting on behalf of a signed-in user, may do X to the things that user can already reach." Mail.Read delegated means "read the mail of whoever signed in." There's always a human in the loop at sign-in time.
  • Application permission = "the app, acting as itself with no user, may do X." Mail.ReadWrite application means "read/write mail in mailboxes the app is allowed to" β€” no human, no sign-in. This is what app-only mode uses.

The same scope name (e.g. Mail.ReadWrite) appears in both lists in Azure. When setting up app-only, you must add the Application versions (Azure labels them "Application" vs "Delegated"). Adding the delegated ones won't work for client credentials, and vice-versa.

What is "admin consent"?

Some permissions are powerful enough that an ordinary user isn't allowed to approve them for the whole organisation β€” a Global Administrator has to click "Grant admin consent" in Azure. All application permissions need admin consent (there's no user to consent, so an admin must). Many delegated permissions a user can consent to themselves at sign-in.

So... do I need User.Read?

No. This is the one that caused confusion. User.Read is the permission to read a user's profile via the Graph /me endpoint. FreeITSM does not require it:

  • For reading/sending mail, the Mail.* scopes are all that's needed.
  • For working out who signed in (the safeguard above), FreeITSM reads the identity straight out of the access token's own claims β€” which needs no permission at all. An earlier build briefly called /me for this, which does need User.Read and returned 403 without it; that dependency has been removed. You do not need to grant User.Read.

Why "least privilege" matters

Only ask for what you need. FreeITSM deliberately doesn't request User.Read, directory access, or anything beyond mail. Fewer scopes = smaller blast radius if a secret ever leaks, and an easier conversation with whoever approves the Azure app.


Setting up the Azure app registration

You need an App registration in Microsoft Entra ID (Azure AD). The same registration can serve delegated or app-only.

Common steps

  1. Entra ID β†’ App registrations β†’ New registration. Note the Application (client) ID and Directory (tenant) ID.
  2. Certificates & secrets β†’ New client secret. Copy the secret value immediately (you can't see it again).
  3. Enter the tenant ID, client ID and secret into the FreeITSM mailbox modal.

For delegated 4. Authentication β†’ Add a platform β†’ Web, and set the Redirect URI to your install's oauth_callback.php (FreeITSM pre-fills this). 5. API permissions β†’ Microsoft Graph β†’ Delegated: add Mail.Read, Mail.ReadWrite, Mail.Send, offline_access, openid, email. 6. Save, then in FreeITSM click Authenticate and sign in as the target mailbox.

For app-only 4. API permissions β†’ Microsoft Graph β†’ Application: add Mail.ReadWrite and Mail.Send. 5. Click Grant admin consent (requires a Global Admin). 6. (Optional, recommended) Lock the app down to just the mailboxes it should touch with an Application Access Policy β€” otherwise an app-only app can in principle read every mailbox in the tenant. 7. In FreeITSM, set Authentication = App-only. No sign-in step β€” it just works on the next check.


Google Workspace (brief)

Google mailboxes use the Gmail API with OAuth 2.0 and behave like delegated mode β€” you authorise once and FreeITSM reads/sends as that account. There's no app-only equivalent in the FreeITSM UI for Google; the redirect URI uses google_oauth_callback.php.


Troubleshooting

Symptom Cause Fix
Badge stuck on Unverified authenticated_as not populated yet Click Check emails once β€” it back-fills from the token
⚠ Wrong account / "Authentication mismatch" The signed-in token belongs to a different account than the target Re-authenticate signing in as the target, set the target back to the account you actually signed in as, or switch to app-only
App-only: "client-credentials token request failed" Secret wrong/expired, or admin consent not granted Check the client secret value; grant admin consent on the Application permissions
App-only reads nothing / 404 on the mailbox App not allowed to access that mailbox Confirm the target address is correct and (if you set one) the Application Access Policy includes it
Delegated: "Mailbox is not authenticated" No stored token Click Authenticate and sign in
Replies fail: "Could not determine mailbox for this ticket" Manual ticket with no mailbox_id Use the Send replies from dropdown when raising manual tickets

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally