Using Web Authentication (OAuth2) to access Figma from Windows and Uno Platform Application

OAuth2 is quite a common authentication standard for web services to use. Whilst a lot of services have now updated to use OpenID Connect, there are still some, like the Figma api, that still use OAuth2. In this post we’re going to walk through using the Uno.Extensions web authentication package to simplify not only the authentication process but also the subsequent calling of the Figma api.

We’re going to start with a new application created using the Uno Platform Template Wizard from within Visual Studio. Using the recommended preset, the only change is to check the Web option for Authentication (I’ve also opted to go with Mvvm for this sample but Mvux will work just as well)

Settings

We’ll start by updating the AppSettings to include information we’ll need. In order to authenticate with Figma you’ll need to register an application. Navigate to the MyApps tab in the Figma developer portal. Add an new application and make note of the generated ClientId, ClientSecret and the Callback url (to support device authentication we’ve added a protocol base callback, btrfigma://callback)

This information can be used to populate the appsettings.json (or the appsettings.development.json for debug) file. The following json can be used but make sure you update the AppConfig values according to the application you’ve registered with Figma.

{
  "AppConfig": {
    "ClientId": "{figma client id}",
    "ClientSecret": "{figma client secret}",
    "RedirectUri": "btrfigma://callback"
  },
  "WebAuthentication": {
    "LoginStartUri": "https://www.figma.com/oauth?client_id={0}&redirect_uri={1}&scope=files:read,file_comments:write&response_type=code",
    "AccessTokenKey": "code"
  },
  "FigmaAuthClient": {
    "Url": "https://www.figma.com",
    "UseNativeHandler": true
  },
  "FigmaApiClient": {
    "Url": "https://api.figma.com",
    "UseNativeHandler": true
  }
}

We’ll need to modify the AppConfig class to replace the Environment property with ClientId, ClientSecret and RedirectUri

public record AppConfig
{
    public string? ClientId { get; init; }
    public string? ClientSecret { get; init; }
    public string? RedirectUri { get; init; }
}

Note that in addition to the AppConfig section, there are three additional sections:

  • WebAuthentication – This is used by Uno.Extensions web configuration to define the url used to start the login process. The inclusion of the redirect_uri parameter will automatically be used to determine the end of the web authentication process
  • FigmaAuthClient – The initial web authentication process will return a code to the application. This needs to be converted into access and refresh tokens by calling the token endpoint, which is part of the oauth endpoint off www.figma.com
  • FigmaApiClient – The Figma apis for retrieving information about figma files are defined on a different base uri, api.figma.com

This information will be used in the subsequent sections as we complete the configuration for the application.

Configuration

There are three main additions that need to be made to the configuration of the application defined in app.xaml.cs: Registering Auth Endpoint, Registering Api Endpoint, Configuring Web Authentication.

Registering Auth Endpoint

In order to register an endpoint that can be called by the application, the first thing to do is to create an interface that defines the path and parameters for the request. The IFigmaAuthClient interface includes two methods, GetToken and GetRefreshToken.

[Headers("Content-Type: application/json")]
public interface IFigmaAuthClient
{
    [Post("/api/oauth/token?client_id={clientId}&client_secret={clientSecret}&redirect_uri={redirectUri}&code={code}&grant_type=authorization_code")]
    Task<AuthResponse> GetToken(string clientId, string clientSecret, string redirectUri, string code, CancellationToken cancellationToken = default);

    [Post("/api/oauth/refresh?client_id={clientId}&client_secret={clientSecret}&refresh_token={refreshToken}")]
    Task<AuthResponse> GetRefreshToken(string clientId, string clientSecret, string refreshToken, CancellationToken cancellationToken = default);
}

public record AuthResponse(long user_id, string access_token, string refresh_token, long expires_in);

In app.xaml.cs update the .UseHttp method as follows:

.UseHttp((context, services) => services
    // Register HttpClient
#if DEBUG
    // DelegatingHandler will be automatically injected into Refit Client
    .AddTransient<DelegatingHandler, DebugHttpHandler>()
#endif
    .AddRefitClient<IFigmaAuthClient>(context))

Registering Api Endpoint

The api endpoint is defined in the same way as the auth endpoint.

[Headers("Content-Type: application/json")]
public interface IFigmaApiClient
{
    [Get("/v1/me")]
    Task<FigmaUser> GetMe(CancellationToken cancellationToken = default);
}

public class FigmaUser
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("email")]
    public string Email { get; set; }

    [JsonPropertyName("handle")]
    public string Handle { get; set; }

    [JsonPropertyName("img_url")]
    public string ImageUrl { get; set; }
}

And the registration:

.UseHttp((context, services) => services
    // Register HttpClient
#if DEBUG
    // DelegatingHandler will be automatically injected into Refit Client
    .AddTransient<DelegatingHandler, DebugHttpHandler>()
#endif
    .AddRefitClient<IFigmaAuthClient>(context)
    .AddRefitClient<IFigmaApiClient>(context)
    )

Configuring Web Authentication

The bulk of the complexity for authenticating with Figma is handled within the web authentication configuration which requires defining three different callbacks on the builder exposed by the AddWeb extension method.

.UseAuthentication(auth =>
    auth.AddWeb(name: "WebAuthentication", configure: builder =>
    {
        // Define PrepareLoginStartUri, PostLogin and Refresh callbacks     
    })
)

First up is the PrepareLoginStartUri method, which is used to add ClientId and RedirectUri to the parameterized loginUri to return the uri that will be used to launch the web authentication process.

builder.PrepareLoginStartUri((IServiceProvider sp, ITokenCache cache, IDictionary<string, string>? credentials, string loginUri, CancellationToken ct) =>
{
    var appConfig = sp.GetRequiredService<IOptions<AppConfig>>().Value;
    return ValueTask.FromResult(string.Format(loginUri, appConfig.ClientId, appConfig.RedirectUri));
})

Next up, the PostLogin method, which converts the code returned by the web authentication process into access and refresh tokens.

.PostLogin(async (sp, cache, credentials, tokens, ct) =>
{
    var figma = sp.GetRequiredService<IFigmaAuthClient>();
    var appConfig = sp.GetRequiredService<IOptions<AppConfig>>().Value;
    var code = tokens[TokenCacheExtensions.AccessTokenKey];

    var authData = await figma.GetToken(appConfig.ClientId, appConfig.ClientSecret, appConfig.RedirectUri, code, ct);
    if (authData is null)
    {
        return tokens;
    }
    tokens[TokenCacheExtensions.AccessTokenKey] = authData.access_token;
    tokens[TokenCacheExtensions.RefreshTokenKey] = authData.refresh_token;
    tokens["ExpiresIn"] = authData.expires_in.ToString();
    tokens["UserId"] = authData.user_id.ToString();
    return tokens;
})

Lastly, the Refresh method, which uses the refresh token returned as part of the login process to create a new access token. This is called each time the application is launched, or when an api call fails with an authorization error, to get a new access token. If the call to Refresh method fails, the application will be automatically logged out, requiring the user to complete the web authentication process again.

.Refresh(async (sp, cache, tokens, ct) =>
{
    try
    {
        var figma = sp.GetRequiredService<IFigmaAuthClient>();
        var appConfig = sp.GetRequiredService<IOptions<AppConfig>>().Value;
        var refreshToken = tokens[TokenCacheExtensions.RefreshTokenKey];

        var authData = await figma.GetRefreshToken(appConfig.ClientId, appConfig.ClientSecret, refreshToken, ct);

        if (authData is null)
        {
            return tokens;
        }
        tokens[TokenCacheExtensions.AccessTokenKey] = authData.access_token;
        tokens["ExpiresIn"] = authData.expires_in.ToString();
        return tokens;
    }
    catch
    {
        return tokens;
    }
});

Callback Protocol

In order for authentication to work, you need to make sure you update the myprotocol protocol setup when the application was created using the Uno Platform template with the protocol used in the application defined with Figma. In the case of this application we’ll replace myprotocol with btrfigma (You can simply do a find and replace all in Visual Studio to make sure you update this value for all platforms).

Application Code

Now that we have the application configured, we can make use of api endpoint within our application. The MainViewModel has been updated as follows:

public partial class MainViewModel : ObservableObject
{
    private IAuthenticationService _authentication;

    private IFigmaApiClient _figma;

    [ObservableProperty]
    private FigmaUser? _user;

    public MainViewModel(
        IAuthenticationService authentication,
        IDispatcher dispatcher,
        INavigator navigator,
        IFigmaApiClient figma)
    {
        _authentication = authentication;
        _figma = figma;
        _ = LoadUserProfile(dispatcher);
    }

    private ValueTask LoadUserProfile(IDispatcher dispatcher)
        => dispatcher.ExecuteAsync(async ()=> User = await _figma.GetMe());


    [RelayCommand]
    private async Task DoLogout(CancellationToken token)
    {
        await _authentication.LogoutAsync(token);
    }
}

And the XAML for MainPage.

<Page x:Class="FigmaWebSampleApp.Presentation.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:FigmaWebSampleApp.Presentation"
      xmlns:uen="using:Uno.Extensions.Navigation.UI"
      xmlns:utu="using:Uno.Toolkit.UI"
      xmlns:um="using:Uno.Material"
      NavigationCacheMode="Required"
      Background="{ThemeResource BackgroundBrush}">

  <Grid utu:SafeArea.Insets="VisibleBounds">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <utu:NavigationBar Content="{Binding Title}" />

    <StackPanel Grid.Row="1"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Spacing="16">
      <Image Height="100"
             Width="100"
             Source="{Binding User.ImageUrl}" />
      <TextBlock Text="{Binding User.Id}" />
      <TextBlock Text="{Binding User.Email}" />
      <Button Content="Logout"
              Command="{Binding LogoutCommand}" />
    </StackPanel>
  </Grid>
</Page>

Summary

Now we have all the pieces setup to run the application, login to Figma and return information about the current Figma user in the application.

Whilst it appears there’s a few steps to setup to get this to work, this is significantly simpler that having to create a web authentication flow from scratch and then having to interact with HttpClient to do the code to token hand off and the subsequent refreshing of tokens. Note that what’s abstracted here is that the access token is automatically passed as part of the calls to the api endpoint, with no extra code required!

1 thought on “Using Web Authentication (OAuth2) to access Figma from Windows and Uno Platform Application”

Leave a comment