ASP.NET Identityを使ったユーザー管理APIの作成

# 概要
ASP.NET Identityを使ったユーザー管理機能を作成します。

作成物：https://github.com/koki-2424/dotnet-identity-sample
# 前提

- .NET 5.x
- dotnet cli

# 事前準備

```bash
dotnet new api  -o dotnet-identity-sample 
cd dotnet-identity-sample 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
```

# データベース
データベースはSQLiteを使用し、マイグレーションまでを実施します。

appsetting.jsonを編集

```json
{
  "ConnectionStrings": {
    "DefaultConnection": "DataSource=app.db;Cache=Shared"
  }
}

```

ApplicationDbContext.csを作成
IdentityDbContextを継承することでIdentity用のクラスを使うことができる。

```c#
namespace dotnet_identity_sample.Data
{
    public class ApplicationDbContext : IdentityDbContext<AppUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}
```

DIコンテナに先程作成したApplicationDbContextを設定する。

> この設定はマストではありません。
> 参考：[Entity Framework の DB 接続の解放について、 using した場合と DI コンテナを使った場合の違い。](https://qiita.com/Nossa/items/b6d84aeb134abc85f027)

```c#
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddDbContext<ApplicationDbContext>(options =>
            options
                .UseSqlite(Configuration
                    .GetConnectionString("DefaultConnection")));
}
```

以下のコマンドを実行しテーブルの作成をします。

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

#　認証サービスの作成

Token発行のService、TokenService.csを作成

```c#
namespace dotnet_identity_sample.Services
{
    public class TokenService
    {
        public string CreateToken(AppUser user){
          var claims = new List<Claim>{
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.NameIdentifier, user.Id),
            new Claim(ClaimTypes.Email, user.Email)
          };

          // ToDo: set a secret string
          var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super secret key"));
          var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

          var tokenDescriptor = new SecurityTokenDescriptor{
            Subject = new ClaimsIdentity(claims),
            Expires = System.DateTime.Now.AddDays(7),
            SigningCredentials = creds
          };

          var tokenHandler = new JwtSecurityTokenHandler();

          var token = tokenHandler.CreateToken(tokenDescriptor);

          return tokenHandler.WriteToken(token);
        }
    }
}
```

認証用の拡張サービスを作成。作成した拡張サービスをStartup.csのDIコンテナに設定する

IdentityServiceExtension.csを作成

```c#
namespace dotnet_identity_sample.Extensions
{
  public static class IdentityServiceExtensions
  {
    public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
    {
      services.AddIdentityCore<AppUser>(opt =>
      {
        opt.Password.RequireNonAlphanumeric = false;
      })
      .AddEntityFrameworkStores<ApplicationDbContext>()
      .AddSignInManager<SignInManager<AppUser>>();

      var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super secret key"));

      services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
      .AddJwtBearer(opt =>
      {
        opt.TokenValidationParameters = new TokenValidationParameters
        {
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = key,
          ValidateIssuer = false,
          ValidateAudience = false
        };
      });
      services.AddScoped<TokenService>();

      return services;
    }
  }
}
```

```c#
public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentityServices(Configuration);
}
```

# 作成したServiceをControllerで使用する。

## DTOの作成

```c#
  public class LoginDto
  {
    public string Email { get; set; }
    public string Password { get; set; }
  }

  public class RegisterDto
  {
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Password { get; set; }
    public string UserName { get; set; }
  }

  public class UserDto
  {
    public string Id { get; set; }
    public string DisplayName { get; set; }
    public string Token { get; set; }
    public string UserName { get; set; }
    public string Iamge { get; set; }
  }
```

## Controllerの作成

```c#
namespace dotnet_identity_sample.Controllers
{
  [ApiController]
  [Route("api/[controller]")]
  public class AccountController : ControllerBase
  {
    private readonly UserManager<AppUser> _userManager;
    private readonly TokenService _tokenService;

    private readonly SignInManager<AppUser> _signInManager;
    public AccountController(
    UserManager<AppUser> userManager,
    SignInManager<AppUser> signInManager,
    TokenService tokenService)
    {
      _tokenService = tokenService;
      _userManager = userManager;
      _signInManager = signInManager;
    }

    [HttpPost("login")]
    public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
    {
      var user = await _userManager.FindByEmailAsync(loginDto.Email);

      if (user == null) return Unauthorized();

      var result = await _signInManager.CheckPasswordSignInAsync(user, loginDto.Password, false);

      if (result.Succeeded)
      {
        return CreateUserObject(user);
      }

      return Unauthorized();
    }

    [HttpPost("register")]
    public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
    {
      if (await _userManager.Users.AnyAsync(x => x.Email == registerDto.Email))
      {
        return BadRequest("Email taken");
      }

      if (await _userManager.Users.AnyAsync(x => x.UserName == registerDto.UserName))
      {
        return BadRequest("User name taken");
      }

      var user = new AppUser
      {
        Email = registerDto.Email,
        //DisplayName = registerDto.DisplayName,
        UserName = registerDto.UserName
      };

      var result = await _userManager.CreateAsync(user, registerDto.Password);

      if (result.Succeeded)
      {
        return CreateUserObject(user);
      }

      return BadRequest("failed regist");
    }

    [Authorize]
    [HttpGet]
    public async Task<ActionResult<UserDto>> GetCurrentUser()
    {
      var user = await _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email));

      return CreateUserObject(user);
    }

    private UserDto CreateUserObject(AppUser user)
    {
      user.Email = user.UserName;
      return new UserDto
      {
        Id = user.Id,
        Iamge = null,
        Token = _tokenService.CreateToken(user),
        UserName = user.UserName
      };
    }
  }
}
```