Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
- [Handling refresh failures](#handling-refresh-failures)
- [Handling MFA during token exchange (mfa_required)](#handling-mfa-during-token-exchange-mfa_required)
- [Completing an out-of-band (OOB) challenge with polling](#completing-an-out-of-band-oob-challenge-with-polling)
- [Token Vault (Federated Connection Access Tokens)](#token-vault-federated-connection-access-tokens)
- [Retrieving a federated connection token](#retrieving-a-federated-connection-token)
- [Forcing a refresh](#forcing-a-refresh-1)
- [Handling a missing refresh token or exchange failure](#handling-a-missing-refresh-token-or-exchange-failure)
- [Organizations](#organizations)
- [Extra parameters](#extra-parameters)
- [Roles](#roles)
Expand Down Expand Up @@ -607,6 +611,75 @@ public async Task<IActionResult> PollOob()
> 5-minute window. This is the **same requirement** as the authentication cookie that already
> carries these tokens.

## Token Vault (Federated Connection Access Tokens)

[Token Vault](https://auth0.com/docs/secure/tokens/token-vault) lets your web app obtain a **third-party API access token** (for a federated connection such as Google, GitHub, or Slack) for the logged-in user — so your app, or an agent acting on the user's behalf, can call that provider's API. The token is obtained by exchanging the session's refresh token; the user does not have to re-authenticate.

A typical use case: the user logged in with (or has linked) their Google account, and your app needs a Google access token to read their calendar. Instead of running a separate Google OAuth flow, you exchange the existing refresh token for a Google connection token.

> :information_source: Token Vault requires refresh tokens. Configure `UseRefreshTokens = true` and a `ClientSecret`, and enable Token Vault for the connection in the Auth0 Dashboard. Connection tokens are cached in the session, keyed by connection name, and reused until they near expiry.

> :information_source: Unlike MRRT, the federated-connection exchange does **not** take a requested `scope` — it returns the scopes already granted for the connection. Tokens are therefore cached per **connection**, not per audience/scope. To change the granted scopes, reconfigure the connection (or re-link the account) in the Auth0 Dashboard.

> :warning: **Token storage and cookie size.** Like MRRT, each connection token is cached in the encrypted **authentication cookie** by default, so retrieving tokens for several connections grows the session the same way fanning out across audiences does. If you expect to hold tokens for more than a couple of connections, move the session **server-side** — see [Server-side session storage](#server-side-session-storage). Where the token set is persisted is the only thing that changes; the API is identical either way.

### Retrieving a federated connection token

Use `HttpContext.GetAccessTokenForConnectionAsync` with an `AccessTokenForConnectionRequest` describing the `Connection`. The SDK serves a cached token from the session when one is present and unexpired, and only exchanges the refresh token otherwise. Newly obtained tokens are persisted back into the session automatically.

```csharp
[Authorize]
public async Task<IActionResult> CallGoogleCalendar()
{
var googleToken = await HttpContext.GetAccessTokenForConnectionAsync(new AccessTokenForConnectionRequest
{
Connection = "google-oauth2"
});

if (googleToken == null)
{
// No refresh token available, or the exchange failed — see "Handling…" below.
return Challenge();
}

var request = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/calendar/v3/users/me/calendarList");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", googleToken);

var response = await _httpClient.SendAsync(request);
return Content(await response.Content.ReadAsStringAsync());
}
```

> :information_source: `GetAccessTokenForConnectionAsync` returns `null` rather than throwing when no refresh token is available or the exchange fails. Always check for `null` before using the token.

The optional `LoginHint` disambiguates which linked identity to use when the user has more than one. It is the **provider-side identity-provider user ID** (e.g. a Google user ID) — not the Auth0 user `sub`, and not the user's email.

`LoginHint` is part of the cache key: tokens for different login hints on the same connection are cached separately, so requesting identity B never returns identity A's cached token. A request with no `LoginHint` is cached separately from one that specifies a hint.

```csharp
var googleToken = await HttpContext.GetAccessTokenForConnectionAsync(new AccessTokenForConnectionRequest
{
Connection = "google-oauth2",
LoginHint = "108251234567890123456"
});
```

### Forcing a refresh

Set `ForceRefresh = true` to bypass the cache and always exchange the refresh token for a new connection token. The freshly retrieved token replaces the cached entry.

```csharp
var googleToken = await HttpContext.GetAccessTokenForConnectionAsync(new AccessTokenForConnectionRequest
{
Connection = "google-oauth2",
ForceRefresh = true
});
```

### Handling a missing refresh token or exchange failure

A federated connection token can only be obtained via the session's refresh token. When none is present, the `OnMissingRefreshToken` event fires and the method returns `null`. When a refresh token is present but the exchange is rejected, the `OnAccessTokenRefreshFailed` event fires (carrying `StatusCode`, `Error`, `ErrorDescription`) and the method returns `null`. These are the same events used by MRRT — see [Handling refresh failures](#handling-refresh-failures) and [Detecting the absense of a refresh token](#detecting-the-absense-of-a-refresh-token) for full configuration examples.

## Organizations

[Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,23 @@ var accessToken = await HttpContext.GetAccessTokenAsync(new AccessTokenRequest

For configuring per-audience default scopes, forcing a refresh, and handling refresh failures, see the [Multi-Resource Refresh Tokens (MRRT) Examples](EXAMPLES.md#multi-resource-refresh-tokens-mrrt).

## Token Vault (Federated Connection Access Tokens)

Token Vault lets your web app obtain a third-party API access token (for a federated connection such as Google, GitHub, or Slack) for the logged-in user, by exchanging the session's refresh token — without running a separate provider OAuth flow.

```csharp
var googleToken = await HttpContext.GetAccessTokenForConnectionAsync(new AccessTokenForConnectionRequest
{
Connection = "google-oauth2"
});
```

For forcing a refresh, supplying a login hint, and handling missing refresh tokens or exchange failures, see the [Token Vault Examples](EXAMPLES.md#token-vault-federated-connection-access-tokens).

## API reference
Explore public API's available in auth0-aspnetcore-authentication.

- [AccessTokenForConnectionRequest](https://auth0.github.io/auth0-aspnetcore-authentication/api/Auth0.AspNetCore.Authentication.AccessTokenForConnectionRequest.html)
- [Auth0WebAppOptions](https://auth0.github.io/auth0-aspnetcore-authentication/api/Auth0.AspNetCore.Authentication.Auth0WebAppOptions.html)
- [Auth0WebAppWithAccessTokenOptions](https://auth0.github.io/auth0-aspnetcore-authentication/api/Auth0.AspNetCore.Authentication.Auth0WebAppWithAccessTokenOptions.html)
- [LoginAuthenticationPropertiesBuilder](https://auth0.github.io/auth0-aspnetcore-authentication/api/Auth0.AspNetCore.Authentication.LoginAuthenticationPropertiesBuilder.html)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Auth0.AspNetCore.Authentication
{
/// <summary>
/// Describes a request for a federated connection (Token Vault) access token —
/// a third-party API token (e.g. Google, GitHub) for the logged-in user.
/// </summary>
public class AccessTokenForConnectionRequest
{
/// <summary>
/// The federated connection to retrieve an access token for (e.g. "google-oauth2").
/// Required.
/// </summary>
public string Connection { get; set; } = null!;

/// <summary>
/// Optional login hint forwarded to the token endpoint to disambiguate which linked
/// identity to use. This is the provider-side identity provider user ID (e.g. a Google
/// user ID) — not the Auth0 user <c>sub</c> and not the user's email.
/// </summary>
public string? LoginHint { get; set; }

/// <summary>
/// When <c>true</c>, always exchanges the refresh token for a new connection token,
/// ignoring any cached token. Defaults to <c>false</c>.
/// </summary>
public bool ForceRefresh { get; set; }
}
}
43 changes: 43 additions & 0 deletions src/Auth0.AspNetCore.Authentication/ConnectionTokenSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;

namespace Auth0.AspNetCore.Authentication
{
/// <summary>
/// Represents a third-party (federated connection) access token retrieved for a
/// specific connection and cached alongside the primary token in the session.
/// </summary>
internal class ConnectionTokenSet
{
/// <summary>
/// The federated connection name the token was retrieved for (e.g. "google-oauth2").
/// </summary>
[JsonPropertyName("connection")]
public string Connection { get; set; } = null!;

/// <summary>
/// The login hint the token was retrieved for, when supplied. Part of the cache key
/// alongside <see cref="Connection"/> so tokens for different linked identities on the
/// same connection are cached separately rather than overwriting one another.
/// </summary>
[JsonPropertyName("login_hint")]
public string? LoginHint { get; set; }

/// <summary>
/// The federated connection access token.
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = null!;

/// <summary>
/// Expiration time as a Unix timestamp (seconds).
/// </summary>
[JsonPropertyName("expires_at")]
public long ExpiresAt { get; set; }

/// <summary>
/// The scopes granted for the connection token, when returned. Optional.
/// </summary>
[JsonPropertyName("scope")]
public string? Scope { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Auth0.AspNetCore.Authentication
{
/// <summary>
/// Pure helper functions for matching, upserting and pruning per-connection
/// (federated connection) access tokens stored in the session.
/// </summary>
internal static class ConnectionTokenSetHelpers
{
/// <summary>
/// Finds the cached token for a connection and login hint, or <c>null</c> when none is
/// present. The login hint is part of the key: a request with a given hint only matches a
/// cached entry stored for that same hint (a request with no hint matches an entry stored
/// with no hint), so tokens for different linked identities on one connection don't collide.
/// </summary>
public static ConnectionTokenSet? FindConnectionTokenSet(IEnumerable<ConnectionTokenSet>? sets, string connection, string? loginHint = null)
{
return sets?.FirstOrDefault(set => Matches(set, connection, loginHint));
}

/// <summary>
/// Applies a freshly retrieved connection token to the collection: prunes expired
/// entries first, then replaces the entry for the connection and login hint if present,
/// otherwise appends a new one. Returns the updated list (a new list instance).
/// </summary>
public static List<ConnectionTokenSet> UpsertConnectionTokenSet(IEnumerable<ConnectionTokenSet>? sets, string connection, AccessTokenResponse response, string? loginHint = null)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var result = sets?.Where(set => set.ExpiresAt > now).ToList() ?? new List<ConnectionTokenSet>();

var entry = new ConnectionTokenSet
{
Connection = connection,
LoginHint = loginHint,
AccessToken = response.AccessToken,
ExpiresAt = now + response.ExpiresIn,
Scope = response.Scope
};

var existing = result.FirstOrDefault(set => Matches(set, connection, loginHint));
if (existing != null)
{
result[result.IndexOf(existing)] = entry;
}
else
{
result.Add(entry);
}

return result;
}

private static bool Matches(ConnectionTokenSet set, string connection, string? loginHint)
{
return set.Connection == connection && set.LoginHint == loginHint;
}
}
}
Loading
Loading