IdentityServer4におけるリソースオーナーパスワード資格情報フローの実装

OAuth 2.0およびOpenID ConnectのフレームワークであるIdentityServer4を活用したアプリケーションにおいて、リソースオーナーパスワード資格情報(Resource Owner Password Credentials, ROPC)フローの具体的な実装方法について解説します。

リソースオーナーパスワード資格情報(ROPC)フローとは

ROPCフローは、リソースオーナー(通常はユーザー)のユーザー名とパスワードを直接利用してアクセストークンを取得する認証方式です。その特性上、クライアントアプリケーションとリソースオーナー間に極めて高い信頼関係が要求されます。主にレガシーシステムとの統合や、他の認可フローが適用困難な場合に限定的に使用が推奨されることに留意してください。

  • ユーザー名とパスワードを直接送信してアクセストークンを要求します。
  • 高信頼のクライアントとリソースオーナー間でのみ適用可能です。
  • 通常、他の認可フローが利用できない場合の最終手段として考慮されます。

WPFクライアントアプリケーションの構築

IdentityServerからトークンを取得し、保護されたリソースにアクセスするためのWPFクライアントを構築します。

MainWindow.xaml(UI定義)

ユーザー名とパスワードの入力フィールド、アクセストークンおよびリソース応答を表示するためのテキストボックス、そして各種操作ボタンを配置します。

<Window x:Class="WpfClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfClient"
        mc:Ignorable="d"
        Title="IdentityServer4 ROPC クライアント" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="70" />
            <RowDefinition Height="40" />
            <RowDefinition />
            <RowDefinition Height="40" />
            <RowDefinition />
            <RowDefinition Height="40" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Margin="20" Orientation="Horizontal">
            <Label Content="ユーザー名:" />
            <TextBox x:Name="UsernameInputBox" Margin="20 0" Width="150" Height="25" Text="alice" />
            <Label Content="パスワード:" />
            <PasswordBox x:Name="PasswordInputBox" Margin="20 0" Width="150" Height="25" Password="alice" />
        </StackPanel>
        <Button Grid.Row="1" Click="GetAccessToken_Click"> 1. アクセストークンを取得 </Button>
        <TextBox Grid.Row="2" 
                 x:Name="AccessTokenDisplay"
                 VerticalAlignment="Stretch"
                 HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 AcceptsReturn="True"
                 TextWrapping="Wrap">
        </TextBox>
        <Button Grid.Row="3" Click="CallApiResource_Click">2. 保護されたAPIリソースを呼び出す</Button>
        <TextBox Grid.Row="4"
                 x:Name="ApiResourceResponseDisplay"
                 VerticalAlignment="Stretch"
                 HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 AcceptsReturn="True"
                 TextWrapping="Wrap">
        </TextBox>
        <Button Grid.Row="5" Click="CallIdentityResource_Click">3. IdentityServerのユーザー情報を取得</Button>
        <TextBox Grid.Row="6"
                 x:Name="IdentityResourceResponseDisplay"
                 VerticalAlignment="Stretch"
                 HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 AcceptsReturn="True"
                 TextWrapping="Wrap">
        </TextBox>
    </Grid>
</Window>

MainWindow.xaml.cs(コードビハインド)

IdentityModelライブラリを使用して、ディスカバリーエンドポイントから設定を取得し、ROPCフローでアクセストークンを要求します。取得したトークンを用いて、保護されたAPIリソースおよびIdentityServerのユーザー情報エンドポイントにアクセスします。

using IdentityModel.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;

namespace WpfClient
{
    public partial class MainWindow : Window
    {
        private string _bearerToken; // 取得したアクセストークン
        private DiscoveryDocumentResponse _discoveryDoc; // ディスカバリードキュメント情報

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void GetAccessToken_Click(object sender, RoutedEventArgs e)
        {
            var userIdentifier = UsernameInputBox.Text;
            var userSecret = PasswordInputBox.Password;

            // IdentityServerのディスカバリーエンドポイントから情報を取得
            var httpClient = new HttpClient();
            _discoveryDoc = await httpClient.GetDiscoveryDocumentAsync("https://localhost:5001"); // IdentityServerのURLに合わせてください
            if (_discoveryDoc.IsError)
            {
                MessageBox.Show($"ディスカバリーエラー: {_discoveryDoc.Error}");
                Console.WriteLine($"ディスカバリーエラー: {_discoveryDoc.Error}");
                return;
            }

            // ROPCフローでアクセストークンを要求
            var tokenResponse = await httpClient.RequestPasswordTokenAsync(new PasswordTokenRequest
            {
                Address = _discoveryDoc.TokenEndpoint,
                ClientId = "wpf.client.app", // IdentityServerに登録するクライアントID
                ClientSecret = "wpf_app_secret", // IdentityServerに登録するクライアントシークレット
                Scope = "scope1 openid profile email phone address", // 要求するスコープ

                UserName = userIdentifier,
                Password = userSecret
            });

            if (tokenResponse.IsError)
            {
                MessageBox.Show($"トークン取得エラー: {tokenResponse.Error}");
                Console.WriteLine($"トークン取得エラー: {tokenResponse.Error}");
                return;
            }

            _bearerToken = tokenResponse.AccessToken;
            AccessTokenDisplay.Text = tokenResponse.Json.ToString();
            MessageBox.Show("アクセストークンを取得しました!");
        }

        private async void CallApiResource_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(_bearerToken))
            {
                MessageBox.Show("まずアクセストークンを取得してください。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            // 保護されたAPIリソースを呼び出す
            var resourceClient = new HttpClient();
            resourceClient.SetBearerToken(_bearerToken);
            
            // APIリソースのURLに合わせてください
            var response = await resourceClient.GetAsync("https://localhost:6001/api/data"); 
            
            if (!response.IsSuccessStatusCode)
            {
                MessageBox.Show($"API呼び出しエラー: {response.StatusCode}");
                ApiResourceResponseDisplay.Text = $"エラー: {response.StatusCode}";
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                ApiResourceResponseDisplay.Text = content;
                MessageBox.Show("APIリソースにアクセスしました!");
            }
        }

        private async void CallIdentityResource_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(_bearerToken) || _discoveryDoc == null)
            {
                MessageBox.Show("まずアクセストークンを取得してください。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            // IdentityServerのUserInfoエンドポイントからユーザー情報を取得
            var resourceClient = new HttpClient();
            resourceClient.SetBearerToken(_bearerToken);
            
            var response = await resourceClient.GetAsync(_discoveryDoc.UserInfoEndpoint);         
            if (!response.IsSuccessStatusCode)
            {
                MessageBox.Show($"ユーザー情報取得エラー: {response.StatusCode}");
                IdentityResourceResponseDisplay.Text = $"エラー: {response.StatusCode}";
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                IdentityResourceResponseDisplay.Text = content;
                MessageBox.Show("IdentityServerからユーザー情報を取得しました!");
            }
        }
    }
}

IdentityServer4の構成

IdentityServerプロジェクト(通常はASP.NET Coreアプリケーション)で、ROPCフローをサポートするための設定を行います。

IdentityResourcesの定義

Config.csファイル内のGetIdentityResourcesメソッドで、クライアントが要求できるユーザー情報(クレーム)を定義します。これにより、クライアントはユーザーのopenid、profile、email、phone、addressなどの情報を取得できるようになります。

public static IEnumerable<IdentityResource> GetIdentityResources()
{
    return new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Email(),
        new IdentityResources.Phone(),
        new IdentityResources.Address()
    };
}

クライアントの定義

GetClientsメソッド内で、WPFクライアントの情報を登録します。重要なのはAllowedGrantTypes = GrantTypes.ResourceOwnerPasswordを指定することと、許可するスコープを定義することです。WPFクライアントで設定したClientIdとClientSecretと一致させる必要があります。

public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
        // ROPCフローをサポートするWPFクライアント
        new Client
        {
            ClientId = "wpf.client.app", // クライアントID
            ClientSecrets = { new Secret("wpf_app_secret".Sha256()) }, // クライアントシークレット

            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, // ROPCフローを許可

            // クライアントがアクセスできるスコープ
            AllowedScopes = { "scope1", "openid", "profile", "email", "phone", "address" }
        }
    };
}

テストユーザーの確認

Config.csファイル内のTestUsers.Usersコレクションに、テスト用のユーザー名とパスワードが登録されていることを確認してください。WPFクライアントで入力するユーザー名とパスワードは、ここで定義されたものと一致する必要があります。

アプリケーションの実行

IdentityServerプロジェクトを起動し、次にWPFクライアントアプリケーションを起動します。WPFクライアントのUIから、定義したユーザー名とパスワードを入力して、各ボタンをクリックすることで、ROPCフローによる認証とリソースアクセスの一連の流れをテストできます。

タグ: IdentityServer4 OAuth2 OpenIDConnect ResourceOwnerPasswordCredentials ROPC

10月4日 15:25 投稿