Skip to content
Rasmus Wulff Jensen edited this page Sep 16, 2026 · 2 revisions

TrelloDotNet supports Trello's OAuth 2.0 authorization-code flow with PKCE. OAuth 2.0 is intended for applications that act on behalf of a Trello user. The user must complete an interactive browser authorization at least once. After that, a refresh token can keep the application authorized without further interaction while the grant and refresh-token chain remain valid.

The implementation has two distinct parts:

  • TrelloOAuth2Flow creates the authorization URL, parses the callback, exchanges the authorization code, and can manually refresh tokens.
  • TrelloClient uses an access token to call the Trello API, optionally managing refresh-token rotation for you.

OAuth 2.0 does not provide Trello with a client-credentials or service-account flow. A client ID and client secret identify the application; they do not replace the user authorization.

Choosing an authentication mode

Scenario API Token handling
User-facing application that should remain authorized TrelloClient.FromOAuth2RefreshToken TrelloDotNet refreshes access tokens and asks the application to persist every replacement refresh token.
An external service already manages OAuth tokens TrelloClient.FromOAuth2AccessToken The supplied access token is used as-is. The application replaces the TrelloClient when it obtains a new access token.
Existing Trello Auth API key and token new TrelloClient(apiKey, token) Uses the original Trello Auth authentication mechanism rather than OAuth 2.0.

Prerequisites

Create or select an app in the Trello apps administration page, then configure its OAuth 2.0 settings:

  1. Choose whether the app is a public or confidential client.
  2. Add the exact callback URL that your application will use.
  3. Enable every Trello scope the application needs.
  4. Copy the client ID.
  5. For a confidential client, securely copy the client secret.

The callback URL passed to TrelloDotNet must exactly match one configured for the app, including its scheme, host, port, path, and trailing slash behavior.

Public and confidential clients

Client type Client secret Typical use
Public No An application that cannot securely protect a secret.
Confidential Yes A server-side application or application with a secure backend.

Never embed a confidential client secret in browser code, Blazor WebAssembly, desktop binaries, mobile applications, or other environments where an end user can extract it.

Complete console example

The following example follows the same flow as the OAuth2Tester sample. It uses a temporary file only to make token rotation visible. A production application should use protected, durable storage.

using TrelloDotNet;
using TrelloDotNet.Model;

string clientId = "<client-id>";
string redirectUri = "https://localhost:7248/oauth/callback";
string refreshTokenPath = Path.Combine(Path.GetTempPath(), "trello-refresh-token.txt");

string? refreshToken = await ReadPreviouslyStoredTokenAsync();

if (string.IsNullOrWhiteSpace(refreshToken))
{
    TrelloOAuth2AuthorizationRequest request =
        TrelloOAuth2Flow.CreateAuthorizationRequest(
            clientId,
            redirectUri,
            [TrelloOAuth2Scope.ReadBoard]);

    // Save request.State and request.CodeVerifier until the callback arrives.
    Console.WriteLine("Open this URL in a browser:");
    Console.WriteLine(request.AuthorizationUri);

    Console.Write("Paste the complete callback URL: ");
    string callbackUrl = Console.ReadLine()
        ?? throw new InvalidOperationException("No callback URL was provided.");

    (string code, string returnedState) =
        TrelloOAuth2Flow.ParseCallbackUrl(callbackUrl);

    if (!string.Equals(returnedState, request.State, StringComparison.Ordinal))
    {
        throw new InvalidOperationException("OAuth state validation failed.");
    }

    TrelloOAuth2TokenResponse tokens =
        await TrelloOAuth2Flow.ExchangeCodeAsync(
            clientId,
            code,
            request.CodeVerifier,
            redirectUri);

    refreshToken = tokens.RefreshToken
        ?? throw new InvalidOperationException("No refresh token was returned.");

    // Save the initial refresh token before relying on it later.
    await StoreTokenAsync(refreshToken);
}

TrelloClient client = TrelloClient.FromOAuth2RefreshToken(
    clientId,
    refreshToken,
    refreshTokenSaver: StoreTokenAsync);

Card card = await client.GetCardAsync("<card-id>");

async Task StoreTokenAsync(string replacementRefreshToken)
{
    await File.WriteAllTextAsync(refreshTokenPath, replacementRefreshToken);
}

async Task<string?> ReadPreviouslyStoredTokenAsync()
{
    return File.Exists(refreshTokenPath)
        ? await File.ReadAllTextAsync(refreshTokenPath)
        : null;
}

For a confidential client, pass the client secret during both the authorization-code exchange and creation of the refreshing TrelloClient:

TrelloOAuth2TokenResponse tokens =
    await TrelloOAuth2Flow.ExchangeCodeAsync(
        clientId,
        code,
        codeVerifier,
        redirectUri,
        clientSecret: clientSecret);

TrelloClient client = TrelloClient.FromOAuth2RefreshToken(
    clientId,
    clientSecret,
    tokens.RefreshToken,
    refreshTokenSaver: StoreTokenAsync);

Understanding the flow

1. Create the authorization request

TrelloOAuth2AuthorizationRequest request =
    TrelloOAuth2Flow.CreateAuthorizationRequest(
        clientId,
        redirectUri,
        [
            TrelloOAuth2Scope.ReadMember,
            TrelloOAuth2Scope.ReadBoard,
            TrelloOAuth2Scope.WriteBoard
        ]);

The result contains:

  • AuthorizationUri: Send the user's browser to this URL.
  • CodeVerifier: A PKCE value required when exchanging the returned authorization code.
  • State: A correlation value that must match the value returned to the callback.

generateRefreshToken is the final optional argument and defaults to true. This adds the special offline_access scope so Trello can return a refresh token:

TrelloOAuth2Flow.CreateAuthorizationRequest(
    clientId,
    redirectUri,
    scopes,
    generateRefreshToken: false);

Use false only when an access token is sufficient and the application does not need unattended access after that token expires.

2. Preserve state and the PKCE verifier

The authorization happens outside the application, so State and CodeVerifier must survive the browser redirect. Store them server-side, in protected session state, or in another tamper-resistant store associated with the login attempt.

  • State protects the callback from being confused with or substituted by another authorization attempt.
  • CodeVerifier proves that the application exchanging the code is the application that created the PKCE challenge.
  • State and code verifier are different values and are not interchangeable.
  • Treat each state value as single-use and remove it after a successful callback.

3. Handle the callback

(string code, string returnedState) =
    TrelloOAuth2Flow.ParseCallbackUrl(completeCallbackUrl);

if (!string.Equals(returnedState, expectedState, StringComparison.Ordinal))
{
    throw new InvalidOperationException("OAuth state validation failed.");
}

ParseCallbackUrl extracts and URL-decodes the authorization code and state. It cannot validate the state on its own because only the application knows which stored authorization attempt is expected.

Authorization codes are short-lived and sensitive. Exchange them promptly and do not write callback URLs to normal application logs.

4. Exchange the code

TrelloOAuth2TokenResponse tokens =
    await TrelloOAuth2Flow.ExchangeCodeAsync(
        clientId,
        code,
        codeVerifier,
        redirectUri);

The redirect URI must be the same URI used when creating the authorization request. A confidential client must also pass its client secret.

The response can contain:

  • AccessToken: Used as a bearer token for Trello API requests.
  • RefreshToken: Exchanged for a new access-token and refresh-token pair.
  • ExpiresIn: Access-token lifetime in seconds.
  • Scope: The scopes granted to the token.
  • TokenType: Normally Bearer.
  • IdToken: An OpenID Connect ID token when Trello returns one.

Persist the initial refresh token immediately. Do not wait for the first API call.

Creating a TrelloClient

Automatically managed refresh tokens

TrelloClient client = TrelloClient.FromOAuth2RefreshToken(
    clientId,
    refreshToken,
    refreshTokenSaver: SaveRefreshTokenAsync);

The client exchanges the refresh token when it needs an access token, saves the replacement refresh token through refreshTokenSaver, and then uses the new access token for API requests.

The saver is part of the correctness of the OAuth flow, not merely a notification callback. It must not complete successfully until the replacement refresh token has been durably persisted.

async Task SaveRefreshTokenAsync(string replacementRefreshToken)
{
    await tokenStore.ReplaceAsync(replacementRefreshToken);
}

If the saver throws, TrelloDotNet does not use the access token associated with the unpersisted refresh token. A later request on the same client retries persistence before attempting another token refresh.

Externally managed access tokens

TrelloClient client =
    TrelloClient.FromOAuth2AccessToken(accessToken);

This client always uses the supplied access token. It does not refresh it. When an external token manager obtains a replacement access token, create a new TrelloClient from that token.

This mode is useful when token refresh and distributed coordination are handled by a separate service.

Refresh-token rotation and client lifetime

Trello refresh tokens are rotating and may only be used once. A successful refresh returns both a new access token and a replacement refresh token. The replacement refresh token becomes the value that must be used and persisted from then on.

One client per refresh-token lineage

Do not construct two TrelloClient instances from the same refresh token:

// Do not do this.
TrelloClient first = TrelloClient.FromOAuth2RefreshToken(
    clientId, sharedRefreshToken, SaveRefreshTokenAsync);

TrelloClient second = TrelloClient.FromOAuth2RefreshToken(
    clientId, sharedRefreshToken, SaveRefreshTokenAsync);

Both clients initially hold the same token. If one rotates it, the other still holds the consumed value and its refresh can fail.

Reuse one long-lived TrelloClient for each independently authorized token lineage. The refresh implementation coordinates concurrent refresh attempts within that one client instance.

Multiple clients are safe when they use refresh tokens from separate authorization grants. Sharing the same client ID is not a problem. Merely having two different token strings is not enough if one is an old token and the other is its rotated replacement.

Multiple servers or processes

FromOAuth2RefreshToken coordinates only inside one client instance. It does not provide a distributed lock across servers or processes.

For a distributed application, use a central token service that:

  1. Stores the current refresh token.
  2. Serializes refresh operations with a distributed lock or equivalent transaction.
  3. Atomically replaces the stored refresh token.
  4. Supplies valid access tokens to application instances.

Those instances can use TrelloClient.FromOAuth2AccessToken. They must create a new client when the external service supplies a new access token.

Storing refresh tokens safely

Refresh tokens grant ongoing access to a user's Trello data. Treat them as credentials.

Production storage should:

  • Encrypt tokens at rest.
  • Restrict access to the smallest possible application identity.
  • Avoid writing tokens, callback URLs, or token endpoint responses to logs.
  • Replace the old token atomically rather than creating ambiguous multiple current values.
  • Use durable storage rather than only process memory.
  • Retry transient storage failures while the new token remains available in memory.
  • Associate each token with the correct user, grant, scopes, and OAuth client.
  • Have a recovery plan for a terminated process during rotation.

A database protected by application-level encryption, an operating-system credential store, or a managed secret service can be appropriate. A plain file is useful for understanding the console sample but is not recommended for production.

If a rotating refresh token is successfully exchanged but its replacement is permanently lost, the application cannot safely return to the consumed token. The user normally must complete the interactive authorization flow again.

Using OAuth 2.0 in Blazor

In a Blazor Server or server-rendered Blazor application, create the request on the server and navigate the browser to AuthorizationUri:

TrelloOAuth2AuthorizationRequest request =
    TrelloOAuth2Flow.CreateAuthorizationRequest(
        clientId,
        callbackUrl,
        [TrelloOAuth2Scope.ReadBoard]);

await pendingAuthorizationStore.SaveAsync(
    request.State,
    request.CodeVerifier);

navigationManager.NavigateTo(
    request.AuthorizationUri.ToString(),
    forceLoad: true);

The callback endpoint should load and consume the stored verifier by state, validate the returned state, exchange the code, and persist the refresh token.

Do not rely only on a Blazor circuit field to preserve state and verifier. The external redirect can recreate the page or circuit. Use protected server-side session state or another durable short-lived store.

Blazor WebAssembly cannot protect a confidential client secret. Use a public client where supported, or send the callback result to a secure backend that performs the exchange and stores the tokens.

Scopes

Use TrelloOAuth2Scope values rather than manually constructing scope strings:

Enum value Trello scope
ReadBoard read:board:trello
WriteBoard write:board:trello
WriteBoardMembership write:board:membership:trello
ReadOrganization read:organization:trello
WriteOrganization write:organization:trello
WriteOrganizationMembership write:organization:membership:trello
ReadMember read:member:trello
WriteMember write:member:trello
ReadEnterprise read:enterprise:trello
WriteEnterprise write:enterprise:trello

Only request scopes the application requires. Scopes must also be enabled in the Trello app configuration.

offline_access is intentionally not an enum value. TrelloDotNet adds it when generateRefreshToken is true.

Webhooks

OAuth 2.0 access tokens can create Trello webhooks when their scopes grant access to the target model. OAuth 2.0 webhook requests use the same signature-validation shape as Trello Auth webhooks, but a confidential OAuth 2.0 client's client secret is the signing key.

Configure the secret used by the WebHook Data Receiver through TrelloClientOptions.Secret:

TrelloClientOptions options = new TrelloClientOptions
{
    Secret = clientSecret
};

Public OAuth 2.0 clients do not have a client secret. Atlassian recommends a confidential client for applications receiving webhook callbacks because webhook receivers require a backend.

FAQ

Can OAuth 2.0 be used without any user interaction?

Not for the initial grant. Trello currently uses a three-legged authorization-code flow, so a Trello user must sign in and grant access through a browser. A client secret identifies a confidential application but does not authorize it to act as a user.

Once a refresh token exists, the application can normally continue unattended until the grant is revoked, the refresh-token chain expires, or a required replacement token is lost.

Does the browser need to run on the same machine as the application?

No. The application can display or otherwise provide the authorization URL to the user. The user can authorize in a browser that can reach the configured callback URL. The callback must ultimately reach the application and match the registered redirect URI.

Why is RefreshToken null after exchanging the code?

The authorization request probably did not include offline_access. CreateAuthorizationRequest requests it by default through generateRefreshToken: true. Also verify that the OAuth client and requested scopes are configured correctly in Trello.

Is state the same as the PKCE code verifier?

No. State correlates and protects the browser callback. The code verifier is used when exchanging the authorization code and proves possession of the PKCE secret generated at the start of the flow.

Why does CreateAuthorizationRequest generate state automatically?

Secure state must be unpredictable. TrelloDotNet generates it cryptographically so callers do not accidentally use a constant, username, timestamp, or other guessable value. The application remains responsible for storing it and validating the callback value.

Why must the initial refresh token be saved immediately?

The refresh-token saver only runs when TrelloDotNet performs a refresh. If the application exits after the initial code exchange but before that refresh, an unsaved initial token is lost. Persist it as soon as ExchangeCodeAsync succeeds.

Why must every replacement refresh token be saved?

Refresh tokens rotate and may only be used once. After a successful exchange, the old token has been consumed. Losing the replacement normally means the interactive authorization flow must be repeated.

What happens if refreshTokenSaver fails?

The API call fails and the new access token is not used. The same TrelloClient retains the replacement refresh token in memory and retries saving it on a later request before attempting another refresh. Retry transient storage failures, and remember that a process crash can still lose a token that exists only in memory.

Can two TrelloClient instances use the same refresh token?

No. Each instance coordinates only its own refresh operations. One instance can consume and replace the token while the other still holds the old value.

Can two TrelloClient instances use different refresh tokens?

Yes, when those tokens belong to independent authorization grants. Each client then manages its own token lineage. Do not treat an old token and its rotated replacement as independent grants.

Can one TrelloClient be used by concurrent operations?

The managed refresh-token implementation serializes refresh operations inside that client so concurrent requests do not independently consume the same refresh token. This protection does not extend to other client instances or processes.

Does FromOAuth2AccessToken refresh the token?

No. It uses exactly the supplied access token. An external token manager must obtain a replacement and create another TrelloClient when required.

What happens if Trello rejects an access token before its expected expiry?

TrelloDotNet does not automatically refresh and replay a request after a 401 response. Automatically replaying write operations could repeat a partially completed operation. Handle the failure according to the operation and authorization state. If the current grant is still valid, stop using the old client and create a new client from the latest securely stored refresh token; if the grant was revoked, the user must authorize again.

Is a custom HttpClient shared by API and token requests?

Yes. The HttpClient supplied to FromOAuth2RefreshToken is used for both Trello API calls and requests to Atlassian's token endpoint. Its default headers, message handlers, logging, and retry policies must therefore be safe for both hosts. Do not configure an unrelated default Authorization header, and ensure sensitive token-request bodies are redacted from logs.

Why does a client created from a refresh token contact the token endpoint on its first API call?

The factory receives a refresh token, not a current access token. The first API call therefore exchanges the refresh token for a usable access token and a replacement refresh token.

Can a public OAuth 2.0 client receive webhooks?

Webhook callbacks require a backend, and webhook signature validation for OAuth 2.0 uses the client secret. Atlassian recommends using a confidential OAuth 2.0 client for applications that receive webhooks.

What requires another interactive authorization?

Typical causes include the user revoking the grant, the refresh token expiring, losing the current replacement refresh token, or receiving an unrecoverable invalid_grant response.

Related Atlassian documentation

Clone this wiki locally