2022-08-11

Azure AD SSO in ASP.NET (使用OWIN)

大多公司是採用微軟企業方案,可以由Azure Portal管理Office、Outlook、群組(Group)等權限和功能。維護舊專案(.NET Framework 4.7.2)時,想加上SSO(Single sign-on)在網頁上,可是網路上大多是寫給.NET Core的教學,雖然官方也有提供.NET Framework的教學,但新增專案時選擇Azure驗證可以得到比較漂亮的寫法,故特此筆記。

一、新增OWIN中介軟體Nuget套件

Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Host.SystemWeb

二、新增Startup.cs

與ASP.NET Core類似,在Startup.cs設定中介軟體的操作,在專案新增「OWIN啟動類別」,命名為"Startup.cs"。


然後,編輯Startup類別。

Startup.cs:

using System;
using Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;

[assembly: OwinStartup(typeof(DemoProject.Startup))

namespace DemoProject
{
    public partial class Startup
    {
        private static string clientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientId"];
        private static string aadInstance = EnsureTrailingSlash(System.Configuration.ConfigurationManager.AppSettings["ida:AADInstance"]);
        private static string tenantId = System.Configuration.ConfigurationManager.AppSettings["ida:TenantId"];
        private static string postLogoutRedirectUri = System.Configuration.ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
        private static string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, aadInstance, tenantId);
    
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    
        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
        
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = authority,
                    PostLogoutRedirectUri = postLogoutRedirectUri
                });
        }
    
        private static string EnsureTrailingSlash(string value)
        {
            if (value == null)
            {
                value = string.Empty;
            }
        
            if (!value.EndsWith("/", StringComparison.Ordinal))
            {
                return value + "/";
            }
        
            return value;
        }
    }

}

三、Azure AD | App registrations

從程式碼可以看出來我們需要4個參數,他們都是從Azure AD取得的,所以再來我們會先到Azure Portal註冊新的程式,然後在『Redirect URIs』裡增加Web的Redirect URI,也就是你的網址。這邊我用https://localhost:44388示範。


記得要勾選"ID tokens"。


四、Web.config設定

在Azure AD設定好之後,回到專案,把相對應參數寫入appSettings裡。

Web.config:

<appSettings>
    ...
    <add key="ida:ClientId" value="Enter_the_Application(client)_ID_here" />
    <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}/" />
    <add key="ida:TenantId" value="Enter_the_Directory(tenant)_ID_here" />
    <add key="ida:PostLogoutRedirectUri" value="https://localhost:44388/" />
</appSettings>

五、Authorize Attribute

有了這些設定我們就已經可以在要管制的Controller、Action上加上[Authorize] attribute,這樣就會強制你要登入了,很神奇吧XD

[Authorize]
public class HomeController : BaseController
{
  // Code here.
}

偵錯F5看看,可以看到網頁會直接先導頁到Azure Login的頁面~


六、登入、登出功能

當然,有了SSO,不可能不讓用戶登出吧,所以我們會增加AccountController來處理這些事。

AccountController.cs:

using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;

namespace DemoProject.Controllers
{
    public class AccountController : Controller
    {
        public void SignIn()
        {
            if (!Request.IsAuthenticated)
            {
                HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
                    OpenIdConnectAuthenticationDefaults.AuthenticationType);
            }
        }

        public void SignOut()
        {
            string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);

            HttpContext.GetOwinContext().Authentication.SignOut(
                new AuthenticationProperties { RedirectUri = callbackUrl },
                    OpenIdConnectAuthenticationDefaults.AuthenticationType, 
                    CookieAuthenticationDefaults.AuthenticationType);
        }

        public ActionResult SignOutCallback()
        {
            if (Request.IsAuthenticated)
            {
                return RedirectToAction("Index", "Home");
            }

            return View();
        }
    }

}

可以看到SignIn()就是拿OpenIdConnectAuthenticationDefaults.AuthenticationType=OpenIdConnect來Challenge執行驗證登入,而我們是只用[Authorize] attribute就搞定這一連串動作。
SignOut()其實只是把Challenge改成SignOut,但這邊我們增加一個方法SignOutCallback(),讓我們執行完登出的時候呼叫他,來跳轉(Redirect)到登出頁。
在_Layout這邊我們可以加入_LoginPartial。

_LoginPartial:

@if (Request.IsAuthenticated)
{
    <text>
        <ul class="nav navbar-nav navbar-right">
            <li class="navbar-text">
                Hello, @User.Identity.Name!
            </li>
            <li>
                @Html.ActionLink("Sign out", "SignOut", "Account")
            </li>
        </ul>
    </text>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Sign in", "SignIn", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

畫面會長這樣~



總結

從微軟預設的驗證程式可以學到蠻多思維的,在嫁接這些寫法的時候也採了很多坑,尤其是Azure Redirect URIs那邊。再來應該會再玩玩看.NET 6,還有就是Group Policy的實作,針對不同API或網頁會需要不同的權限,應該會記錄在下集ㄎㄎ。

沒有留言:

張貼留言