最近工作上遇到一個需求,一個系統登入分別有三種模式
- 使用者自行建立的帳號密碼
- 公司網域的AD登入帳號密碼
- 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後基本上就大功告成了,進入資料庫比對就能完成第三方登入的驗證流程了。
很謝謝這次專案的經驗讓我對第三方授權驗證有了更深的了解,每一個服務的流程或許有些差異但大致上的流程是差不多的,以上的寫法有任何需要改進或一起討論交流的地方,歡迎前輩們不吝賜教非常感謝~~