Identity Before Convenience

Fastpotify's multiple sign-ins can look like unnecessary friction until the capability map is taken seriously. This chapter follows the application's identity lifecycle, from browser approval and PKCE through account verification, protected storage, refresh, sign-out, and recovery. The practical goal is to understand which grant is being used, which account it belongs to, and what Fastpotify must reject when storage or an old asynchronous result cannot be trusted.

One Spotify account, three authorization jobs

Fastpotify does not treat “signed in to Spotify” as one universal condition. Its Web API access and its local streaming access come from different Spotify-facing contracts, so the client keeps three possible grants separate: a shared Web API grant, an optional personal Web API grant, and a playback grant for the librespot streaming session.

The shared Web API grant is the default library relationship. It gives Fastpotify the registered application identity used for broad catalogue and playlist coverage. A personal Web API app is optional. It can provide a separate Development Mode quota for supported operations, but it does not replace the shared route for every request. Fastpotify still needs shared access for views and searches whose coverage depends on Spotify-owned or external playlists and for operations that Development Mode does not support.

The playback grant has a different purpose. It uses the streaming client identity that librespot can play with and asks for the streaming permission rather than the broad set of library permissions. Local playback therefore requires a second browser approval even after the library has loaded. The separation is useful to the user: a Free account can browse and search without being treated as though it should be able to start a local audio engine. It is also useful to the system: a streaming credential is not silently reused as a Web API credential, and library access does not imply permission to play on this computer.

The three grants are independent, but they are not unrelated. The shared and personal Web API sessions must describe the same account when both are installed. The playback grant must be converted into a reusable credential that names the same verified account before it can be handed to the local engine. Fastpotify is therefore not choosing among three arbitrary login methods. It is maintaining one account relationship across three authorization jobs, each with a different scope and failure path.

Fastpotify uses Authorization Code with PKCE for these browser flows. PKCE can be explained without treating it as a mysterious security acronym. The application creates a secret verifier and sends Spotify a derived challenge. The browser completes the consent step on Spotify's own page. Spotify returns a one-time authorization code to a loopback listener on the local computer, and Fastpotify exchanges that code together with the verifier. The process sees the code, not the user's Spotify password.

The loopback listener also checks the state value that Fastpotify generated for that attempt. A request to the expected callback path is not enough by itself. The returned state must match, and an error response from Spotify must be treated as a failed approval. Requests that are malformed, aimed at another path, or left over from an old browser tab are ignored or rejected rather than accepted as the current sign-in. This is a small boundary with a large practical consequence: the application can ask the browser to handle account credentials without turning the local callback into an unrestricted command channel.

The flow has a useful failure shape for a desktop program. The browser may never return, the user may cancel consent, the callback may time out, or the token endpoint may be temporarily unreachable. Fastpotify can cancel the waiting flow and clear the authorizing state without treating an unfinished browser visit as a successful sign-in. A later attempt starts with a new verifier, state value, and authorization generation. The old attempt is not allowed to finish into the new one merely because its network response arrived late.

After exchanging the code, Fastpotify still does not consider the Web API session ready. It verifies the grant by asking Spotify for the account. That verification gives the application an account identity with which later work can be associated. The optional personal grant must verify as the same account as the shared grant. A playback credential must likewise be bound to the account verified through the Web API before the local engine starts. If a personal grant belongs to a different account, the gateway refuses to install it rather than creating a hybrid session whose library and playback state describe different people.

This account check matters even when each individual credential is valid. A valid grant for account A can still be wrong for a Fastpotify process currently showing account B. Without the check, a personal quota could be attached to the wrong library, or a saved playback credential could cause the computer to join Connect under a different identity from the one the user sees in the interface. The failure is not a bad password. It is a mismatch between valid identities, which is why the client treats it as its own boundary.

The check also keeps restoration from becoming an invisible account switch. On startup, Fastpotify can find a saved playback credential before every Web API grant has finished loading. It must not start that engine merely because the credential is structurally valid. The account verified by the current Web API session has to match the account named by the playback grant. Otherwise a computer could begin playing for one account while the interface is loading another person's library.

The application can also make progress when the two Web API sessions become ready at different times. A verified personal app may allow supported work to begin while shared verification is still waiting. Shared-only views do not pretend that the personal session can answer them; they remain pending until the shared session is available. This is a more precise behavior than either blocking the entire application on the slowest grant or declaring the user signed out when only one capability is still authorizing.

This partial readiness is an example of identity being tied to capability rather than to a single screen-wide flag. A catalogue request can use the personal session when it is ready, while the complete playlist library waits for shared coverage. The user may see useful data before the whole authorization picture has settled, but the client must keep the unresolved boundary visible in its loading behavior. Progress and correctness are not opposites here; progress means doing only the work whose authority has been established.

The lifecycle can be summarized as a set of transitions rather than a single login switch:

stateDiagram-v2 [*] --> SignedOut SignedOut --> BrowserApproval: start a grant flow BrowserApproval --> CodeReceived: browser returns code and state BrowserApproval --> SignedOut: cancel or timeout CodeReceived --> Verifying: exchange code with PKCE verifier Verifying --> AccountBound: account accepted Verifying --> Rejected: grant rejected or malformed Verifying --> AccountMismatch: different account AccountMismatch --> BrowserApproval: authorize again Rejected --> BrowserApproval: fresh browser approval AccountBound --> Persisting: remember verified grant Persisting --> Ready: protected write read back Persisting --> ReadyWithWarning: store unavailable ReadyWithWarning --> Ready: usable for current session Ready --> Refreshing: access token near expiry Refreshing --> Ready: refresh succeeds Refreshing --> Reauthorize: refresh grant rejected Ready --> SignedOut: user signs out Reauthorize --> BrowserApproval: browser must mint a new grant SignedOut --> Revoked: invalidate generations and write marker Revoked --> SignedOut: no grant restored

The diagram separates two ideas that are often collapsed. A grant can be usable for the current session even when it cannot yet be remembered safely. Conversely, a grant can exist in a credential store but be unusable because Spotify rejected its refresh token or because it belongs to another account. Fastpotify's visible status is the result of these transitions, not merely the presence of a token-shaped record.

Persistence is part of sign-out

Once a grant has been verified, Fastpotify needs to remember it so the browser is not required on every launch. It stores durable grants through the host platform's credential service: Secret Service on Linux, Keychain on macOS, and Credential Manager on Windows. Ordinary settings and session data remain readable JSON, but the Web API and playback grants occupy separate protected slots. This separation reflects different authority, not just a preference for naming files.

The separation also makes sign-out and replacement explicit. A shared Web API grant can be forgotten without treating a personal app setting as the same object. A personal Client ID change clears the old personal grant rather than silently applying it to the new application identity. Playback has its own stored credential and its own account check. The cache directory may hold audio, artwork, lyrics, or library metadata, but deleting those caches does not sign the user out because they are not the authority that restores a Spotify session.

The credential store is not called directly from the interface thread. Native calls run on a dedicated worker with a bounded queue and a timeout. That design recognizes an uncomfortable desktop reality: a keyring may be locked, unavailable, slow to answer, or waiting for user interaction. If the UI thread made that call synchronously, a storage problem could appear as a frozen application. If the command loop waited indefinitely, shutdown and playback could be held hostage by a service whose failure is outside Fastpotify's control.

The result is a deliberately asymmetric user experience. A new sign-in can work for the current session even if the keyring is locked, while Fastpotify explains that it cannot remember the grant yet. The application does not need to pretend that persistence succeeded, and it does not need to punish the user by making the interface unusable. The current in-memory authority and the durable restart authority are separate states with separate failure messages.

A successful save has more steps than writing bytes. Fastpotify validates that the grant belongs in the slot being written, serializes it as a protected record, writes it to the native store, reads it back, and checks that the returned bytes match. Only then does it clear the local migration or revocation state and remove an old plaintext copy when one exists. If the protected store cannot retain or verify the grant, the current session may still use it, but the application reports that the sign-in could not be remembered. “It worked once” and “it is durably stored” are separate outcomes.

This distinction makes migration safer. Older versions used unencrypted token files. On upgrade, Fastpotify can read a valid legacy grant and move it into the protected store, but it removes the old file only after the new record has been read back successfully. A failed migration keeps the original available for recovery and reports a warning. The client does not solve a storage failure by writing a new plaintext fallback, because that would make a temporary keyring problem a permanent exposure.

Migration also has to preserve the difference between a usable current grant and a trustworthy durable record. The old file may remain temporarily because removing it would destroy the only recoverable copy, not because the application wants to keep two equally authoritative stores forever. Once the protected copy has been verified, the old copy is removed so a later launch cannot choose a stale plaintext version over the current protected record.

Sign-out has the reverse ordering problem. It is not enough to delete whatever file or key happens to contain a token. Fastpotify first invalidates the authorization generation and writes a non-secret revocation marker before attempting to delete protected entries and legacy copies. If the keyring is locked and deletion fails, the marker tells the next launch not to restore the old grant. Without that marker, a failed deletion could make a user who had explicitly signed out appear signed in again.

The same generation mechanism protects asynchronous work. A lease represents permission to read or write one grant for one authorization generation. When sign-out or a replacement authorization increments the generation, work carrying the older lease becomes stale. A refresh that returns after sign-out cannot save its new token into the replacement session. A browser result from the old attempt cannot authorize playback after the user has changed accounts. A late engine connection can be shut down rather than installed into a session that no longer owns it.

This prevents a particularly confusing class of desktop bugs. The user signs out, signs in again, and then sees an error from the old session arrive and clear the new one. Or a slow browser flow finishes after a new account is already connected and silently replaces the current user. Fastpotify's generations and authorization attempts make these results old in a way the command loop can test. Time alone is not a sufficient defense; the result must carry the right ownership.

This is why storage, refresh, and sign-out cannot be designed as independent features. Suppose the user signs out while a refresh is waiting on Spotify and a keyring write is waiting on the native store. The correct outcome is not simply “delete the current token.” The refresh must lose permission to update the account, the pending write must fail its generation check, the protected record must not be restored on the next launch, and the interface must settle in a signed-out state. The ordering is the security property.

Refresh itself follows the same distinction between temporary failure and invalid identity. Fastpotify refreshes a Web API access token before it is close to expiry and can force a refresh after an unauthorized response. If the token endpoint is unreachable, the current token may remain usable when it has not expired. If Spotify rejects the saved refresh grant, the client treats that grant as expired and asks for a new browser approval. It does not keep retrying a credential that the service has already refused.

This lets the interface distinguish a network interruption from a revoked relationship. A temporary inability to reach the token endpoint should not erase a still-valid grant, because doing so would turn a transient outage into needless reauthorization. A definitive rejection should not be hidden behind endless retries, because the browser is the only path that can mint a replacement grant. The same principle appears in storage: preserve a recoverable state when the infrastructure is unavailable, but do not preserve an identity that the authority has rejected.

The protected store still has a clear limit. It reduces the exposure created by copying ordinary application files, but it cannot protect a usable live session from arbitrary code running as the same user. On Linux, the result also depends on the desktop keyring being configured and unlocked. This is a calibrated security boundary, not a claim that native storage makes credentials invulnerable. Fastpotify's design improves the failure and exposure story while leaving the operating system's user boundary intact.

Seen this way, the extra browser approval is the visible part of a larger identity model. Web API access, personal quota, and streaming playback have different jobs. Account verification binds them when they are combined, protected storage gives them durable lifecycles, and generations stop old work from crossing into a new session. The next problem is what Fastpotify does after it knows who is authorized: it must route each request to the right grant while keeping the interface useful when Spotify answers slowly, unevenly, or in two parts.

References