2016年7月31日

[Azure AD]用Azure AD B2C管理使用者登入

Azure AD B2C日前在北美正式可用(General Available)。透過Azure AD B2C,網站開發者可以透過Azure AD管理網站使用者的登入,允許使用者透過社群網路帳號(Facebook、Microsoft Account、Google Account…etc),或者是使用者自行輸入的帳號登入網站。在以往,要達到這樣的功能,除了我們必須自行設計維護使用者資料庫之外,為了透過社群帳號登入,開發人員也必須了解如何與社群驗證機制整合;現在透過Azure B2C,開發人員可以把這些繁瑣的動作交給Azure AD B2C,專注於開發商業應用。
目前(2016/7/30)Azure AD B2C還無法透過新的portal管理(https://portal.azure.com);想要透過Azure AD B2C管理使用者登入,首先我們必須回到https://manage.windowsazure.com建立一個新的AD B2C Tenant
  • 在Azure Portal上建立一個新的Azure B2C AD Tenant;建立一個B2C Tenant約需要兩分鐘左右的時間。
  • 要特別注意的是,目前為止(2016/7/30);Azure AD B2C只有在北美地區正式可用;如果你所選擇的國家/地區並非美國,則雖然你依然可以建立此Tenant,但會是一個Preview Tenant;我們並不建議你在Production環境上使用Preview Tenant

  • 建立完成後,切換到AD B2C Tenant的Configuration頁簽的管理B2C設定。

  • 這會打開Azure AD B2C在新portal (https://portal.azure.com)上的管理介面,將此Blade釘到Dashboard以方便管理。

  • 接著,我們要為網站在Azure AD中建立一個App;如果之前有使用過Azure AD管理應用程式登入,其實是一樣的概念,指示操作介面稍有不同。
  • 填上基本資料,這裡Redirect URL即是使用者驗證完畢後,Azure AD將Authentication Toekn送回的位址,通常是網站的首頁

  • 按下Generate Key後將Key抄下來,然後按下建立開始建立Azure AD B2C Tenant

  • 建立完成後,將Application ID抄下來

  • 接著,我們要為登入、註冊等使用者經驗建立原則;在這裡不需要為每一種使用者經驗建立原則,只需要挑我們需要的原則即可;在這裡,我只針對Sign-up or Sign-in建立原則。
  • 原則可以進行下列的設定來控制行為:
    • 可供使用的帳戶類型
    • 註冊時要向使用者收集的資訊(姓名、電話、地址…等等)
    • 是否使用Multi-Factor驗證
    • 頁面外觀與風格
    • 驗證完成後回傳給網站的Claim

  • 在這個範例中,我並不允許使用者透過社群帳號登入,而只允許使用者透過向Azure AD B2C註冊自己的email來登入我的網站。
    • 設定時請務必確定Email signup旁邊的Check Box有打勾。

  • 接著選擇登入畫面上需要的欄位

  • 選擇登入時是否需要啟用多因素驗證

  • 如果需要,可以在這裡針對各種登入頁面進行客製化

  • 當一切設定完成後,給予此原則名稱並按下建立。
    • Azure AD B2C會自動在名稱前面加上B2C_1_的前綴,例如我的原則名為SignInOrSignUp,在Azure AD B2C會將其命名為B2C_1_SignInOrSignUp

  • 重複上述步驟建立所有所需的原則

  • 到這裡,我們已經完成Azure AD B2C上所有的相關設定,接下來,我們要開始讓我站可以使用Azure AD B2C做使用者身分驗證與管理。
  • 首先建立一個空白的ASP.Net MVC網站,為了簡單,我的範例網站選擇無身分驗證、並且不host on cloud

  • 打開Nuget Manager Console

  • 依序執行以下指令安裝所需要的Nuget Package;其中黃色的部分版本務必要是1.0.2.206221351;否則執行時會因為網址錯誤導致無法登入要求無法導向Azure AD B2C
Install-Package Microsoft.Owin.Security.OpenIdConnect -Version 3.0.1
Install-Package Microsoft.Owin.Security.Cookies -Version 3.0.1
Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.1
Install-Package System.IdentityModel.Tokens.Jwt -Version 4.0.2
Install-Package Microsoft.IdentityModel.Protocol.Extensions -Version 1.0.2.206221351
  • 在Web.Config的AppSetting中,加入以下設定 <appSettings>
      ………
      <add key="ida:Tenant" value="michib2c.onmicrosoft.com" />
      <add key="ida:ClientId" value="{your Application ID}" />
      <add key="ida:AadInstance" value="https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}" />
      <add key="ida:RedirectUri" value="https://localhost:44316/" />
      <add key="ida:SusiPolicyId" value="B2C_1_SignInOrSignUp" />
    </appSettings>
      • 其中
        • ida:Tenant為我們的Azure AD B2C tenant的名稱
        • ida:ClientId為剛剛在Portal抄下來的Application ID
        • ida:RedirectUri為剛剛Portal上的Redirect URL
        • ida: SusiPolicyId為我所使用的SignInOrSignUp原則的名稱;如果我們需要多個原則,可以在這裡新增其他app setting設定。
  • 在專案中新增一個Startup.cs

  • 內容如下
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
namespace b2cdemo3
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

  • 在APP_Start中新增一個Startup.Auth.cs

  • 內容如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
// The following using statements were added for this sample
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Notifications;
using Microsoft.IdentityModel.Protocols;
using System.Web.Mvc;
using System.Configuration;
using System.IdentityModel.Tokens;

namespace b2cdemo3
{
    public partial class Startup
    {
        // App config settings
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
        private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
        // B2C policy identifiers
       public static string SusiPolicyId = ConfigurationManager.AppSettings["ida:SusiPolicyId"];
        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            // Configure OpenID Connect middleware for each policy
            app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(PasswordResetPolicyId));
            app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SusiPolicyId));
//app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy({AnotherPolicy…etc}));
        }
        // Used for avoiding yellow-screen-of-death TODO
        private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();
            if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
            {
                // If the user clicked the reset password link, redirect to the reset password route
                notification.Response.Redirect("/Account/ResetPassword");
            }
            else if (notification.Exception.Message == "access_denied")
            {
                // If the user canceled the sign in, redirect back to the home page
                notification.Response.Redirect("/");
            }
            else
            {
                notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
            }
            return Task.FromResult(0);
        }
        private Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            // If you wanted to keep some local state in the app (like a db of signed up users),
            // you could use this notification to create the user record if it does not already
            // exist.
            return Task.FromResult(0);
        }
        private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
        {
            return new OpenIdConnectAuthenticationOptions
            {
                // For each policy, give OWIN the policy-specific metadata address, and
                // set the authentication type to the id of the policy
                MetadataAddress = String.Format(aadInstance, tenant, policy),
                AuthenticationType = policy,
                // These are standard OpenID Connect parameters, with values pulled from web.config
                ClientId = clientId,
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = redirectUri,
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = AuthenticationFailed,
                    SecurityTokenValidated = OnSecurityTokenValidated,
                },
                Scope = "openid",
                ResponseType = "id_token",
                // This piece is optional - it is used for displaying the user's name in the navigation bar.
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                },
            };
        }
    }
}
    • 其中黃色標起來的地方,就是我們設定要使用的原則名稱,以及告訴Azure AD B2C需要套用哪些原則的地方,你可以依照需要增減。
  • 因為我們的專案一開始並沒有設定身分驗證機制,因此這裡我們需要加上一個登入的Controller – AccountController.cs
  • 命名為AccountController

  • 程式碼如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Owin.Security.Cookies;
using System.Security.Claims;
using System.Web.Http;
namespace b2cdemo3.Controllers
{
    public class AccountController : Controller
    {
        public void Login()
        {
            if (!Request.IsAuthenticated)
            {
                // To execute a policy, you simply need to trigger an OWIN challenge.
                // You can indicate which policy to use by specifying the policy id as the AuthenticationType
                HttpContext.GetOwinContext().Authentication.Challenge(
                    new AuthenticationProperties() { RedirectUri = "/" }, Startup.SusiPolicyId);
            }
        }

        public void Logout()
        {
            // To sign out the user, you should issue an OpenIDConnect sign out request.
            if (Request.IsAuthenticated)
            {
                IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
                HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
                Request.GetOwinContext().Authentication.GetAuthenticationTypes();
            }
        }
    }
}

  • 接著,我們要加上登入連結;打開_Layout.cshtml,加上登入控制標籤
程式碼如下:
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Login", "Login", "Account")</li>
    <li>@Html.ActionLink("Claim", "Claim", "Home")</li>
    </ul>
  • 新增一個Claim View,程式碼如下
@using System.Security.Claims
@{
    ViewBag.Title = "Claims";
}
<h2>@ViewBag.Title</h2>
<h4>Claims Present in the Claims Identity: @ViewBag.DisplayName</h4>
<table class="table table-hover table-striped table-condensed">
    <tr>
        <th>Claim Type</th>
        <th>Claim Value</th>
    </tr>
    @foreach (Claim claim in ClaimsPrincipal.Current.Claims)
    {
        <tr>
            <td>@claim.Type</td>
            <td>@claim.Value</td>
        </tr>
    }
</table>
  • 在HomeController.cs新增一個Claim方法
public ActionResult Claim()
{
    System.Security.Claims.Claim displayName = ClaimsPrincipal.Current.FindFirst(ClaimsPrincipal.Current.Identities.First().NameClaimType);
    ViewBag.DisplayName = displayName != null ? displayName.Value : string.Empty;
    return View();
}
  • 最後,修改Visual Studio專案設定,將執行網址(localhost)改為與Azure AD上的設定一致

  • 執行網站,可以看到首頁;點擊右上角Login連結,我們便會被導向Azure AD B2C的預設登入頁面;此時輸入任意帳號登入,會收到錯誤訊息(因為我們並未註冊任何帳號)

  • 點下Sign up now,我們便會被導向註冊頁面;這裡所顯示的欄位便是我們在Azure AD B2C上設定的欄位;輸入完後建立帳號

  • 登入後,切換到Claim頁面

  • 可以看到我們從Azure AD B2C收到的Claim

大功告成!

Blog Archive

About Me