つくるオーオース　MVC編

# はじめに
この記事では.NET Core 3.1をつかってオーオースを学習する方法を書いてみます。共通関数を追加しました。(2020/4/21)

# 実行環境
下記バージョンで動作確認しています。
- MacOS
- .NET Core 3.1

```
% dotnet --version
3.1.101
```

# 学習方針
コマンドプロンプトから実行する事で、Windows、Linuxにおいてもそのままできると思います。

```
% mkdir myop
% cd myop
% dotnet new mvc --auth individual
```

必要なツールをインストールします。

```
% dotnet tool install --global dotnet-ef
% dotnet tool install --global dotnet-aspnet-codegenerator
% dotnet tool list --global
```

必要なパッケージをインストールします。

```
% dotnet add package Microsoft.EntityFrameworkCore.Sqlite
% dotnet add package Microsoft.EntityFrameworkCore.Design
% dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
% dotnet add package Microsoft.EntityFrameworkCore.SqlServer
% dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
% dotnet add package Microsoft.AspNetCore.Identity.UI
% dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
% dotnet add package Microsoft.Extensions.Options
```

Clients、Tokens、Codesモデルを作成します。

```C#:Models/Tables.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace myop.Models
{
//  public class ApplicationDbContext : IdentityDbContext
//  {
//        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}
  public class myopContext : DbContext
  {
        public DbSet<Client> Clients { get; set; }
        public DbSet<Token> Tokens { get; set; }
        public DbSet<Code> Codes { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite("Data Source=app.db");
  }

  public class Client
  {
    [Key]
    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("client_secret")]
    public string ClientSecret { get; set; }

    [DisplayName("access_type")]
    public string AccessType { get; set; }

    [DisplayName("redirect_uris")]
    public string RedirectUris { get; set; }

    [DisplayName("grant_types")]
    public string GrantTypes { get; set; }

    [DisplayName("allowed_scope")]
    public string AllowedScope { get; set; }

    [DisplayName("client_name")]
    public string ClientName { get; set; }

    [DisplayName("auth_method")]
    public string AuthMethod { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class Token
  {
    [Key]
    [DisplayName("user_id")]
    public string UserId { get; set; }

    [DisplayName("access_token")]
    public string AccessToken { get; set; }

    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("refresh_token")]
    public string RefreshToken { get; set; }

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

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class Code
  {
    [Key]
    [DisplayName("code")]
    public string CodeId { get; set; }

    [DisplayName("user_id")]
    public string UserId { get; set; }

    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("nonce")]
    public string Nonce { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }
}
```

モデルからデータベースを生成します。今回データベースにはSQLiteを使います。

```
% dotnet ef migrations add InitialCreate --context myopContext
% dotnet ef database update --context myopContext
```

テスト用なのでポート番号は5000のみで良いと思います。

```JSON:Properties/launchSetting.json
{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:38239",
      "sslPort": 44320
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "myop": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

スキャフォールドでデータベースを確認してみます。

```
% dotnet aspnet-codegenerator controller -name ClientsController -m Client -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

% dotnet aspnet-codegenerator controller -name TokensController -m Token -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries

% dotnet aspnet-codegenerator controller -name CodesController -m Code -dc myopContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
```

ソースコードを一部修正して実行します。これでClients、Tokens、CodesがMVCで登録できます。
http://localhost:5000/Clients/
http://localhost:5000/Tokens/
http://localhost:5000/Codes/

```C#:Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore;
using myop.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace myop
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews().AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.IgnoreNullValues = true;
            });
           services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            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.UseAuthentication();
            app.UseAuthorization();

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

```C#:Models/Tables.cs
using System;
using System.IO;
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography;
using System.IdentityModel.Tokens.Jwt;

namespace myop.Models
{
  public static class Util
  {
    public static bool ByteArraysEqual(byte[] a, byte[] b)
    {
      if (a == null && b == null)
      {
        return true;
      }
      if (a == null || b == null || a.Length != b.Length)
      {
        return false;
      }
      var areSame = true;
      for (var i = 0; i < a.Length; i++)
      {
        areSame &= (a[i] == b[i]);
      }
      return areSame;
    }

    public static bool PasswordEqual(string PasswordHash, string Password)
    {
      byte[] buffer4;
      byte[] src = Convert.FromBase64String(PasswordHash);
      byte[] dst = new byte[0x10];
      Buffer.BlockCopy(src, 0x0D, dst, 0, 0x10);
      byte[] buffer3 = new byte[0x20];
      Buffer.BlockCopy(src, 0x1D, buffer3, 0, 0x20);
      using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(Password, dst, 0x2710, HashAlgorithmName.SHA256))
      {
        buffer4 = bytes.GetBytes(0x20);
      }
      return ByteArraysEqual(buffer3, buffer4);
    }

    public static string GetAtHash(string random)
    {
      SHA256Managed hashstring = new SHA256Managed();
      byte[] bytes = Encoding.Default.GetBytes(random);
      byte[] hash = hashstring.ComputeHash(bytes);
      Byte[] sixteen_bytes = new Byte[16];
      Array.Copy(hash, sixteen_bytes, 16);
      return Convert.ToBase64String(sixteen_bytes).Trim('=');
    }

    public static string GetIdToken(Claim[] claims, string client_id)
    {
      var pemStr = System.IO.File.ReadAllText(@"./private.pem");
      var base64 = pemStr
      .Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty)
      .Replace("-----END RSA PRIVATE KEY-----", string.Empty)
      .Replace("\r\n", string.Empty)
      .Replace("\n", string.Empty);
      var der = Convert.FromBase64String(base64);
      var rsa = RSA.Create();
      rsa.ImportRSAPrivateKey(der, out _);
      var key = new RsaSecurityKey(rsa);
      key.KeyId = "testkey";
      var creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
      var jwtHeader = new JwtHeader(creds);
      var jwtPayload = new JwtPayload(
      issuer: "http://localhost:5000/op",
        audience: client_id,
        claims: claims,
        notBefore: DateTime.Now,
        expires: DateTime.Now.AddMinutes(600),
        issuedAt: DateTime.Now
      );
      var jwt = new JwtSecurityToken(jwtHeader, jwtPayload);
      return new JwtSecurityTokenHandler().WriteToken(jwt);
    }
  }

  public class ApplicationDbContext : IdentityDbContext
  {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}
//  public class myopContext : DbContext
//  {
        public DbSet<Client> Clients { get; set; }
        public DbSet<Token> Tokens { get; set; }
        public DbSet<Code> Codes { get; set; }
//        protected override void OnConfiguring(DbContextOptionsBuilder options)
//            => options.UseSqlite("Data Source=app.db");
  }
  public class Client
  {
    [Key]
    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("client_secret")]
    public string ClientSecret { get; set; }

    [DisplayName("access_type")]
    public string AccessType { get; set; }

    [DisplayName("redirect_uris")]
    public string RedirectUris { get; set; }

    [DisplayName("grant_types")]
    public string GrantTypes { get; set; }

    [DisplayName("allowed_scope")]
    public string AllowedScope { get; set; }

    [DisplayName("client_name")]
    public string ClientName { get; set; }

    [DisplayName("auth_method")]
    public string AuthMethod { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class Token
  {
    [Key]
    [DisplayName("user_id")]
    public string UserId { get; set; }

    [DisplayName("access_token")]
    public string AccessToken { get; set; }

    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("refresh_token")]
    public string RefreshToken { get; set; }

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

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }

  public class Code
  {
    [Key]
    [DisplayName("code")]
    public string CodeId { get; set; }

    [DisplayName("user_id")]
    public string UserId { get; set; }

    [DisplayName("client_id")]
    public string ClientId { get; set; }

    [DisplayName("nonce")]
    public string Nonce { get; set; }

    [DisplayName("iat")]
    public DateTime Iat { get; set; }
  }
}
```

自動生成されたData配下を削除し統合します。

```C#:Migrations/myopContextModelSnapshot.cs
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using myop.Models;

namespace myop.Migrations
{
    [DbContext(typeof(ApplicationDbContext))]
    partial class myopContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasAnnotation("ProductVersion", "3.1.3");

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
                {
                    b.Property<string>("Id")
                        .HasColumnType("TEXT");

                    b.Property<string>("ConcurrencyStamp")
                        .IsConcurrencyToken()
                        .HasColumnType("TEXT");

                    b.Property<string>("Name")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.Property<string>("NormalizedName")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.HasKey("Id");

                    b.HasIndex("NormalizedName")
                        .IsUnique()
                        .HasName("RoleNameIndex");

                    b.ToTable("AspNetRoles");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("INTEGER");

                    b.Property<string>("ClaimType")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClaimValue")
                        .HasColumnType("TEXT");

                    b.Property<string>("RoleId")
                        .IsRequired()
                        .HasColumnType("TEXT");

                    b.HasKey("Id");

                    b.HasIndex("RoleId");

                    b.ToTable("AspNetRoleClaims");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
                {
                    b.Property<string>("Id")
                        .HasColumnType("TEXT");

                    b.Property<int>("AccessFailedCount")
                        .HasColumnType("INTEGER");

                    b.Property<string>("ConcurrencyStamp")
                        .IsConcurrencyToken()
                        .HasColumnType("TEXT");

                    b.Property<string>("Email")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.Property<bool>("EmailConfirmed")
                        .HasColumnType("INTEGER");

                    b.Property<bool>("LockoutEnabled")
                        .HasColumnType("INTEGER");

                    b.Property<DateTimeOffset?>("LockoutEnd")
                        .HasColumnType("TEXT");

                    b.Property<string>("NormalizedEmail")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.Property<string>("NormalizedUserName")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.Property<string>("PasswordHash")
                        .HasColumnType("TEXT");

                    b.Property<string>("PhoneNumber")
                        .HasColumnType("TEXT");

                    b.Property<bool>("PhoneNumberConfirmed")
                        .HasColumnType("INTEGER");

                    b.Property<string>("SecurityStamp")
                        .HasColumnType("TEXT");

                    b.Property<bool>("TwoFactorEnabled")
                        .HasColumnType("INTEGER");

                    b.Property<string>("UserName")
                        .HasColumnType("TEXT")
                        .HasMaxLength(256);

                    b.HasKey("Id");

                    b.HasIndex("NormalizedEmail")
                        .HasName("EmailIndex");

                    b.HasIndex("NormalizedUserName")
                        .IsUnique()
                        .HasName("UserNameIndex");

                    b.ToTable("AspNetUsers");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("INTEGER");

                    b.Property<string>("ClaimType")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClaimValue")
                        .HasColumnType("TEXT");

                    b.Property<string>("UserId")
                        .IsRequired()
                        .HasColumnType("TEXT");

                    b.HasKey("Id");

                    b.HasIndex("UserId");

                    b.ToTable("AspNetUserClaims");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
                {
                    b.Property<string>("LoginProvider")
                        .HasColumnType("TEXT")
                        .HasMaxLength(128);

                    b.Property<string>("ProviderKey")
                        .HasColumnType("TEXT")
                        .HasMaxLength(128);

                    b.Property<string>("ProviderDisplayName")
                        .HasColumnType("TEXT");

                    b.Property<string>("UserId")
                        .IsRequired()
                        .HasColumnType("TEXT");

                    b.HasKey("LoginProvider", "ProviderKey");

                    b.HasIndex("UserId");

                    b.ToTable("AspNetUserLogins");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
                {
                    b.Property<string>("UserId")
                        .HasColumnType("TEXT");

                    b.Property<string>("RoleId")
                        .HasColumnType("TEXT");

                    b.HasKey("UserId", "RoleId");

                    b.HasIndex("RoleId");

                    b.ToTable("AspNetUserRoles");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
                {
                    b.Property<string>("UserId")
                        .HasColumnType("TEXT");

                    b.Property<string>("LoginProvider")
                        .HasColumnType("TEXT")
                        .HasMaxLength(128);

                    b.Property<string>("Name")
                        .HasColumnType("TEXT")
                        .HasMaxLength(128);

                    b.Property<string>("Value")
                        .HasColumnType("TEXT");

                    b.HasKey("UserId", "LoginProvider", "Name");

                    b.ToTable("AspNetUserTokens");
                });

            modelBuilder.Entity("myop.Models.Client", b =>
                {
                    b.Property<string>("ClientId")
                        .HasColumnType("TEXT");

                    b.Property<string>("AccessType")
                        .HasColumnType("TEXT");

                    b.Property<string>("AllowedScope")
                        .HasColumnType("TEXT");

                    b.Property<string>("AuthMethod")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClientName")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClientSecret")
                        .HasColumnType("TEXT");

                    b.Property<string>("GrantTypes")
                        .HasColumnType("TEXT");

                    b.Property<DateTime>("Iat")
                        .HasColumnType("TEXT");

                    b.Property<string>("RedirectUris")
                        .HasColumnType("TEXT");

                    b.HasKey("ClientId");

                    b.ToTable("Clients");
                });

            modelBuilder.Entity("myop.Models.Code", b =>
                {
                    b.Property<string>("CodeId")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClientId")
                        .HasColumnType("TEXT");

                    b.Property<DateTime>("Iat")
                        .HasColumnType("TEXT");

                    b.Property<string>("Nonce")
                        .HasColumnType("TEXT");

                    b.Property<string>("UserId")
                        .HasColumnType("TEXT");

                    b.HasKey("CodeId");

                    b.ToTable("Codes");
                });

            modelBuilder.Entity("myop.Models.Token", b =>
                {
                    b.Property<string>("UserId")
                        .HasColumnType("TEXT");

                    b.Property<string>("AccessToken")
                        .HasColumnType("TEXT");

                    b.Property<string>("ClientId")
                        .HasColumnType("TEXT");

                    b.Property<DateTime>("Iat")
                        .HasColumnType("TEXT");

                    b.Property<string>("RefreshToken")
                        .HasColumnType("TEXT");

                    b.Property<string>("Scope")
                        .HasColumnType("TEXT");

                    b.HasKey("UserId");

                    b.ToTable("Tokens");
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
                {
                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
                        .WithMany()
                        .HasForeignKey("RoleId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
                {
                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
                        .WithMany()
                        .HasForeignKey("UserId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
                {
                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
                        .WithMany()
                        .HasForeignKey("UserId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
                {
                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
                        .WithMany()
                        .HasForeignKey("RoleId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();

                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
                        .WithMany()
                        .HasForeignKey("UserId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();
                });

            modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
                {
                    b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
                        .WithMany()
                        .HasForeignKey("UserId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();
                });
#pragma warning restore 612, 618
        }
    }
}
```

#次回
つくるオーオース　WEBAPI編
https://qiita.com/namikitakeo/items/38be899790cb27a323df