---
url: https://integrations.pocketarc.com/features/authenticated-identity.md
---
# Authenticated identity

Ask which account an integration's credentials authenticate as (the principal behind the token) without a provider-specific call. A provider opts in by implementing `IdentifiesAuthenticatedUser`, and the package adds caching, resilience, and a CLI surface on top.

This answers "who are we acting as?" for self-authored filtering (skip the activity your own integration produced), audit, and display.

## The contract

```php
use Integrations\Contracts\IdentifiesAuthenticatedUser;
use Integrations\Data\AuthenticatedUser;
use Integrations\Models\Integration;

interface IdentifiesAuthenticatedUser
{
    public function authenticatedUser(Integration $integration): AuthenticatedUser;
}
```

The implementation makes the upstream "who am I" call through the integration's request builder, so the circuit breaker, rate limiter, and request logging all apply, and maps the payload to a provider-agnostic `AuthenticatedUser`.

## The AuthenticatedUser DTO

A Spatie `Data` object with a stable, provider-agnostic shape:

```php
class AuthenticatedUser extends Data
{
    public function __construct(
        public readonly string $id,
        public readonly ?string $username = null,
        public readonly ?string $name = null,
        public readonly ?string $email = null,
        public readonly array $raw = [],
    ) {}
}
```

* `id` — the stable provider user id.
* `username` — the provider's human handle (GitHub login, Zendesk email, …).
* `name`, `email` — when the upstream exposes them.
* `raw` — the full upstream payload, for provider-specific needs the mapped fields don't cover.

## Reading the identity

```php
$user = $integration->authenticatedUser();
$user->id;        // "583231"
$user->username;  // "octocat"
```

`authenticatedUser()` throws `UnsupportedByProvider` when the provider doesn't implement the contract. That's a misconfiguration to fix, not a runtime failure to degrade past. Pre-check with `supportsAuthenticatedUser()` to branch without catching:

```php
if ($integration->supportsAuthenticatedUser()) {
    $me = $integration->authenticatedUser();
}
```

## Caching

The identity rarely changes. To skip the live call on a hot path, or while the upstream is down, pass `cacheFor`. The identity is then cached per integration:

```php
// Cached for a day. The upstream is called only when no cached entry exists.
$me = $integration->authenticatedUser(cacheFor: now()->addDay());

// Clear the cached entry and fetch the identity again.
$me = $integration->authenticatedUser(cacheFor: now()->addDay(), refresh: true);
```

If `cacheFor` is null, every call is live.

With `refresh: true`, `authenticatedUser()` clears the cached entry before it fetches the identity. If the fetch fails, the cache stays empty. The next call with `cacheFor` then fetches the identity from the upstream. It does not return the account you asked to replace.

On a cache hit, `authenticatedUser()` returns a plain `AuthenticatedUser` built from the cached fields. The identity is cached as JSON. A provider's `AuthenticatedUser` subclass is therefore returned as the base class, and an object in `raw` is returned as decoded JSON. On a cache miss, `authenticatedUser()` returns the provider's object unchanged.

### Credential changes

The OAuth callback and revoke routes call `forgetAuthenticatedUser()`. After a user reconnects as a different account or revokes the token, the next call does not return the old identity.

If you change an integration's credentials in a different way (for example, from an API key field in your own settings screen), call `forgetAuthenticatedUser()` after the change:

```php
$integration->update(['credentials' => $newCredentials]);
$integration->forgetAuthenticatedUser();
```

## Treat a failure as unknown

The call goes through the request executor, so it can throw: a provider error, or `CircuitOpenException` when the breaker is open. A warm cache shields a hot path from this, but with no cached value to fall back on, the exception propagates. Catch it and carry on without the identity rather than letting it fail unrelated work:

```php
try {
    $me = $integration->authenticatedUser(cacheFor: now()->addDay());
} catch (\Throwable) {
    $me = null; // proceed without the identity
}
```

## Implementing it on a provider

Map the upstream "who am I" response to an `AuthenticatedUser`, making the call through the integration's request builder so it's logged and gated:

```php
use Integrations\Contracts\IdentifiesAuthenticatedUser;
use Integrations\Data\AuthenticatedUser;
use Integrations\Models\Integration;

class GitHubProvider implements IntegrationProvider, IdentifiesAuthenticatedUser
{
    public function authenticatedUser(Integration $integration): AuthenticatedUser
    {
        $response = $integration
            ->at('/user')
            ->get(fn () => Http::withToken($integration->credentialsArray()['token'])
                ->get('https://api.github.com/user'));

        return new AuthenticatedUser(
            id: (string) $response['id'],
            username: $response['login'],
            name: $response['name'] ?? null,
            email: $response['email'] ?? null,
            raw: $response,
        );
    }
}
```

If you pass `cacheFor`, the identity must be JSON-encodable, and every top-level key of `raw` must be a string. Otherwise, `authenticatedUser()` throws `UncacheableAuthenticatedUser`. The exception's `user` property holds the identity that the provider returned. A call without `cacheFor` is not affected.

When PHP decodes JSON, it converts a numeric string key such as `"123"` to an integer. So if you decode `raw` from an upstream object that has a numeric top-level key, the identity can't be cached.

## In the CLI

`integrations:health` shows the resolved identity for providers that support it:

```text
=== GitHub (github) ===
  Health: healthy
  ...
  Authenticated as: octocat (id: 583231)
```

It caches the lookup briefly so a report over many integrations doesn't fan out a live call each, and a failure to resolve degrades to `Authenticated as: unknown (…)` rather than aborting the report.
