Service Fabric: ปกป้องข้อมูล ASP.NET Core WebApi ด้วย IdentityServer4

ไทย/Eng

OAuth2 คือ authorization framework ที่ช่วยให้เราสามารถควบคุมการเข้าถึงข้อมูลของผู้ใช้ได้ โดยผู้ใช้จะใช้ credential ที่ตัวเองมีในการยืนยันตัวตน และจะได้ access token กลับมาในการเรียก API อื่นๆ เพื่อขอข้อมูล รูปด้านล่างเป็นขั้นตอนการทำงานคร่าวๆของ OAuth2 ได้มาจาก Harsha Kumara

IdentityServer4 คือ middleware ที่สามารถเพิ่ม OpenId and OAuth2 เข้าไปใน ASP.NET Core Application ในบทความก่อนหน้านี้พูดถึงการทำ secured cluster ซึ่งปกป้อง cluster ของเราจากผู้ใช้นิรนาม (anonymous user) แต่การทำ OpenAPI ที่ใครก็ตามที่มี url สามารถเข้าถึงได้โดยง่าย จึงต้องมีการจำกัดสิทธิ์ของผู้ใช้เหล่านี้

สิ่งที่ต้องเตรียม

  1. Azure Portal Subscription Account – If you don’t have one. Try it for free
  2. Visual Studio – This article used VS2017 Preview 2 with .NET Version 4.6.1
  3. Service Fabric SDK – See How
  4. Secure Cluster – See how
  5. ASP.NET Core WebApi Setup – See how

โค้ดฉบับเต็มดูได้ที่ GitHub แต่มันอยู่ที่ branch “IdentityServer4” คุณต้องทำการ checkout ไปที่ branch นั้นก่อน

เริ่มกันเลย

    1. บทความนี้ ต่อทำต่อการ โพสต์นี้
    2. ในโพสต์นี้ ต้องติดตั้ง NuGet 3 แพคเกจ ได้แก่ “IdentityServer4”, “IdentityServer4AccessTokenValidation”, “WindowAzure.Storage”


    1. สร้างไฟล์ใหม่ชื่อว่า “Config.cs”. ไฟล์นี้จะเก็บ ApiResource สำหรับ IdentityServer4.
public static IEnumerable GetApiResources()
{
    return new List
    {
        new ApiResource("api1", "My API")
    };
}
    1. สร้างไฟล์ใหม่ชื่อว่า “StorageClient.cs”. ไฟล์นี้จะเป็นคลาสสำหรับกาดึงข้อมูล Client จาก AzureStorage โดยในโพสต์นี้ ได้ใช้ local storage ดังนั้นจึงจำเป็นต้องรัน Azure Storage Emulator หรือไม่อย่างนั้นก็ใช้ azure storage จาก server
public class StorageClient : IDisposable
{
    private string ConnectionString => "UseDevelopmentStorage=true";
    private string PrefixTable => "IDx";

    public CloudTable ClientStore { get { ThrowIfDisposed(); return _ClientStore; } set { _ClientStore = value; } }
    private CloudTable _ClientStore;

    private CloudTableClient CloudStorageClient = null;

    public StorageClient()
    {
        CloudStorageClient = CloudStorageAccount.Parse(ConnectionString).CreateCloudTableClient();
        _ClientStore = CloudStorageClient.GetTableReference(PrefixTable + "ClientStore");
        _ClientStore.CreateIfNotExistsAsync();
    }
    private bool _disposed = false;
    ~StorageClient()
    {
        this.Dispose(false);
    }
    private void ThrowIfDisposed()
    {
        if (this._disposed)
        {
            throw new ObjectDisposedException(base.GetType().Name);
        }
    }
    public void Dispose()
    {
        this.Dispose(true);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
        {
            CloudStorageClient = null;
            _ClientStore = null;
            _disposed = true;
        }
    }
}
    1. สร้างไฟล์ใหม่ชื่อว่า “ClientEntity.cs” ไฟล์นี้จะเป็นโมเดลของข้อมูลที่มาจากฐานข้อมูล
public class ClientEntity : TableEntity
{
    // PK = "ClientData"
    // RK = {ClientId}

    public string Secret { get; set; }
    public string Scope { get; set; }
}

    1. สร้างไฟล์ใหม่ชื่อว่า “ClientStore.cs”. ไฟล์นี้จะทำหน้าที่ จัดการ ClientId และ ClientSecret สำหรับ IdentityServer4.
public StorageClient Db { get; set; }
public ClientStore()
{
    Db = new StorageClient();
}
public async Task FindClientByIdAsync(string clientId)
{
    TableResult RetrieveResult = await Db.ClientStore.ExecuteAsync(TableOperation.Retrieve("ClientData", clientId));
    var Entity = (ClientEntity)RetrieveResult.Result;
    return new Client()
    {
        ClientId = clientId,
        AllowedGrantTypes = GrantTypes.ClientCredentials,

        // secret for authentication
        ClientSecrets =
        {
            new Secret(Entity.Secret.Sha256())
        },

        // scopes that client has access to
        AllowedScopes = { Entity.Scope }
    };
}
    1. เปิดไฟล์ “Startup.cs” ในเมธอด “ConfigureServices” เพิ่มโค้ดเหล่านี้
services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddClientStore();

services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:8994";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "api1";
                });
    1. ในเมธอด “Configure” เพิ่มโค้ดดังนี้
app.UseIdentityServer();
    1. เพิ่มข้อมูล ClientId และ ClientSecret ดังนี้

    1. สร้าง ApiController ชื่อ “PersonController.cs”
[Produces("application/json")]
[Route("api/Person")]
[Authorize]
public class PersonController : Controller
{
    [HttpGet]
    public List GetPersons()
    {
        return new List()
        {
            new Person()
            {
                Firstname = "Jakkrit",
                Lastname = "Junrat",
                Age = 27
            },
            new Person()
            {
                Firstname = "Apinya",
                Lastname = "Pimpisan",
                Age = 27
            }
        };
    }
}

public class Person
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public int Age { get; set; }
}
    1. Deploy โปรเจ็คแล้วไปที่ “http://localhost:8994/api/Person”. คุณจะได้รับ 401 (Unauthorized) นั่นหมายความว่า API ของคุณได้รับการปกป้องแล้ว

    1. คราวนี้ มาสร้าง Client กัน
    2. เริ่มแรก สร้าง ConsoleApp โปรเจ็ค และติดตั้ง NuGet แพคเกจชื่อ “IdentityModel”.

    1. ในเมธอด “Main” เพิ่มโค้ดต่อไปนี้
// discover endpoints from metadata
var disco = DiscoveryClient.GetAsync("http://localhost:8994").GetAwaiter().GetResult();
if (disco.IsError)
{
    Console.WriteLine(disco.Error);
    return;
}

// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ClientId", "ClientSecret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("api1").GetAwaiter().GetResult();

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return;
}

Console.WriteLine(tokenResponse.Json);

// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);

var response = client.GetAsync("http://localhost:8994/api/Person/").GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    Console.WriteLine(JArray.Parse(content));
}
    1. รันโปรเจ็ค แล้วฟลลัพธ์ที่ได้ จะเป็นดังนี้

ขอบคุณที่ติดตาม แล้วพบกันใหม่
Happy Coding.

Leave a Reply

Your email address will not be published. Required fields are marked *