Sign in with Apple を ASP.NET Core のウェブサイトに導入してみる

## 前書き
ASP.NET Core で作成したウェブサイトに Sign in WIth Apple を組み込む必要があったので、調べたついでに導入手順を書き残します。

需要があるかわかりませんが、GitHub にサンプルもあげておきます。
https://github.com/Yuki0928/SignInWIthAppleSample

ASP .NET Core では Facebook、Twitter、Google、Microsoft などの外部プロバイダーの認証を非常に簡単にアプリに組み込めるのですが、Sign in with Apple は勝手が違うところがあり少し手間でした。

## 作業環境
Windows 10
Visual Studio 2019

## Apple 開発者ポータルでの事前準備
アップルのヘルプなどを参考に Sign in with Apple を使用する準備を行います。
https://help.apple.com/developer-account/#/devde676e696

主な作業は3つです。
- App ID の作成
- Service ID の作成
- Private Key の作成

個人的にはヘルプは少々わかりづらかったので、こちらの記事を参考に準備を行いました。
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

#### ドメインと認証後のコールバック URL の登録 について
今回はローカルで動作確認を行うため、example.com と https://example.com:5001/signin-apple を登録し、hosts に 127.0.0.1 example.com のレコードを登録しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/feb15176-8fc6-9d5f-9bbb-0b4aa46a2656.png)
localhost で登録できればいいんですが、ちょっと面倒ですね。

## ASP.NET Core Web アプリケーションを作成と動作確認

Visual Studio を起動して ASP.NET Core Web アプリケーション を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/204864f6-d433-08b3-18f4-608db55e6e8a.png)

任意のプロジェクト名などを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/24f76042-5be7-6f49-4651-82a08109f69f.png)

Web アプリケーション（モデル ビュー コントローラー）を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/636e9f71-424e-ba9a-3898-d1ebcfcc4fae.png)

Properties から LaunchSettings.json を開きます。
下のほうの applicationUrl の localhost を example.com に書き換えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/5d6df1df-276c-a09d-32f1-fc1b03ca8f1a.png)

IIS Express の横の小さな三角ボタンを押して、SignInWithAppleSample を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/cadb793a-1ace-061c-bb16-523b18a1478b.png)

SignInWithAppleSample ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/66f060e3-bdc7-c3bc-624b-8465c19b2a12.png)

証明書を信頼します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/d23e36bb-b43f-42d6-ae04-41a8cd58d7b2.png)

Windows ファイアウォールの警告が表示された場合はアクセスを許可します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/8317bc41-798e-4c55-d09f-0e85ba13e0c6.png)

ブラウザで警告が表示された場合は接続を許可します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/05d9e334-ddfc-6900-b43a-736efab276ca.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/73f6cea5-6b01-b03b-612f-691d19d511af.png)

https://example.com:5001 で ASP.NET Core Web アプリケーションが動いていることが確認できました。このブラウザは一旦閉じます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/287dd8e9-30ca-d31f-56da-cf6a090760ce.png)

### Microsoft.AspNetCore.Authentication.OpenIdConnect をインストール
NuGet で Microsoft.AspNetCore.Authentication.OpenIdConnect をインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/67a9c227-76c9-5197-ed5b-63aa3197cf63.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/78c59ec0-dac1-28dd-c777-7f486bce770b.png)

## TokenGenerator の作成
Model に TokenGenerator クラスを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/c1164830-035b-649e-50fe-265802b5c65c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/f05341ac-7724-6228-f3fd-2b16c8b3cf96.png)

TokenGenerator.cs のコードはこちらをコピペして上書きします。
iss、sub、kid、privateKey は自分の情報に書き換えます。（詳しくは後述）

```csharp
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;

namespace SignInWithAppleSample.Models
{
    public static class TokenGenerator
    {
        public static string CreateNewToken()
        {
            const string iss = "TeamID をいれます";
            const string sub = "ServiceID をいれます";
            const string kid = "KeyID をいれます";
            const string privateKey = "PrivateKey　をいれます";

            var cngKey = CngKey.Import(
              Convert.FromBase64String(privateKey),
              CngKeyBlobFormat.Pkcs8PrivateBlob);

            var handler = new JwtSecurityTokenHandler();
            var token = handler.CreateJwtSecurityToken(
                issuer: iss,
                audience: "https://appleid.apple.com",
                subject: new ClaimsIdentity(new List<Claim> { new Claim("sub", sub) }),
                expires: DateTime.UtcNow.AddMinutes(5),
                issuedAt: DateTime.UtcNow,
                notBefore: DateTime.UtcNow,
                signingCredentials: new SigningCredentials(new ECDsaSecurityKey(new ECDsaCng(cngKey)) { KeyId = kid }, SecurityAlgorithms.EcdsaSha256));

            return handler.WriteToken(token);
        }
    }
}

```

TeamID と ServiceID は Apple の開発者ポータルから取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/c79f16ef-5317-dfde-3be1-36895950b7a6.png)

KeyID とPrivateKey は開発者ポータルで Key を発行したときに `AuthKey_xxxxxxxxxx.p8` というファイルをダウンロードしているはずですのでそちらから取得します。

KeyID は上記のファイル名の xxxxxxxxxx の部分です。

PrybateKey はファイル内の文字列の改行を削除して１行にしたものを入れます。
先頭の `-----BEGIN PRIVATE KEY-----` と末尾の `-----END PRIVATE KEY-----` も削除してください。

## Sign in with Apple を組み込む
Startup.cs を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/de13f67d-04e4-ee17-206d-1c0df03a7a04.png)

using を追加します。

```csharp
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using SignInWithAppleSample.Models;
```

ConfigureServices メソッド を以下のように書き換えます。
ServiceID は先ほどと同じものをいれます。

``` csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(async options =>
    {
        options.ClientId = "ServiceID をいれます";
        options.CallbackPath = "/signin-apple";
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.Scope.Clear();

        options.Configuration = new OpenIdConnectConfiguration
        {
            AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize",
            TokenEndpoint = "https://appleid.apple.com/auth/token",
        };

        options.Events.OnAuthorizationCodeReceived = context =>
        {
            context.TokenEndpointRequest.ClientSecret = TokenGenerator.CreateNewToken();
            return Task.CompletedTask;
        };

        options.TokenValidationParameters.ValidIssuer = "https://appleid.apple.com";

        var jwks = await new HttpClient().GetStringAsync("https://appleid.apple.com/auth/keys");
        options.TokenValidationParameters.IssuerSigningKeys = new JsonWebKeySet(jwks).Keys;

        options.ProtocolValidator.RequireNonce = false;
    });
}
```

Configure メソッドに `app.UseAuthentication();` を追加します。

``` csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseAuthentication(); // この1行を追加

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

HomeController.cs を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/943ff448-aa4b-a6ab-3001-7c0260db982f.png)

適当な場所に SignInWithApple メソッドを追加します。

``` csharp
public IActionResult SignInWithApple ()
{
    return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
```

Index.cshtml を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/25bbdaf6-8e5f-fb2a-3d4b-731f5dc50146.png)

末尾に以下のコードを書き足します。

``` csharp
@if (User.Identity.IsAuthenticated)
{
    @User.Claims.First(x => x.Type.Contains("nameidentifier")).Value
}
else
{
    @Html.ActionLink("Sign in With Apple でサインインする", "SignInWithApple")
}
```

以上で導入は一旦完了です。
ログアウト処理を書いていませんが、Sign in with Apple だからといって変わったことはないのでここでは割愛します。

## Sign in With Apple の動作確認
最初の動作確認と同様に Web アプリケーションをデバッグ実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/2cff1479-6dc2-023c-0370-58c16d4a8470.png)

Sign in With Apple でサインインする のリンクをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/709b445c-3cea-b8ec-8fd5-6f784dc4a4b6.png)

Apple の認証画面に飛ばされるのでサインインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/9d2f1353-b94d-f77e-5f50-3965bf1b5c8b.png)

サインインすると元の画面に戻ります。
Apple から取得したユーザーの ID が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97193/e6ee3997-b008-8872-5008-35128a5c2c68.png)

これでアプリやゲームに Sign in With Apple を組み込めますね！

### 参考
https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core#aspnetcore

https://docs.microsoft.com/ja-jp/aspnet/core/security/authentication/social/?view=aspnetcore-3.1&tabs=visual-studio