はじめに
この記事では.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モデルを作成します。
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のみで良いと思います。
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/
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();
});
}
}
}
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配下を削除し統合します。
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