[ASP.NET Identity] OAuth Server OWIN Setup

續上篇 https://dotblogs.com.tw/yc421206/2016/08/02/asp_net_identity_basic 了解 ASP.NET Identity 的 UserManager 的運作方式後,這裡就要把 UserManager 丟到 OWIN 執行

開發環境

  • Windows 10 Enterprise x64 CHT
  • VS2015 Update3 ENG

本文連結

Step1.從 Nuget 安裝套件

在 Simple.OAuthServer 專案安裝以下套件

Install-Package Microsoft.Owin.Host.SystemWeb
Dependencies

  • Owin (>= 1.0.0)
  • Microsoft.Owin (>= 3.0.1)

Install-Package Microsoft.AspNet.Identity.Owin
Dependencies

  • Microsoft.AspNet.Identity.Core (>= 2.2.1)
  • Microsoft.Owin.Security (>= 2.1.0)
  • Microsoft.Owin.Security.Cookies (>= 2.1.0)
  • Microsoft.Owin.Security.OAuth (>= 2.1.0)

Install-Package Microsoft.Owin.Security.OAuth
Dependencies

  • Owin (>= 1.0.0)
  • Microsoft.Owin (>= 3.0.1)
  • Newtonsoft.Json (>= 6.0.4)
  • Microsoft.Owin.Security (>= 3.0.1)

Install-Package Microsoft.AspNet.WebApi.Owin
Dependencies

  • Microsoft.AspNet.WebApi.Core (>= 5.2.3 && < 5.3.0)
  • Microsoft.Owin (>= 2.0.2)
  • Owin (>= 1.0.0)
  •  

Step2.加入空的 Web API2 Controller

完成後,會新增 WebApiConfig.cs,必須要在 Global.asax 的 Application_Start 方法調用 GlobalConfiguration.Configure(WebApiConfig.Register);

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    GlobalConfiguration.Configure(WebApiConfig.Register);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

Step3.在 AccountApiController 加入 Register 方法

ModelState.IsValid 用來檢查 Model 欄位的 Attribute

[RoutePrefix("api/account")]
public class AccountApiController : ApiController
{
    private ApplicationUserManager _userManager;

    public ApplicationUserManager UserManager
    {
        get { return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); }
        private set { _userManager = value; }
    }

    // POST api/Account/Register
    [AllowAnonymous]
    [Route("Register")]
    public async Task<IHttpActionResult> Register(RegisterBindingModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var user = new ApplicationIdentityUser
        {
            UserName = model.Email,
            Email = model.Email
        };
        var result = await UserManager.CreateAsync(user, model.Password);

        if (!result.Succeeded)
        {
            return GetErrorResult(result);
        }

        return Ok();
    }

    private IHttpActionResult GetErrorResult(IdentityResult result)
    {
        if (result == null)
        {
            return InternalServerError();
        }

        if (!result.Succeeded)
        {
            if (result.Errors != null)
            {
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError("", error);
                }
            }

            if (ModelState.IsValid)
            {
                // No ModelState errors are available to send, so just return an empty BadRequest.
                return BadRequest();
            }

            return BadRequest(ModelState);
        }

        return null;
    }
}

 

RegisterBindingModel

欄位上的 Atturibute 會被檢查(ModelState.IsValid)

public class RegisterBindingModel
{
    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

Step4.加入 AuthorizationServerProvider

GrantResourceOwnerCredentials 方法:用來取得資源,比如 Token Access

ValidateClientAuthentication 方法:通常會先頒發 client_id 和 client_secret 給 Client端,然後再用這個方法判斷 Client,這裡就不處理

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        ApplicationIdentityUser user;
        try
        {
            user = await userManager.FindAsync(context.UserName, context.Password);
        }
        catch
        {
            context.SetError("server_error");
            context.Rejected();
            return;
        }
        if (user != null)
        {
            var identity = await userManager.CreateIdentityAsync(user,
                                                                 DefaultAuthenticationTypes.ExternalBearer);
            context.Validated(identity);
        }
        else
        {
            context.SetError("invalid_grant", "Invalid User Id or password'");
            context.Rejected();
        }
    }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        // Resource owner password credentials does not provide a client ID.
        if (context.ClientId == null)
        {
            context.Validated();
        }

        return Task.FromResult<object>(null);
    }
}

Step5.加入 Startup

ConfigureOAuth 方法:定義 OAuth Server 組態

CreateUserManager 方法:定義 UserManager 的密碼、帳號檢查規則

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureOAuth(app);
    }

    private void ConfigureOAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(() => new ApplicationDbContext());
        app.CreatePerOwinContext<ApplicationUserManager>(CreateUserManager);

        //app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        var oauthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/oauth/token"),
            Provider = new AuthorizationServerProvider(),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
            AllowInsecureHttp = true
        };

        app.UseOAuthAuthorizationServer(oauthOptions);
    }

    private static ApplicationUserManager CreateUserManager(IdentityFactoryOptions<ApplicationUserManager> options,
                                                        IOwinContext context)
    {
        var dbContext = context.Get<ApplicationDbContext>();
        var userStore = new ApplicationUserStore(dbContext);
        var userManager = new ApplicationUserManager(userStore);
        
        userManager.UserValidator = new UserValidator<ApplicationIdentityUser>(userManager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        userManager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true
        };
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            userManager.UserTokenProvider =
                new DataProtectorTokenProvider<ApplicationIdentityUser>(
                    dataProtectionProvider.Create("ASP.NET Identity"));
        }
        return userManager;
    }
}

Step6.使用 Fiddler 測試

建立帳號

http://localhost:14695/Api/Account/Register

Header:

Content-Type: application/json; charset=utf8

Request Body:

{
  "Email": "test@aa.bb",
  "Password": "Pass@w0rd1",
  "ConfirmPassword": "Pass@w0rd1"
}

操作步驟如下圖:

 

取得 Access Token

http://localhost:14695/oauth/token

Request Body:

grant_type=password&username=test@aa.bb&password=Pass@w0rd1

操作步驟如下圖:

專案位置:

https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.OAuthServer/

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo