💡

OAuth 2.0でTwitter APIを操作するデスクトップアプリ

2024/11/24に公開

0. 概要

この記事では、C#とOAuth 2.0を使ってTwitter APIを操作するWindows専用のツイートアプリを作成する方法を解説します。
WPFアプリケーションを使用し、認証フローを実装してアクセストークンを取得する仕組みを学びます。

1. 開発環境のセットアップ

必要な環境

  • ツール
    • Visual Studio 2022
    • .NET 6.0 (LTS)
  • アカウント
    • Twitter Developer Account を取得して、以下をメモ
      • API Key
      • API Key Secret
      • Bearer Token
      • Access Token
      • Client ID
      • Client Secret
    • CallbackURL を入れる欄には "http://127.0.0.1:3000/callback" を入力

アカウントの設定

Twitter Developer Accountを作って、以下の設定を行う

デフォルトが Read Only の部分を一度 Revoke して Read and Write に変更

2. アプリケーションの構造

プロジェクトの作成

Visual Studioを起動
「新しいプロジェクト」を選択

WPFアプリケーションを開く

.NET 6.0 (長期的なサポート)を選択

UIの構築 (MainWindow.xaml)

MainWindow.xaml

<Window x:Class="TwitterApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TwitterApp" Height="400" Width="400">
    <Grid Margin="10">
        <StackPanel>
            <TextBlock Text="認証コードを入力してください:" Margin="0,10" />
            <TextBox Name="CodeInputBox" Width="300" />
            <Button Content="アクセストークンを取得" Width="200" Margin="0,10" Click="OnGetAccessTokenClick" />
            <TextBlock Text="ツイート内容を入力してください:" Margin="0,20" />
            <TextBox Name="TweetInputBox" Width="300" />
            <Button Content="ツイートを投稿" Width="200" Margin="0,10" Click="OnPostTweetClick" />
        </StackPanel>
    </Grid>
</Window>

認証クライアントの実装 (TwitterAuthClient)

MainWindow.xaml.cs

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Text;

public class TwitterAuthClient
{
    private readonly HttpClient _httpClient;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string _callbackUrl;

    public TwitterAuthClient(string clientId, string clientSecret, string callbackUrl)
    {
        _clientId = clientId;
        _clientSecret = clientSecret;
        _callbackUrl = callbackUrl;
        _httpClient = new HttpClient();
    }

    public (string Url, string CodeVerifier) GenerateAuthUrl()
    {
        // PKCEのcode_verifierを生成
        string codeVerifier = GenerateCodeVerifier();
        string codeChallenge = GenerateCodeChallenge(codeVerifier);
        string state = Guid.NewGuid().ToString();

        var url = "https://twitter.com/i/oauth2/authorize" +
                 $"?response_type=code" +
                 $"&client_id={Uri.EscapeDataString(_clientId)}" +
                 $"&redirect_uri={Uri.EscapeDataString(_callbackUrl)}" +
                 $"&scope={Uri.EscapeDataString("tweet.read tweet.write users.read offline.access")}" +
                 $"&state={Uri.EscapeDataString(state)}" +
                 $"&code_challenge={Uri.EscapeDataString(codeChallenge)}" +
                 $"&code_challenge_method=S256";

        return (url, codeVerifier);
    }

    public async Task<string> GetAccessTokenAsync(string code, string codeVerifier)
    {
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("client_id", _clientId),
            new KeyValuePair<string, string>("client_secret", _clientSecret),
            new KeyValuePair<string, string>("code", code),
            new KeyValuePair<string, string>("grant_type", "authorization_code"),
            new KeyValuePair<string, string>("redirect_uri", _callbackUrl),
            new KeyValuePair<string, string>("code_verifier", codeVerifier)
        });

        var response = await _httpClient.PostAsync("https://api.twitter.com/2/oauth2/token", content);
        var responseBody = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Failed to get access token. Status: {response.StatusCode}, Response: {responseBody}");
        }

        var json = JsonSerializer.Deserialize<JsonElement>(responseBody);
        return json.GetProperty("access_token").GetString();
    }

    private string GenerateCodeVerifier()
    {
        var bytes = new byte[32];
        using (var rng = new RNGCryptoServiceProvider())
        {
            rng.GetBytes(bytes);
        }
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    private string GenerateCodeChallenge(string codeVerifier)
    {
        using (var sha256 = SHA256.Create())
        {
            var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
            return Convert.ToBase64String(challengeBytes)
                .TrimEnd('=')
                .Replace('+', '-')
                .Replace('/', '_');
        }
    }
}

3. 認証フローの実装

Step 1: 認証URLの生成と認証

これだけでは動かなくて、x.com に許可を得て認証を行う必要がある。

Program.cs

using System;
using System.Diagnostics;

class Program
{
    private const string ClientId = "ここにあなたのClientID";
    private const string CallbackUrl = "http://127.0.0.1:3000/callback";

    static void Main(string[] args)
    {
        // 認証URLを生成
        string authUrl = GenerateAuthUrl();

        Console.WriteLine("以下のURLをブラウザで開いて、認証を完了してください:");
        Console.WriteLine(authUrl);

        // 自動的にブラウザで開く
        OpenUrlInBrowser(authUrl);

        // 認証コードを入力
        Console.WriteLine("認証が完了したら、リダイレクトURLの「code」パラメータをコピーして貼り付けてください:");
        string code = Console.ReadLine();

        Console.WriteLine($"入力された認証コード: {code}");
        Console.WriteLine("このコードを使ってアクセストークンを取得してください。");

        // 終了待ち
        Console.WriteLine("終了するには何かキーを押してください...");
        Console.ReadKey();
    }

    // 認証URLを生成
    private static string GenerateAuthUrl()
    {
        string state = Guid.NewGuid().ToString();
        return $"https://twitter.com/i/oauth2/authorize" +
               $"?response_type=code" +
               $"&client_id={ClientId}" +
               $"&redirect_uri={Uri.EscapeDataString(CallbackUrl)}" +
               $"&scope={Uri.EscapeDataString("tweet.read tweet.write users.read offline.access")}" +
               $"&state={state}" +
               $"&code_challenge=challenge" +
               $"&code_challenge_method=plain";
    }

    // ブラウザでURLを開く
    private static void OpenUrlInBrowser(string url)
    {
        try
        {
            // Windowsの標準ブラウザで開く
            Process.Start(new ProcessStartInfo
            {
                FileName = url,
                UseShellExecute = true
            });
        }
        catch (Exception ex)
        {
            Console.WriteLine($"ブラウザを開く際にエラーが発生しました: {ex.Message}");
        }
    }
}

上記のコードを、任意の場所でコンソールを開いて、

dotnet new console

で生まれた Program.cs に貼り付けて

dotnet run

を実行する。
すると x.com の許可を求めるサイトが開く

Step 2: アクセストークンの取得

許可を押すと、接続が切れるものの、URL が
http://127.0.0.1:3000/callback?state=twiddle&code=twaddle

となるので、twaddle をコピー

認証コードの使用

  • 30秒以内の制限について
    アプリに貼り付けて「アクセストークンを取得」しないと、
    認証に失敗する!


うまく認証できていればツイートできる!

まとめ

リフレッシュトークンを使うことで、制限を回避できるらしい
次はそれにチャレンジ

Discussion