Skip to content

Fix the presence write commands - #72

Open
aberoham wants to merge 5 commits into
osodevops:mainfrom
aberoham:fix/presence-writes
Open

Fix the presence write commands#72
aberoham wants to merge 5 commits into
osodevops:mainfrom
aberoham:fix/presence-writes

Conversation

@aberoham

@aberoham aberoham commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Default delegated logins omitted the permission all three presence-write
commands need. presence set and presence clear also sent an invalid or
missing sessionId. Reproductions and the Graph responses are in #70 and #71.

Default scopes (#70)

setPresence, setStatusMessage and clearPresence all require the delegated
Presence.ReadWrite permission, which DEFAULT_DELEGATED_SCOPES did not
request. Microsoft does not mark it admin-consent required, so it
can join the default set. docs/auth.md and docs/faq.md now list it with the
rest.

Existing sessions keep the scopes they were granted, so users need to run
teams auth login again. auth refresh cannot add a scope that was never
consented to. That instruction only reaches logins that take the defaults:
--scopes, TEAMS_CLI_SCOPES and a profile's scopes field replace the
default set rather than extend it, so a login that names its own scopes has to
restate the whole list with Presence.ReadWrite in it. README.md and
docs/auth.md say so.

Deployment follow-up, outside this PR: verify whether the OSO app
registration's static delegated-permission list also needs updating.

Session ID (#71)

Graph identifies a presence session by the application that owns it, and both
setPresence and clearPresence document that application's ID as the
sessionId they expect. clearPresence was sent an empty body; setPresence
sent a new random UUID on every invocation.

Both now use a configured client ID — TEAMS_CLI_CLIENT_ID or the profile's
client_id — when there is one, because that names the application directly
and Microsoft asks callers to treat access tokens as opaque. Otherwise the
value comes from the token's own claims, preferring azp and falling back to
appid, matching redeem_refresh_token's claim order; that is the path a
login through the built-in application takes, since it configures no client ID.
Blank values are skipped, and a token with no usable application-ID claim and
no configured client ID fails with an auth error rather than a guess. set and
clear agree either way, because both resolve the value the same way.

Clearing a session that is not there

Graph answers 404 when the application has no presence session to clear, which
is the state clear exists to reach, so a second clear — or a retry after an
ambiguous response — failed against presence that was already automatic. That
answer is no longer an error.

It is not the same outcome as closing a live session: a session opened under a
different application ID answers the same way. clear reports which one Graph
gave, presence_cleared or no_presence_session, and both set and clear
report the session ID they used, so a clear run under different configuration
than the set is visible rather than silent. A retry whose earlier attempt
succeeded but lost its response also sees 404, and the prose says so.

Tests

wiremock asserts the serialized setPresence and clearPresence payloads, and
covers clearPresence answering 404 directly and answering it after a retried
503. Unit tests cover default-scope membership and session-ID selection: a
configured client ID winning over the token, a blank one falling through, an
opaque token accepted when a client ID is configured, and the azp/appid
order, blank claims and undecodable tokens on the fallback path.

Not covered

--expiration is still passed through without local validation. Presence writes
also do not reject app-only tokens locally, unlike message writes, which call
require_delegated_token.

aberoham and others added 5 commits August 21, 2026 22:46
The presence write commands all call Graph operations that require the
delegated Presence.ReadWrite permission, but the built-in scope string asked
only for Presence.Read.All, so every one of them returned 403 for a login that
did not override the defaults. Microsoft does not mark Presence.ReadWrite as
admin-consent required, so it can join the default set.

Closes osodevops#70
Graph identifies a presence session by the application that owns it, and both
setPresence and clearPresence expect that application's ID as sessionId.
clearPresence was sent an empty body and rejected with a 400, while setPresence
sent a fresh UUID on every invocation, so nothing could name a session again
once it was opened. Both now read the ID from the access token's azp or appid
claim, ignoring blank values, which stays correct if the profile's configured
client ID changes or the token arrived through TEAMS_CLI_ACCESS_TOKEN.

Closes osodevops#71
`--scopes`, `TEAMS_CLI_SCOPES` and a profile's `scopes` field replace the
default delegated scope string rather than extend it, so the instruction to
log in again after `Presence.ReadWrite` joined the defaults only works for
logins that take the defaults. A login that names its own scopes has to
restate the whole list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Graph keys a presence session to the application that owns it, and the CLI
read that application's ID out of the access token's `azp` or `appid` claim.
Microsoft asks callers to treat access tokens as opaque, and a Graph token is
not guaranteed to be a readable JSON Web Token, so a configured client ID —
`TEAMS_CLI_CLIENT_ID` or the profile's `client_id` — now names the application
directly and wins. The token claim remains the fallback, which is what logins
through the built-in application use, since those configure no client ID.

`set` and `clear` still agree, because both resolve the value the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Graph answers 404 when the application has no presence session to clear,
which is the state `clear` exists to reach, so a second clear — or a retry
after an ambiguous response — failed against presence that was already
automatic. That answer is no longer an error.

It is not the same outcome as closing a live session, though: a session
opened under a different application ID answers the same way. `clear` now
reports which one Graph gave, `presence_cleared` or `no_presence_session`,
and both `set` and `clear` report the session ID they used, so a `clear` run
under different configuration than the `set` is visible rather than silent.

A retry whose earlier attempt succeeded but lost its response also sees 404.
The prose and a wiremock test covering 503 then 404 both say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant