Note: see the link below for the English version of this article.

https://duongnt.com/integrate-sso-asp-net-core

Đây là bài số 2 trong loạt 3 bài về xác thực một lần (SSO) sử dụng SAML với OKTA. Đường link tới những phần còn lại nằm tại đây

Chúng ta sẽ dùng app đã đăng ký trong phần một để thực hiện xác thực người dùng trước khi cho phép họ truy cập vào tài nguyên trong ứng dụng web viết bằng ASP.NET Core. Hai thông tin quan trọng nhất mà ta cần quan tâm tới là Identity Provider IssuerIdentity Provider Metadata URL.

Các bạn có thể download code trong bài này từ link dưới đây.

https://github.com/duongntbk/SamlSample

Tổng quan về quá trình xác thực

Ta sẽ đăng ký module xác thực làm một service mới trong ứng dụng. Quá trình xác thực sử dụng cookie và bao gồm các bước dưới đây.

  • Người dùng truy cập vào ứng dụng.
  • Ứng dụng kiểm tra cookie, nếu thấy người dùng chưa đăng nhập thì người dùng sẽ được chuyển tiếp tới trang đăng nhập. Trong ví dụ của ta, trang đăng nhập của ứng dụng là /account/login.
  • Trang đăng nhập của ứng dụng tạo một request và đính kèm request đó khi chuyển tiếp người dùng tới trang đăng nhập của OKTA. Người dùng đăng nhập vào tài khoản OKTA.
  • OKTA kiểm tra thông tin đăng nhập của người dùng. Nếu tài khoản/mật mã sai, hoặc nếu người dùng không được cấp quyền với app tương ứng trên OKTA, OKTA sẽ hiển thị thông báo lỗi.
  • Nếu người dùng đăng nhập thành công vào OKTA, OKTA sẽ tạo một response và đính kèm response đó khi chuyển tiếp người dùng về lại trang đăng nhập của ứng dụng.
  • Trang đăng nhập của ứng dụng xác nhận response đúng là từ OKTA và chưa bị hết hạn.
  • Nếu tất cả các xác nhận đều thành công thì cookie của người dùng sẽ được cập nhận với trạng thái đã đăng nhập. Sau đó người dùng được chuyển tiếp về lại trang họ định truy cập.
  • Bây giờ người dùng có thể tùy ý sử dụng ứng dụng.

Tích hợp OKTA vào một ứng dụng web có sẵn

Vì bài toán bảo mật là rất phức tạp và mọi sai lầm đều gây hậu quả lớn nên chúng ta không tự mình viết module đăng nhập. Thay vào đó ta sẽ sử dụng một thư viện có tên là Sustainsys. Chạy câu lệnh sau để cài Sustainsys.

dotnet add package Sustainsys.Saml2 --version 2.8.0

Ta thay đổi hàm ConfigureServices trong file Startup.cs để thêm module thực hiện xác thực thành vào danh sách service của ứng dụng.

services
    .AddAuthentication("OktaSaml")
    .AddSamlAuthentication(Configuration.Saml);

Sau đó ta thêm các middleware bảo mật sau đây vào ứng dụng.

app
    .UseAuthentication()
    .UseAuthorization();

Ta đặt tên service xác thực là OktaSaml. Service này được thiết lập bằng cách gọi một hàm extension, hàm extension đó sẽ lại gọi một hàm extension khác để chuyền vào các thông tin thiết lập. Ta sẽ lần lượt xem từng hàm extension đó trong phần dưới. Lưu ý là ứng dụng của ta nằm ở địa chỉ https://localhost:5001.

Lớp AuthenticationExtensions

public static AuthenticationBuilder AddSamlAuthentication(this AuthenticationBuilder auth,
    Action<Saml2Options> configureOptions) =>
    auth
        .AddCookie("OktaSaml", options => {
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.SameSite = SameSiteMode.None;
            options.Cookie.Path = "/";
        })
        .AddSaml2(configureOptions);

Hàm này thiết lập các tùy chọn cho việc sử dụng cookie trong quá trình xác thực. Tất cả các tùy chọn này được đăng ký với service OktaSaml.

Theo mặc định, trang xác thực của ứng dụng nằm tại địa chỉ account/login. Tuy nhiên ta có thể thay đổi đường dẫn đó bằng cách thêm vào câu lệnh dưới đây.

options.LoginPath = "<trang xác thực mới>"

Lớp ConfigurationExtensions

public static void Saml(this IConfiguration config, Saml2Options options)
{
    options.SPOptions.PublicOrigin = "https://localhost:5001/";
    options.SPOptions.EntityId = "https://localhost:5001/Saml2";
    options.SPOptions.ReturnUrl = new Uri("/", UriKind.Relative);

    var idp = new IdentityProvider(new EntityId("<Identity Provider Issuer from OKTA>"), options.SPOptions)
    {
        MetadataLocation = "<Identity Provider Metadata URL from OKTA>",
        AllowUnsolicitedAuthnResponse = true
    };

    options.IdentityProviders.Add(idp);
}

Ý nghĩa của từng dòng trong hàm Saml như sau.

options.SPOptions.PublicOrigin = "https://localhost:5001/";

Giá trị trong PublicOrigin sẽ được sử dụng làm root của đường link tới trang xử lý response từ OKTA trong ứng dụng của ta. Trang này được sinh tự động bởi Sustainsys và nằm ở địa chỉ /Saml2/Acs. Với PublicOrigin như trên, đường link đầy đủ tới trang đó sẽ là https://localhost:5001/Saml2/Acs. Ta có thể đặt giá trị này giống với root của ứng dụng.

options.SPOptions.EntityId = "https://localhost:5001/Saml2";

Ứng dụng của ta sẽ dùng giá trị này làm Id của mình trong request gửi tới OKTA. Id này phải trùng với giá trị của trường Audience URI (SP Entity ID) mà ta đã đặt trước đó trên OKTA.

options.SPOptions.ReturnUrl = new Uri("/", UriKind.Relative);

Nếu trong request gửi tới OKTA ta không yêu cầu chuyển tiếp người dùng về trang nào sau khi họ đăng nhập xong thì người dùng sẽ được chuyển tiếp tới ReturnUrl.

var idp = new IdentityProvider(new EntityId("<Identity Provider Issuer from OKTA>"), options.SPOptions)
{
    MetadataLocation = "<Identity Provider Metadata URL from OKTA>",
    AllowUnsolicitedAuthnResponse = true
};

Những giá trị ở đây quyết định app nào của OKTA sẽ thực hiện xác thực cho người dùng của ứng dụng của ta. Nếu AllowUnsolicitedAuthnResponse được đặt là true thì OKTA được phép gửi response tới ứng dụng kể cả khi ứng dụng không chủ động gửi request trước. Tùy chọn này cho phép người dùng đăng nhập vào ứng dụng của ta trực tiếp từ OKTA.

Trang xác thực của ứng dụng

Khác với các ứng dụng web bình thường, trang xác thực của ta không kiểm tra tài khoản/mật mã của người dùng. Nó chỉ gửi request tới OKTA rồi đợi response.

// route: /account/login
public IActionResult OnGetLogin(string returnUrl)
{
    var redirectUri = Url.Page("account", "callback", new { returnUrl });
    return new ChallengeResult("Saml2",
        new AuthenticationProperties() { IsPersistent = true, RedirectUri = redirectUri });
}

// route: /account/callback
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl)
{
    var auth = await HttpContext.AuthenticateAsync();

    if (!auth.Succeeded)
    {
        return Unauthorized();
    }

    await HttpContext.SignInAsync(auth.Principal, auth.Properties);

    return LocalRedirect(returnUrl);
}

Có lẽ nhiều người sẽ nghĩ rằng OKTA sẽ gửi response trực tiếp tới /account/callback. Nhưng thực ra response sẽ được gửi tới địa chỉ /Saml2/Ascmà ta đã nhắc đến lúc trước. Sau khi /Saml2/Acs đã xác nhận response đúng là của OKTA và chưa bị quá hạn thì nó mới chuyển response tới /account/callback. Đây là lý do vì sao ta phải đặt giá trị của Single sign on URL trên OKTA là https://localhost:5001/Saml2/Acs ở trong phần một.

Chạy thử

Đầu tiên, ta cần nhập thông tin của OKTA server vào file appsettings.json của ứng dụng.

"Authentication": {
    "Saml": {
      "EntityId": "https://localhost:5001/Saml2",
      "IdentityProviderIssuer": "<entity id của identity provider>",
      "MetadataUrl": "<link tới file metadata của identity provider>"
    }
}

Ví dụ của ta có 3 trang là Home, PublicSecret. Ta sẽ yêu cầu người dùng phải đăng nhập khi truy cập vào Secret. Để làm điều này ta chỉ cần gắn attribute sau vào trang Secret.

[Authorize(AuthenticationSchemes = "OktaSaml")]

Người dùng có thể truy cập vào HomePublic mà không cần đăng nhập.

Nhưng khi thử truy cập Secret, họ sẽ được chuyển tiếp tới trang đăng nhập của OKTA.

Nếu họ nhập sai tài khoản/mật mã, OKTA sẽ hiện thông báo lỗi.

Nếu đăng nhập thành công nhưng tài khoản của họ chưa được cấp quyền truy cập ứng dụng thì OKTA vẫn sẽ hiện thông báo lỗi.

Nếu người dùng đăng nhập thành công vào một tài khoản có quyền truy cập ứng dụng thì họ sẽ xem được trang Secret.

Như đã nhắc đến ở phần trước, người dùng có thể đăng nhập vào ứng dụng ngay từ OKTA. Từ trang chủ của OKTA, người dùng có thể thấy được danh sách các app họ được phép truy cập. Sau khi nhấn vào một app, OKTA sẽ gửi response tới ứng dụng của ta. Nếu như tùy chọn AllowUnsolicitedAuthnResponse trong ứng dụng có giá trị true thì người dùng sẽ được phép đăng nhập.

Kết thúc

Ta đã tích hợp được SSO sử dụng OKTA vào ứng dụng của mình. Tuy nhiên đôi khi ta cần test ứng dụng một cách độc lập, không phụ thuộc vào các service khác. Trong trường đó ta phải làm sao để tạm thời tách ứng dụng của mình khỏi OKTA? Câu trả lời sẽ có trong phần ba.

A software developer from Vietnam and is currently living in Japan.

3 Thoughts on “Tích hợp SSO với OKTA vào ứng dụng web ASP.NET Core”

Leave a Reply