Skip to content

Saving OAuth email inbox overwrites tokens with masked placeholder values #386

Description

@stackblaze-adam

Summary

After successfully connecting a Microsoft (or Google) OAuth email inbox, saving the inbox edit form can overwrite real OAuth credentials in the database with the UI masked placeholder ••••••••••. This silently breaks IMAP/SMTP authentication until OAuth is reconnected.

Environment

  • Libredesk version: v2.3.1 (Build: 5cd691f)
  • Channel: Microsoft OAuth email inbox
  • Provider: Microsoft 365 / Outlook (support@example.com)

Steps to reproduce

  1. Connect a Microsoft OAuth email inbox via Admin → Inboxes → New inbox → Microsoft
  2. Complete OAuth authorization successfully (toast: "Inbox connected successfully")
  3. Edit the inbox (e.g. rename it) and click Save
  4. Wait for the next IMAP poll interval (~5 minutes)

Expected behavior

Saving unrelated inbox fields (name, settings, etc.) should preserve the stored OAuth access_token, refresh_token, and client_secret.

Actual behavior

IMAP authentication fails on every poll:

error authenticating with OAuth to IMAP server: imap: NO AUTHENTICATE failed.

Inspecting the database shows the encrypted OAuth fields decrypt to the literal placeholder string:

Field Stored value (decrypted)
access_token ••••••••••
refresh_token ••••••••••
client_secret ••••••••••

Libredesk is effectively sending Bearer •••••••••• to the IMAP server.

Root cause

When the inbox is fetched via the API, sensitive OAuth fields are masked as •••••••••• in the response. When the edit form is saved, those masked values are sent back in the update payload.

In internal/inbox/inbox.go, the Update() method only preserves existing OAuth values when the incoming value is empty:

// Preserve existing OAuth fields if update has empty
for k, v := range currentCfg.OAuth {
    if updateCfg.OAuth[k] == "" {
        updateCfg.OAuth[k] = v
    }
}

Because •••••••••• is not empty, it overwrites the real encrypted tokens.

Note: the OAuth reconnect callback correctly uses UpdateConfig() directly and bypasses this logic — which is why reconnect fixes the issue temporarily until the form is saved again.

Workaround

  1. Click Reconnect to restore real tokens
  2. Do not click Save on the inbox edit page after reconnecting

Suggested fix

Treat masked placeholder values the same as empty when merging OAuth config on update, e.g.:

func isMaskedSecret(s string) bool {
    return s == "" || s == "••••••••••" || strings.HasPrefix(s, "••••")
}

for k, v := range currentCfg.OAuth {
    if isMaskedSecret(updateCfg.OAuth[k]) {
        updateCfg.OAuth[k] = v
    }
}

Alternatively, omit access_token, refresh_token, and client_secret from update payloads entirely unless the user explicitly reconnects OAuth.

The same preservation logic may need the same treatment for IMAP/SMTP passwords if they are also masked in API responses.

Additional context

  • OAuth reconnect stores valid tokens (JWT audience https://outlook.office.com, correct scopes, correct UPN)
  • After reconnect without saving the form, IMAP works and emails are imported successfully
  • This bug can be mistaken for an Azure permissions or mailbox IMAP configuration issue, making it difficult to diagnose

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions