最近工作上遇到一個需求,一個系統登入分別有三種模式

  1. 使用者自行建立的帳號密碼
  2. 公司網域的AD登入帳號密碼
  3. Microsoft Azure active directory 帳號驗證登入

由於Azure 上面的使用者資訊已經同步進資料庫了,只要透過第三方OAuth 2.0驗證去Microsoft登入頁面讓使用者自行填寫帳號以及密碼,之後轉跳回公司的系統驗面,將code兌換成token。

再用這組token透過Microsoft Graph取回使用者的資訊後回資料庫進行比對就大功告成了。

這邊是整個過程的流程圖,附上微軟的連結

OAuth2是現今第三方驗證登入的授權框架

這邊有更詳細的介紹

專案是ASP.NET MVC Framework 以下是實作的code

 public ActionResult AADLogin()
        {
            var uri = new Uri($@"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
                         client_id=xxxxxx 
                         &response_type=code
                         &redirect_uri={Request.Url.Scheme}://{Request.Url.Host}:{Request.Url.Port}/Home/AADUser
                         &response_mode=query
                         &scope=offline_access%20user.read%20mail.read
                         &state=");

            return Redirect(uri.AbsoluteUri);
        }

client_id 請輸入在Azure 申請服務時所發放的client_id

redirect_uri 是要轉跳回來的地址

scope 是同意使用者可以存取的授權服務

state 會跟隨著回應傳回,可以填寫任意值用來進行驗證,最好是隨機產生的唯一值,用來防範XSRF,這邊是範例就不填寫了正常使用在專案中,為了資安是建議要做驗證的

其他要填寫的參數和說明參造微軟的官方文件

  public ActionResult AADUser(string code, string error, string state, string _description)
        {
            if (string.IsNullOrEmpty(error))
            {
                TempData["code"] = code;
                return View();
            }
            return RedirectToAction("Login");
        }

檢查返回值是否有包含error 如果沒有的話將code放進TempData中,(正常來說還要拿傳回來的state 值檢查是否與發送過去的state值相同) 但這邊同樣為了範例寫的不是很嚴謹,進入前端頁面時做一個等待頁面,靠前端那邊寫JS來觸擊RedirectToLogin這個Action

  public ActionResult RedirectToLogin()
        {
            string token = string.Empty;

            string code = Convert.ToString(TempData["code"]);

            HttpClient client = new HttpClient();
            var uri = new Uri("https://login.microsoftonline.com/common/oauth2/v2.0/token");
            dynamic obj;
            try
            {
                client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded");
                var body = new FormUrlEncodedContent(new[] {
                    new KeyValuePair<string, string>("clinet_id","xxxxxxxxxxxx"),
                    new KeyValuePair<string, string>("code",code),
                    new KeyValuePair<string, string>("redirect_uri",$"{Request.Url.Scheme}://{Request.Url.Host}:{Request.Url.Port}/Home/AADUser"),
                    new KeyValuePair<string, string>("grant_type","authorization_code"),
                    new KeyValuePair<string, string>("client_secret","xxxxxxxxxxxxxxxxxxxx")
                });

                var responseMessage = client.PostAsync(uri, body).Result;
                responseMessage.EnsureSuccessStatusCode();//失敗拋出例外
                var content = respinseMessage.Content.ReadAsStringAsync().Result;

                if (content.Contains("access_token"))
                {
                    obj = JsonConvert.DeserializeObject<dynamic>(content);
                    token = obj.access_token;
                }
                else
                {
                    return RedirectToAction("xxxx", "xxxx");
                }
            }
            catch (Exception ex)
            {
                return RedirectToAction("xxxx", "xxxx");
            }

依造微軟的要求header Content-type 為application/x-www-form-urlencoded
使用FormUrlEncodedContent會自動幫我們做好編碼

client_secret 填寫Azure 申請服務時所發放的client_secret


HttpClient class提供的方法都是非同步方法但在這邊不需要非同步不用在前面加async Task

Result將現行的執行緒,直到非同步方法傳回結果解除caller執行緒的閒置狀態

從微軟那邊回傳回來的是Json格式,這邊用dynamic 型別,如果在專案中的話最好自己寫一個class來轉換取得token

 string userPrincipalName = string.Empty;
            string id = string.Empty;

            try
            {
                uri = new Uri("https://graph.microsoft.com/v1.0/me");
                client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
                var responseMessage = client.GetAsync(uri).Result;
                responseMessage.EnsureSuccessStatusCode();
                var content = responseMessage.Content.ReadAsStringAsync().Result;
                if (content.Contains("userPrincipalName") && content.Contains("id"))
                {
                    obj = JsonConvert.DeserializeObject<dynamic>(content);
                    userPrincipalName = obj.userPrincipalName;
                    id = obj.id;
                }
                else
                {
                    return RedirectToAction("Login", "Home");
                }
            }
            catch (Exception ex)
            {
                return RedirectToAction("Login", "Home");
            }

            return View();

將取得的token 透過Microsoft Graph取回使用者的資訊以及id

header的地方注意一下要符合微軟的規定

取回使用者名稱和id後基本上就大功告成了,進入資料庫比對就能完成第三方登入的驗證流程了。

很謝謝這次專案的經驗讓我對第三方授權驗證有了更深的了解,每一個服務的流程或許有些差異但大致上的流程是差不多的,以上的寫法有任何需要改進或一起討論交流的地方,歡迎前輩們不吝賜教非常感謝~~

Visited 66 times, 1 visit(s) today

Leave A Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *