---
title: "Building Desktop OAuth for Outlook and Slack"
description: "Why Foxl uses direct PKCE for Microsoft, a secret-bearing relay exchange for Slack, loopback callbacks, and scoped token storage."
author: "Foxl Team"
date_published: "2026-04-17"
date_modified: "2026-07-16"
canonical_url: "https://foxl.ai/blog/integrations-outlook-slack"
markdown_url: "https://foxl.ai/blog/integrations-outlook-slack/index.md"
image: "https://foxl.ai/blog/integrations-architecture.png"
social_image: "https://foxl.ai/blog/social/integrations-outlook-slack.png"
content_type: "Deep dive"
topics: ["OAuth","PKCE","Credential boundaries"]
products: ["Foxl Agent","Foxl Relay"]
---

<!-- Generated by scripts/blog-publishing.mjs; do not edit. -->

# Building Desktop OAuth for Outlook and Slack

> PKCE for Microsoft, relay-proxied secret for Slack, loopback callback servers, and skill-gated tool registration. The engineering behind 37 new agent actions.

- Author: Foxl Team
- Published: 2026-04-17
- Last reviewed: 2026-07-16
- Reading time: 8 minutes
- Canonical HTML: [https://foxl.ai/blog/integrations-outlook-slack](<https://foxl.ai/blog/integrations-outlook-slack>)

![Two desktop OAuth paths: direct PKCE with Microsoft and a Slack code exchange that keeps the client secret at the relay boundary.](<https://foxl.ai/blog/integrations-architecture.png>)

Foxl v0.2.9 introduced OAuth-backed Microsoft 365 and Slack tools. v0.2.10 added Slack workspace selection and corrected repeated-connect behavior. The implementation solves two separate problems: obtaining delegated user access without asking for pasted tokens, and keeping unavailable tool schemas out of the agent context. The provider flows share a local callback server, but their token exchanges have different trust boundaries.

## The problem

A desktop agent needs delegated access to email, calendars, tasks, and messages. Requiring each user to create an application and paste a token makes installation difficult and encourages long-lived credentials to be handled manually. Bundling a provider client secret in the desktop application is not a solution because, as the [OAuth guidance for native apps](<https://www.rfc-editor.org/rfc/rfc8252.html>) explains, a distributed binary cannot keep a shared secret confidential.

OAuth moves authorization to the provider's consent page. Foxl opens that page in the system browser, receives a short-lived authorization code on loopback, exchanges it, and stores the resulting connection locally. The exact exchange differs by provider.

## Architecture

1. The Integrations page calls the local Foxl server to begin a provider flow.
2. The local server creates random state, starts a short-lived loopback listener, and returns an HTTPS authorization URL.
3. Electron opens only `http:` or `https:` URLs through its `openExternal` IPC handler.
4. The provider redirects the browser to loopback with a code and state. The listener accepts only the expected path and matching state.
5. Foxl exchanges the code, saves a provider connection, and re-runs tool registration. The UI polls sanitized status until it sees the connection.

### Microsoft 365: public client with PKCE

The Microsoft flow uses the `common` tenant and the [authorization-code flow](<https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow>) with [S256 PKCE](<https://www.rfc-editor.org/rfc/rfc7636.html>). Foxl generates a verifier, hashes it into a challenge, puts the challenge in the authorization request, and retains the verifier in memory until the callback. The code exchange goes directly from the local Foxl server to `login.microsoftonline.com`; the Foxl relay is not on that path.

Requested scopes cover identity, offline access, mail read/write and send, calendars, tasks, contacts, and mailbox settings. The resulting refresh token supports long-running desktop use. The Graph client refreshes before a recorded expiry and retries once after a 401. Other Graph failures are returned to the tool as errors.

### Slack: secret-bearing exchange at the relay

Slack's [OAuth v2 access exchange](<https://api.slack.com/authentication/oauth-v2>) uses the application's client secret. Foxl keeps that secret in the relay Worker environment instead of placing it in the desktop bundle. The current data path is:

```
desktop -> relay: { code, redirect_uri, client_id }
relay -> Slack:  { code, redirect_uri, client_id, client_secret }
Slack -> relay:  token-bearing OAuth response
relay -> desktop: the same response data as JSON
```

This boundary matters. The relay sees the authorization code, redirect URI, client ID, and Slack's token response. That response can contain both `authed_user.access_token` and a top-level bot access token. The handler parses the response and sends it back to the desktop. It makes no D1 write and has no explicit token logging, but the tokens do pass through the relay runtime. It is therefore inaccurate to say that Slack tokens never leave the desktop or that the relay sees only the code.

The tools use the Slack user access token, so API calls execute with the connected user's Slack permissions. The current connection record also retains the returned bot token in metadata, although the Slack tool client does not read it.

## Loopback callback contract

Microsoft registers `http://localhost:3848/integrations/microsoft/callback`. Slack uses port `3849` with its corresponding path. The server binds to `127.0.0.1`, rejects other paths, validates a random state value, returns a small success or error page, and closes after the callback. An unanswered flow times out after five minutes.

State protects the browser round trip from an unsolicited callback. PKCE separately binds the Microsoft authorization code to the local verifier. Slack does not use the PKCE helper in this implementation; its confidential exchange is performed by the relay.

The callback module attempts the next few ports when the preferred port is occupied, but the authorization URL and stored redirect URI still contain the preferred fixed port. That means the alternate listener is not a reliable recovery path. In practice, ports 3848 and 3849 must be available for the implemented browser flow.

## Token storage and visibility

Provider connections are serialized as JSON into the local `agent_memory` table under keys such as `integration:microsoft`. The database is `DATA_DIR/pilot.sqlite`, which resolves to `~/.foxl/data/pilot.sqlite` in the default production layout. Microsoft access and refresh tokens, the Slack user token, and the retained Slack bot token are stored in that record.

The application does not encrypt these integration values or move them into the operating-system keychain. Protection therefore depends on local account permissions and any disk encryption configured on the machine. This storage boundary is part of the broader [local-first trust model](<https://foxl.ai/blog/why-local-first>). The status API returns only provider, connection time, label, and profile fields; it does not expose the token fields to the renderer.

Disconnecting deletes Foxl's local connection record and removes the tool. It does not call a Microsoft or Slack revocation endpoint. Conversely, revoking Foxl at the provider does not delete the local record, so the tool can remain registered until an API call fails. A provider-side revocation and a Foxl-side disconnect are distinct operations.

## Skill-gated tools

The Outlook and Slack implementations each expose one agent tool with an `action` discriminator instead of registering every operation as a separate top-level tool. Their skill files declare `requires: [integration:microsoft]` and `requires: [integration:slack]`. At startup and after a local connect or disconnect, the registry checks for the corresponding `agent_memory` record, adds eligible tools, and removes ineligible integration tools without restarting the server.

This is local availability gating, not continuous authorization validation. It reduces irrelevant tool schemas and prevents an unconnected tool from being offered to the model. It does not prove that a stored token is still accepted by the provider.

## Current tool surface

The Outlook dispatcher covers mail, calendar, and Microsoft To-Do. Mail includes reading, sending, replies, forwarding, search, folders, drafts, attachments, contacts, moves, categories, and message updates. Calendar includes views, meeting changes, availability, room discovery, search, and shared calendars. To-Do includes lists, tasks, and checklist items.

The current Outlook action enum contains 13 mail values, 6 calendar values, and 3 To-Do values. That is 22 action identifiers. Older copy and the tool description say 21, so the enum is the authoritative contract until those surfaces are reconciled.

The Slack dispatcher currently contains 16 action identifiers for identity, search, recent messages, channel and member listing, history, threads, DMs, user lookup, attachment download, message posting, reactions, and external file upload. Access to a private conversation still depends on Slack scopes and the connected user's membership.

## Workspace selection

Without a workspace value, Foxl uses Slack's standard authorization host. When a user enters a team domain such as `mycompany`, Foxl builds the authorization URL under `mycompany.slack.com`. This chooses the consent host; it does not expand the permissions granted by Slack.

## Failure modes

- State mismatch, consent denial, missing code, or callback timeout aborts the loopback flow without saving a connection.
- A busy preferred loopback port can prevent the provider from reaching the listener because the advertised redirect URI is fixed.
- Microsoft refresh failure leaves the stored connection present but unusable until the user reconnects.
- Slack has no refresh path in this implementation. An invalid or revoked user token causes Slack API calls to fail.

## Validation

Validate each provider with a dedicated real test account. Confirm that the consent page shows the intended scopes, a mismatched state is rejected, the status response contains no credentials, provider calls use the delegated identity, Microsoft refresh survives access-token expiry, and disconnect immediately removes the tool from the registry. For Slack, inspect Worker logs and D1 to confirm the handler neither logs nor persists token payloads.

The retained automated suite does not contain focused tests for these OAuth modules, the loopback listener, or the relay exchange route. Source inspection and build success verify structure, not a live provider round trip. A gated integration test must use real Microsoft and Slack sandbox accounts rather than a pretend OAuth service.

## Current limitations

The default Microsoft client ID is still marked in the repository as a placeholder that must be replaced with a Foxl-owned app registration. Integration tokens remain plaintext in local SQLite. The Electron `foxl://integrations/callback` handler and local exchange endpoint exist, but the current command path does not carry the callback payload through the renderer, and the Microsoft fallback would also need its retained PKCE verifier. The fixed-port loopback path is the working contract today.

## References and further reading

1. [Microsoft identity platform and OAuth 2.0](<https://learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow>) - Reference
2. [Slack OAuth V2](<https://api.slack.com/authentication/oauth-v2>) - Reference
3. [Foxl skills and integrations](<https://docs.foxl.ai/docs/desktop/skills>) - Documentation