Contents

Google登入-手動建立登入 OAuth 2

Google登入-手動建立登入 OAuth 2

Google 文件我實在不太習慣,所以這邊算是有點東拼西湊的,照自己的理解實作,我會把找到的文件參考連結,放在步驟裡面

Google 官方也有一份.NET OAuth,似乎是獨立文件,只是裡面有很多使用Google 自行開發的Library,我沒有採用,一樣放在參考部分

實作

  1. 先找到OAuth 驗證連結,參考要傳送的選項,組合驗證連結,這邊要留意文件中Score說明

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public IActionResult Index()
    {
        State = Guid.NewGuid();
        // scope 來自於要啟用的Google API,因為我們只是要取得最基本的資料,所以這邊直接設定 profile
        // 如果是使用不同的Google API服務,這邊的Scope值也會不同
        ViewData["GoogleAuth"] = $"https://accounts.google.com/o/oauth2/auth?" +
            $"scope={HttpUtility.UrlEncode("profile email")}" +
            $"&response_type=code" +
            $"&state={State}" +
            $"&redirect_uri={RedirectUrl}" +
            $"&client_id={_appId}";
        return View();
    }
    
  2. 參考OAuth Response結果,這邊大致上都是code、state、error,依照這結果設置使用者同意或不同意的導回結果

    1
    2
    3
    4
    5
    6
    
    public async Task<IActionResult> SignInGoogle(string code, Guid state, string error, string error_description)
    {
        // 有錯誤訊息(未授權等)、State遺失、State不相同、沒有code
        if (!string.IsNullOrEmpty(error) || state == null || State != state || string.IsNullOrEmpty(code))
            return RedirectToAction(nameof(Index));
    }
    
  3. 尋找 使用代碼交換存取權杖 的連結,一樣在 OAuth 驗證頁面,先根據回傳結果建立物件

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public class GoogleLoginResource
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    
        [JsonProperty("token_type")]
        public string TokenType { get; set; }
    
        [JsonProperty("expires_in")]
        public string ExpiresIn { get; set; }
    
        [JsonProperty("scope")]
        public string Scope { get; set; }
    
        [JsonProperty("refresh_token")]
        public string RefreshToken { get; set; }
    }
    
  4. 透過API取得回傳結果

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    var url = "https://oauth2.googleapis.com/token";
    var postData = new Dictionary<string, string>()
    {
        {"client_id",_appId},
        {"client_secret",_appSecret},
        {"code",code},
        {"grant_type","authorization_code"},
        {"redirect_uri",RedirectUrl}
    };
    string json = JsonConvert.SerializeObject(postData);
    HttpContent contentPost = new StringContent(json, Encoding.UTF8, "application/json");
    
    var client = _clientFactory.CreateClient();
    var response = await client.PostAsync(url, contentPost);
    
    string responseContent;
    if (response.IsSuccessStatusCode)
        responseContent = await response.Content.ReadAsStringAsync();
    else
        return RedirectToAction(nameof(Index));
    
    var googleLoginResource = JsonConvert.DeserializeObject<GoogleLoginResource>(responseContent);
    
  5. 有AccessToken,接下來就是找,我們要使用的服務,這邊只打算取得簡單的ID、Name、Email,所以瀏覽一下服務後,決定選擇People API使用,這時你會發現一個絕望的事實…Simple中沒有.NET 選項,所以可以放棄了…,然後點開Read Profiles,又會看到.NET Code,就會感到又驚又喜又害怕,一臉茫然不知道這個怎麼出來的,所以又可以放棄了…

    /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-28_15-11-41.png /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-28_15-15-13.png
  6. 不!,這時候就是回到原點(你料理的原點是什麼..?),我們參考了Protocol,就會發現,他其實是WebAPI使用而已

    /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-28_15-24-27.png
  7. 所以我們根據這個API,組合出下面的結果

    1
    2
    3
    4
    5
    6
    7
    8
    
    url = $"https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses";
    
    response = await client.GetAsync(url);
    if (response.IsSuccessStatusCode)
    {
        responseContent = await response.Content.ReadAsStringAsync();
        var user = JsonConvert.DeserializeObject<GoogleProfile>(responseContent);
    }
    
  8. 一切就是計畫中,世界就是這麼美好,直接看執行結果…WTF

    /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-28_15-36-51.png
  9. 幸福來的太突然…,翻找了前後程式碼不覺得有不同啊,就在三度想放棄的這個時候,忽然想起,似乎沒有用到AccessToken,苦熬著破爛的英文,翻找了數頁後,在Authorize Requests中,發現一句話…

    /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-14_17-07-25.png
  10. 所以我們憑直覺、憑運氣、憑經驗,直接加上這段,改成…

    1
    2
    
    url = $"https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses" +
        $"&access_token={googleLoginResource.AccessToken}";
    
  11. 結果

    /static/Google_手動建立登入_OAuth2_38d9112c2cca4ce6bb7309f15dd57458/2020-09-14_17-02-25.png

完整程式碼

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class LoginController : Controller
{
    private readonly string _appId = "{應用程式編號}";
    private readonly string _appSecret = "{應用程式密鑰}";

    private readonly IHttpClientFactory _clientFactory;

    public LoginController(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    private string RedirectUrl => "https://" + HttpContext.Request.Host.ToString() + "/Login/SignInGoogle";

    [TempData]
    public Guid State { get; set; }

    public IActionResult Index()
    {
        State = Guid.NewGuid();
        // scope 來自於要啟用的Google API,因為我們只是要取得最基本的資料,所以這邊直接設定 profile
        // 如果是使用不同的Google API服務,這邊的Scope值也會不同
        ViewData["GoogleAuth"] = $"https://accounts.google.com/o/oauth2/auth?" +
            $"scope={HttpUtility.UrlEncode("profile email")}" +
            $"&response_type=code" +
            $"&state={State}" +
            $"&redirect_uri={RedirectUrl}" +
            $"&client_id={_appId}";
        return View();
    }

    public async Task<IActionResult> SignInGoogle(string code, Guid state, string error, string error_description)
    {
        // 有錯誤訊息(未授權等)、State遺失、State不相同、沒有code
        if (!string.IsNullOrEmpty(error) || state == null || State != state || string.IsNullOrEmpty(code))
            return RedirectToAction(nameof(Index));

        // 使用代碼交換存取權杖
        var url = "https://oauth2.googleapis.com/token";
        var postData = new Dictionary<string, string>()
        {
            {"client_id",_appId},
            {"client_secret",_appSecret},
            {"code",code},
            {"grant_type","authorization_code"},
            {"redirect_uri",RedirectUrl}
        };
        string json = JsonConvert.SerializeObject(postData);
        HttpContent contentPost = new StringContent(json, Encoding.UTF8, "application/json");

        var client = _clientFactory.CreateClient();
        var response = await client.PostAsync(url, contentPost);

        string responseContent;
        if (response.IsSuccessStatusCode)
            responseContent = await response.Content.ReadAsStringAsync();
        else
            return RedirectToAction(nameof(Index));

        var googleLoginResource = JsonConvert.DeserializeObject<GoogleLoginResource>(responseContent);

        // 接下來就是應用Google API,因為我們只打算取得最基礎的ID、Name、Email,所以我們採用Google People API
        // 這部份會影響一開始scope設定值,可以說,其實要先知道使用哪個API服務,前面是OAuth流程
        url = $"https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses" +
              $"&access_token={googleLoginResource.AccessToken}";

        response = await client.GetAsync(url);
        if (response.IsSuccessStatusCode)
        {
            responseContent = await response.Content.ReadAsStringAsync();
            var user = JsonConvert.DeserializeObject<GoogleProfile>(responseContent);
        }

        return View();
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class GoogleLoginResource
{
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }

    [JsonProperty("token_type")]
    public string TokenType { get; set; }

    [JsonProperty("expires_in")]
    public string ExpiresIn { get; set; }

    [JsonProperty("scope")]
    public string Scope { get; set; }

    [JsonProperty("refresh_token")]
    public string RefreshToken { get; set; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class GoogleProfile
{
    [JsonProperty("resourceName")]
    public string ResourceName { get; set; }

    [JsonProperty("etag")]
    public string Etag { get; set; }

    [JsonProperty("names")]
    public Name[] Names { get; set; }

    [JsonProperty("emailAddresses")]
    public Emailaddress[] EmailAddresses { get; set; }

    public class Name
    {
        [JsonProperty("metadata")]
        public Metadata Metadata { get; set; }

        [JsonProperty("displayName")]
        public string DisplayName { get; set; }

        [JsonProperty("familyName")]
        public string FamilyName { get; set; }

        [JsonProperty("givenName")]
        public string GivenName { get; set; }

        [JsonProperty("displayNameLastFirst")]
        public string DisplayNameLastFirst { get; set; }

        [JsonProperty("unstructuredName")]
        public string UnstructuredName { get; set; }
    }

    public class Metadata
    {
        [JsonProperty("primary")]
        public bool Primary { get; set; }

        [JsonProperty("source")]
        public Source Source { get; set; }
    }

    public class Source
    {
        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("id")]
        public string Id { get; set; }
    }

    public class Emailaddress
    {
        [JsonProperty("metadata")]
        public Metadata Metadata { get; set; }

        [JsonProperty("value")]
        public string Value { get; set; }
    }
}

補充

上面沒有關於Google People API的Response物件,是因為我有點懶得找了,直接使用將ReadAsStringAsync,取得的資料,使用Visusal Studio 內建的貼上JSON做為類別的功能(編輯 > 選擇性貼上 > 貼上JSON做為類別),在稍加修改而成

正規流程應該是,先看一開始設置的Score範圍,可以讀取那些資料,然後再設置personFields的時候以,間格加上去,再到這邊去看組成格式,產生JSON To Object物件。

參考

Google Using OAuth 2.0 for Web Server Applications

https://developers.google.com/identity/protocols/oauth2/web-server

Google 官方 .NET OAuth 實作

https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth

Google People API

https://developers.google.com/people

Google People API Authorize Requests說明

https://developers.google.com/people/v1/how-tos/authorizing

Google People API 資料Response格式

https://developers.google.com/people/api/rest/v1/people

結論

嗯…其實結果畫面一出來,我大概就知道結論要打什麼了,這就是我為什麼"不那麼喜歡"串Google API的原因,一切盡在不言中,反正結果是出來了,希望這篇能讓你少走一些路…