Back to Articles

Building a Microsoft Teams CLI: Reading JWT Tokens and Terminal UIs in Go

[ View on GitHub ]

Building a Microsoft Teams CLI: Reading JWT Tokens and Terminal UIs in Go

Hook

The official Microsoft Teams desktop client consumes over 700MB of RAM to display text messages. A Go-based terminal interface can do the same job in under 20MB.

Context

Microsoft Teams has become ubiquitous in enterprise development workflows, but its official client presents a frustrating paradox: it’s essential for collaboration yet notorious for resource consumption. Built on Electron, the Teams desktop client regularly consumes 500-800MB of RAM and significant CPU cycles—a hefty price for what’s fundamentally a chat application. For developers who spend their day in terminals, switching to a GUI just to check if someone mentioned you in a channel feels like context-switching overhead.

This friction is particularly acute in specific scenarios: SSH sessions into remote development machines, resource-constrained environments like older laptops or containers, or workflows where you’re already living in tmux with Vim and want Teams monitoring without Alt-Tabbing to a separate application. The teams-cli project addresses this gap by providing a terminal-based interface to Microsoft Teams, trading the polish of a GUI for the efficiency of keyboard-driven navigation and minimal resource footprint. It’s part of a broader trend of bringing enterprise SaaS tools into terminal-native workflows, similar to how projects like gh brought GitHub interaction to the command line.

Technical Insight

teams-api Package

Authentication

teams-cli TUI

keyboard input

keyboard input

extract & generate

load token

validate JWT

fetch conversations

fetch messages

poll updates

authenticated requests

JSON responses

data

data

User Terminal

Navigation Pane

Teams/Channels/Chats

Message View Pane

Background Refresh

teams-token Tool

JWT Token File

Local Storage

Token Loader

HTTP Client

Microsoft Teams API

System architecture — auto-generated

The architecture of teams-cli reveals interesting decisions about authentication, API interaction, and terminal UI design in Go. At its core, the project separates into three layers: authentication token management, API communication through the teams-api package, and the TUI presentation layer.

Authentication is handled through JWT tokens rather than OAuth flows, which is both a strength and a complexity. Users must first generate tokens using a separate teams-token tool that extracts authentication credentials from an existing Teams session. This approach bypasses the need for Microsoft to officially support third-party clients (which they don’t), but requires manual token refresh. The tokens are stored locally and loaded by teams-cli on startup:

// Token loading from the teams-api package
func LoadToken(tokenPath string) (*AuthToken, error) {
    data, err := ioutil.ReadFile(tokenPath)
    if err != nil {
        return nil, fmt.Errorf("failed to read token file: %w", err)
    }
    
    var token AuthToken
    if err := json.Unmarshal(data, &token); err != nil {
        return nil, fmt.Errorf("failed to parse token: %w", err)
    }
    
    // Validate token expiration
    if time.Now().After(token.ExpiresAt) {
        return nil, fmt.Errorf("token expired at %v", token.ExpiresAt)
    }
    
    return &token, nil
}

The teams-api package handles all HTTP communication with Microsoft’s Teams backend. This separation is architecturally significant—it means teams-cli is essentially a thin presentation layer over a reusable API client. This enables other automation tools to leverage the same API package for scripting, webhooks, or custom integrations without reimplementing authentication and request handling.

The TUI itself uses a dual-pane layout pattern common in terminal applications like Midnight Commander or mutt. The left pane displays a hierarchical tree of teams, channels, and chats, while the right pane shows message content for the selected conversation. Navigation is keyboard-driven with Vim-like bindings—j/k for movement, Enter to select, and q to quit. The TUI implementation uses Go’s concurrency primitives to handle background refresh without blocking user interaction:

// Simplified refresh mechanism
type App struct {
    conversations ConversationTree
    messages      MessageView
    refreshTicker *time.Ticker
    updateChan    chan Update
}

func (a *App) StartBackgroundRefresh() {
    a.refreshTicker = time.NewTicker(30 * time.Second)
    go func() {
        for range a.refreshTicker.C {
            updates, err := a.client.FetchNewMessages()
            if err != nil {
                log.Printf("refresh error: %v", err)
                continue
            }
            a.updateChan <- Update{Messages: updates}
        }
    }()
}

This goroutine-based refresh pattern is idiomatic Go: a ticker fires every 30 seconds, fetches new messages in a background goroutine, and sends updates through a channel that the main UI loop consumes. This prevents the UI from freezing during network requests while keeping conversations synchronized with the server.

One clever implementation detail is the ‘doctor’ command, which diagnoses common setup issues. It validates token format, checks expiration, verifies network connectivity to Teams endpoints, and confirms API accessibility. This is excellent UX engineering—CLI tools often fail silently or with cryptic errors, but doctor provides actionable debugging information:

$ teams-cli doctor
 Token file found at ~/.config/teams-cli/token.json
 Token format valid
 Token expires in 6 hours
 Network connectivity to teams.microsoft.com
 API test failed: 401 Unauthorized
 Token may be expired or invalid
 Run 'teams-token' to generate a new token

The read-only nature of the current implementation reflects a pragmatic development strategy. Reading messages requires only GET requests to Teams APIs, while sending messages involves more complex CRUD operations, message formatting, and real-time synchronization. By focusing on read operations first, the project delivers immediate value (monitoring channels) while building toward the more complex write functionality.

Gotcha

The most significant limitation is the read-only restriction. You can browse teams, channels, and chats, and you can read messages, but you cannot send messages, react to messages, upload files, or perform any write operations. This fundamentally limits teams-cli to a monitoring role rather than a full client replacement. If your workflow involves active participation in Teams conversations throughout the day, you’ll still need the official client running alongside teams-cli.

The manual token management process introduces both friction and security considerations. Extracting JWT tokens from an existing Teams session isn’t straightforward for non-technical users, and tokens expire requiring periodic regeneration. There’s no automatic token refresh mechanism, so you’ll encounter authentication failures that require running teams-token again. From a security perspective, storing long-lived JWT tokens in plaintext on disk (even in a user-only readable file) is less secure than OAuth flows with short-lived tokens and refresh mechanisms. Organizations with strict security policies may prohibit this approach entirely. Additionally, because this isn’t an officially supported Microsoft client, there’s always a risk that Teams API changes could break functionality without warning—this is community-maintained tooling for an undocumented API surface.

Verdict

Use teams-cli if you live in the terminal and need lightweight monitoring of Teams channels without the resource overhead of the official client—ideal for checking mentions, monitoring deployment channels during on-call rotations, or running on remote servers via SSH. It’s particularly valuable in resource-constrained environments where every 500MB of RAM matters, or when you’re already running a terminal multiplexer and want Teams visibility without context switching to a GUI. Skip it if you need to actively participate in conversations beyond reading (no message sending yet), if your organization’s security policies prohibit unofficial API clients, or if you’re uncomfortable with manual JWT token extraction and management. Also skip if you prefer mouse-driven interfaces or need Teams features beyond basic messaging like video calls, screen sharing, or file collaboration. This is a monitoring tool first, not yet a full client replacement.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/fossteams-teams-cli.svg)](https://starlog.is/api/badge-click/developer-tools/fossteams-teams-cli)