Authentication design¶
Goals¶
- Authenticate end users (never service principals) against Microsoft Entra ID.
- Support multiple accounts in multiple tenants simultaneously.
- Get tokens with the audience that OneLake DFS and the Fabric REST API both accept:
https://storage.azure.com/. - Cache tokens persistently across daemon restarts using the macOS Keychain.
- Handle token refresh transparently; surface re-auth requests minimally (per Sam's preference for non-intrusive UX).
- Public cloud only in MVP.
Microsoft Entra App Registration¶
We register a multi-tenant public client application in our own tenant:
| Property | Value |
|---|---|
| Display name | OneLake File Explorer for macOS |
| Supported account types | Accounts in any organizational directory (multi-tenant) |
| Redirect URI | http://localhost (Public client/native) |
| Allow public client flows | Yes |
| API permissions | https://storage.azure.com/user_impersonation (delegated). Optionally also Fabric Service if needed for admin-search endpoints in later phases. |
The client ID lives in internal/auth/client.go as a constant — it is a public identifier, not a secret.
Why our own registration instead of reusing the Azure CLI client ID (
04b07795-…)? Branded consent screen ("OneLake File Explorer for macOS" instead of "Microsoft Azure CLI"), clean telemetry attribution, our own scope control, and we don't get caught by tenant policies that specifically block the Azure CLI app.
Token acquisition flows¶
We support two flows, chosen automatically per environment:
- Interactive browser (default on macOS desktop):
- Opens
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?...in the system default browser. - We run a tiny localhost HTTP server (random port between 49152–65535) to catch the redirect.
- User signs in, consents (first time), and is redirected back to
http://localhost:PORT/?code=...&state=.... -
We exchange the authorization code for an access token + refresh token.
-
Device code (fallback for SSH sessions, headless environments, or when
--device-codeis passed): - We call
/devicecodeendpoint to getuser_codeanddevice_code. - We display:
To sign in, visit https://microsoft.com/devicelogin and enter the code: XXXX-YYYY. - We poll
/tokenevery 5 seconds until success, error, or expiry.
Both flows are implemented on top of the Go MSAL library github.com/AzureAD/microsoft-authentication-library-for-go (msal-go). MSAL Go natively supports both flows on a PublicClientApplication.
Why MSAL Go and not the Azure SDK's
azidentity?azidentity'sInteractiveBrowserCredentialandDeviceCodeCredentialare convenience wrappers around MSAL anyway, with less control over the cache backend and tenant-switching semantics. For a multi-account / multi-tenant app where we manageAuthenticationRecords explicitly, going to MSAL directly is cleaner.
Multi-tenant + multi-account model¶
Each account is identified by a user-chosen short alias (e.g. work, client-a). Internally each account has:
type Account struct {
Alias string // user-chosen, unique
HomeAccountID string // MSAL's unique identifier (objectId.tenantId)
Username string // UPN, for display only
TenantID string // GUID
TenantName string // display name, optional
AddedAt time.Time
}
We construct one PublicClientApplication per account-tenant (MSAL recommends a separate authority per tenant for clean cache scoping). The authority for account X is https://login.microsoftonline.com/{tenantId}, not /common or /organizations — that way refresh and silent acquisition stay scoped to the right tenant.
A shared in-process account registry keeps the list of accounts and dispatches token requests to the right MSAL client.
Token cache: macOS Keychain¶
MSAL Go uses an in-memory cache.ExportReplace interface by default. We implement it backed by the macOS Keychain:
- One Keychain item per account, labeled
dev.debruyn.ofem — <alias> (<UPN>). - Service:
dev.debruyn.ofem. - Account name:
<HomeAccountID>. - Data: MSAL's serialized JSON token cache for that account.
Library: github.com/keybase/go-keychain or github.com/zalando/go-keyring. Both work on macOS without any C dependency.
Keychain access is scoped per-app (via the app's code signature once we have a Developer ID). Until then (Phase 0 dev builds), access is per-user.
Token refresh and silent acquisition¶
On every OneLake request:
- Look up the cached access token for the account.
- If present and not within 5 minutes of expiry → use it.
- Otherwise call MSAL
AcquireTokenSilentto refresh. - If silent refresh fails with
interaction_required(e.g. Conditional Access challenge, MFA expired) → mark account asneeds_reauth, queue a menu-bar error indicator. No system notification. - The next time the user opens the menu bar app (or runs
ofem status), the indicator shows them which account needs re-auth. They re-runofem login --account <alias>to interactively unblock.
Conditional Access / MFA challenges¶
Per Sam's preference: silent retry on a fixed interval (30 minutes for AADSTS50076 / AADSTS50079 / interaction_required), plus a menu bar error indicator. No macOS notification.
Rationale: data engineers are used to seeing the "click to re-auth" pattern in many tools and would rather have a quiet UI than a poking notification.
Logout / account removal¶
ofem account remove <alias>:
- Removes the Keychain item.
- Removes the account from the registry config.
- Optionally calls Microsoft's
/oauth2/v2.0/logout(best effort, no error if it fails). - Removes the account's folder from
~/OneLake/<alias>/after a confirmation prompt (or--force).
Sovereign clouds¶
Out of scope for MVP. Architecturally we keep the authority host configurable per account (authority_host: login.microsoftonline.com default), so adding US Gov / China / Germany is a config change + endpoint mapping when someone files an issue.
Security considerations¶
- No client secret: public client only, so there is no secret to leak.
- PKCE is used for the authorization code flow (MSAL Go does this by default).
- State parameter is generated per flow and validated.
- Random port for localhost redirect avoids collisions and reduces attack surface vs. a fixed well-known port.
- Refresh tokens are stored in Keychain, scoped per account; they cannot be exported via the CLI.
- No token printing: even
ofem debugcommands never echo tokens. - The Entra App Registration is owned by Sam; users only consent to delegated permissions and can revoke at any time via https://myapplications.microsoft.com.