Skip to content

Fix the presence write commands - #72

Merged
sionsmith merged 7 commits into
osodevops:mainfrom
aberoham:fix/presence-writes
Aug 29, 2026
Merged

Fix the presence write commands#72
sionsmith merged 7 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 7 commits August 29, 2026 18:17
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>
Adding a scope to the default set only reaches a session where consent for it
can be given. Dynamic consent covers a scope the registration does not list
statically, but a tenant that reserves consent to an administrator rejects the
login regardless, so the advice to run `teams auth login` again is incomplete
for anyone authenticating through a registration of their own. `auth
consent-url` is the route in that case, and the text now says so rather than
implying the permission has to be added to the registration first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C736oc7CeNtRZmAJrXm8sG
The presence write commands were the only writes in the CLI that did not
call require_delegated_token; an app-only token cannot act against `/me`,
so the request could only fail at Graph. The shared message no longer
claims the caller was sending a chat message, since presence reaches it now.

Adds the CHANGELOG entries for osodevops#70 and osodevops#71.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqBTdhSRTBabYv7kkEiUkE
@sionsmith
sionsmith force-pushed the fix/presence-writes branch from 8ce6909 to 5099ce9 Compare August 29, 2026 17:19
@sionsmith
sionsmith merged commit 3134871 into osodevops:main Aug 29, 2026
6 checks passed
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.

2 participants