1

我有一个使用ASP.NET Core的API,它将被本机移动应用程序(当前UWP,Android)使用,并且我试图实现一种客户端可以注册的方式并使用用户名/密码和外部提供商(例如Google和Facebook)登录。现在我使用的是openIddict,我的ExternalProviderCallback必须返回我认为当前返回cookie的本地令牌! (我已经从某处复制了大部分代码),并且它看起来不是AuthorizationCodeFlow,我认为这是正确的方法!通过使用OpenIddict与外部提供商一起登录来获取令牌

现在这里是我的启动类

public class Startup 
{ 
    public Startup(IHostingEnvironment env) 
    { 
     var builder = new ConfigurationBuilder() 
      .SetBasePath(env.ContentRootPath) 
      .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 

     if (env.IsDevelopment()) 
     { 
      builder.AddUserSecrets(); 
     } 
     builder.AddEnvironmentVariables(); 
     Configuration = builder.Build(); 
    } 

    public IConfigurationRoot Configuration { get; } 

    // This method gets called by the runtime. Use this method to add services to the container. 
    public void ConfigureServices(IServiceCollection services) 
    { 
     services.AddSingleton<IConfiguration>(c => Configuration); 
     services.AddEntityFramework(); 
     services.AddIdentity<ApplicationUser, IdentityRole>(config => 
     { 
      //Setting some configurations 
      config.User.RequireUniqueEmail = true; 
      config.Password.RequireNonAlphanumeric = false; 
      config.Cookies.ApplicationCookie.AutomaticChallenge = false; 
      config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents() 
      { 
       OnRedirectToLogin = context => 
       { 
        if (context.Request.Path.StartsWithSegments("/api") && 
        context.Response.StatusCode == 200) 
         context.Response.StatusCode = 401; 
        return Task.CompletedTask; 
       }, 
       OnRedirectToAccessDenied = context => 
       { 
        if (context.Request.Path.StartsWithSegments("/api") && 
        context.Response.StatusCode == 200) 
         context.Response.StatusCode = 403; 
        return Task.CompletedTask; 
       } 
      }; 
     }) 
     .AddEntityFrameworkStores<ApplicationDbContext>() 
     .AddDefaultTokenProviders(); 
     services.AddDbContext<ApplicationDbContext>(options => 
     { 
      options.UseSqlite(Configuration["Data:DefaultConnection:ConnectionString"]); 
      options.UseOpenIddict(); 
     }); 

     services.AddOpenIddict() 
      .AddEntityFrameworkCoreStores<ApplicationDbContext>() 
      .UseJsonWebTokens() 
      .AddMvcBinders() 
      .EnableAuthorizationEndpoint(Configuration["Authentication:OpenIddict:AuthorizationEndPoint"]) 
      .EnableTokenEndpoint(Configuration["Authentication:OpenIddict:TokenEndPoint"]) 
      .AllowPasswordFlow() 
      .AllowAuthorizationCodeFlow() 
      .AllowImplicitFlow() 
      .AllowRefreshTokenFlow() 
      .DisableHttpsRequirement() 
      .AddEphemeralSigningKey() 
      .SetAccessTokenLifetime(TimeSpan.FromMinutes(2)) 
      .SetRefreshTokenLifetime(TimeSpan.FromMinutes(10)); 
     services.AddSingleton<DbSeeder>(); 
     services.AddMvc(options => 
     { 
      options.SslPort = 44380; 
      options.Filters.Add(new RequireHttpsAttribute()); 
     }); 
    } 

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
     ILoggerFactory loggerFactory, DbSeeder dbSeeder) 
    { 
     loggerFactory.AddConsole(Configuration.GetSection("Logging")); 
     loggerFactory.AddDebug(); 

     app.UseIdentity(); 
     app.UseOAuthValidation(); 

     app.UseGoogleAuthentication(new GoogleOptions() 
     { 
      AutomaticAuthenticate = true, 
      AutomaticChallenge = true, 
      ClientId = Configuration["Authentication:Google:ClientId"], 
      ClientSecret = Configuration["Authentication:Google:ClientSecret"], 
      CallbackPath = "/signin-google", 
      Scope = { "email" } 
     }); 
     app.UseFacebookAuthentication(new FacebookOptions() 
     { 
      AutomaticAuthenticate = true, 
      AutomaticChallenge = true, 
      AppId = Configuration["Authentication:Facebook:AppId"], 
      AppSecret = Configuration["Authentication:Facebook:AppSecret"], 
      CallbackPath = "/signin-facebook", 
      Scope = { "email" } 
     }); 

     app.UseOpenIddict(); 

     app.UseMvcWithDefaultRoute(); 
     try 
     { 
      dbSeeder.SeedAsync().Wait(); 
     } 
     catch (AggregateException ex) 
     { 
      throw new Exception(ex.ToString()); 
     } 
    } 
} 

,这里是的AccountController这是做外部供应商工作:

[Route("api/[controller]")] 
public class AccountsController : BaseController 
{ 
    private readonly IConfiguration _configuration; 

    #region Constructor 

    public AccountsController(ApplicationDbContext context, 
     SignInManager<ApplicationUser> signInManager, 
     UserManager<ApplicationUser> userManager, 
     IConfiguration configuration) 
     : base(context, signInManager, userManager) 
    { 
     _configuration = configuration; 
    } 

    #endregion Constructor 


    #region External Authentication Providers 

    // GET: /api/Accounts/ExternalLogin 
    [HttpGet("ExternalLogin/{provider}")] 
    public IActionResult ExternalLogin(string provider, string returnUrl = null) 
    { 
     switch (provider.ToLower()) 
     { 
      case "facebook": 
      case "google": 
      case "twitter": 
       // Request a redirect to the external login provider. 
       var redirectUrl = Url.Action("ExternalLoginCallback", 
        "Accounts", new { ReturnUrl = returnUrl }); 
       var properties = 
        SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 
       return Challenge(properties, provider); 
      default: 
       return BadRequest(new 
       { 
        Error = $"Provider '{provider}' is not supported." 
       }); 
     } 
    } 

    [HttpGet("ExternalLoginCallBack")] 
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, 
     string remoteError = null) 
    { 
     try 
     { 
      if (remoteError != null) 
      { 
       throw new Exception(remoteError); 
      } 
      var info = await SignInManager.GetExternalLoginInfoAsync(); 
      if (info == null) 
      { 
       throw new Exception("ERROR: No login info available."); 
      } 
      var user = await UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); 
      if (user == null) 
      { 
       var emailKey = 
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; 
       var email = info.Principal.FindFirst(emailKey).Value; 
       user = await UserManager.FindByEmailAsync(email); 
       if (user == null) 
       { 
        var now = DateTime.Now; 
        var idKey = 
         "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; 
        var username = string.Format("{0}{1}", info.LoginProvider, 
         info.Principal.FindFirst(idKey).Value); 
        user = new ApplicationUser 
        { 
         UserName = username, 
         Email = email, 
         CreatedDate = now, 
         LastModifiedDate = now 
        }; 
        await UserManager.CreateAsync(user, "SomePass4ExProvider123+-"); 
        await UserManager.AddToRoleAsync(user, "Registered"); 
        user.EmailConfirmed = true; 
        user.LockoutEnabled = false; 
       } 
       await UserManager.AddLoginAsync(user, info); 
       await DbContext.SaveChangesAsync(); 
      } 
      // create the auth JSON object 
      var auth = new 
      { 
       type = "External", 
       providerName = info.LoginProvider 
      }; 

      // output a <SCRIPT> tag to call a JS function registered into the parent window global scope 
      return Content("<script type=\"text/javascript\">" + 
          "window.opener.externalProviderLogin(" + 
          JsonConvert.SerializeObject(auth) + ");" + 
          "window.close();" + "</script>", "text/html"); 

     } 
     catch (Exception ex) 
     { 
      return BadRequest(new {Error = ex.Message}); 
     } 
    } 

    [HttpPost("Logout")] 
    public IActionResult Logout() 
    { 
     if (HttpContext.User.Identity.IsAuthenticated) 
     { 
      SignInManager.SignOutAsync().Wait(); 
     } 
     return Ok(); 
    } 

    #endregion External Authentication Providers 
} 

和最后ConnectController将生成令牌:

[Route("api/[controller]")] 
public class ConnectController : Controller 
{ 
    private readonly UserManager<ApplicationUser> _userManager; 
    private readonly SignInManager<ApplicationUser> _signInManager; 
    private readonly IConfiguration _configuration; 

    public ConnectController(
     UserManager<ApplicationUser> userManager, 
     SignInManager<ApplicationUser> signInManager, 
     IConfiguration configuration) 
    { 
     _userManager = userManager; 
     _signInManager = signInManager; 
     _configuration = configuration; 
    } 

    [HttpPost("token"), Produces("application/json")] 
    public async Task<IActionResult> Token(OpenIdConnectRequest request) 
    { 
     if (request.IsPasswordGrantType()) 
     { 
      var user = await _userManager.FindByNameAsync(request.Username); 

      #region Authenticate User 

      if (user == null) 
      { 
       // Return bad request if the user doesn't exist 
       return BadRequest(new OpenIdConnectResponse 
       { 
        Error = OpenIdConnectConstants.Errors.InvalidGrant, 
        ErrorDescription = "Invalid username or password" 
       }); 
      } 
      if (!await _signInManager.CanSignInAsync(user) || 
       (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))) 
      { 

       return BadRequest(new OpenIdConnectResponse 
       { 
        Error = OpenIdConnectConstants.Errors.InvalidGrant, 
        ErrorDescription = "The specified user cannot sign in." 
       }); 
      } 

      if (!await _userManager.CheckPasswordAsync(user, request.Password)) 
      { 
       // Return bad request if the password is invalid 
       return BadRequest(new OpenIdConnectResponse 
       { 
        Error = OpenIdConnectConstants.Errors.InvalidGrant, 
        ErrorDescription = "Invalid username or password" 
       }); 
      } 

      // The user is now validated, so reset lockout counts, if necessary 
      if (_userManager.SupportsUserLockout) 
      { 
       await _userManager.ResetAccessFailedCountAsync(user); 
      } 

      #endregion 

      var identity = new ClaimsIdentity(
       OpenIdConnectServerDefaults.AuthenticationScheme, 
       OpenIdConnectConstants.Claims.Name, null); 

      identity.AddClaim(OpenIdConnectConstants.Claims.Subject, 
       user.Id, 
       OpenIdConnectConstants.Destinations.AccessToken); 

      identity.AddClaim(OpenIdConnectConstants.Claims.Name, 
       user.DisplayName??user.UserName, 
       OpenIdConnectConstants.Destinations.AccessToken); 


      var principal = new ClaimsPrincipal(identity); 

      var ticket = await CreateTicketAsync(principal, request, new AuthenticationProperties()); 

      return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); 
     } 
     if (request.IsRefreshTokenGrantType()) 
     { 
      var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
       OpenIdConnectServerDefaults.AuthenticationScheme); 



      var id = info.Principal.FindFirst(OpenIdConnectConstants.Claims.Subject)?.Value; 
      var user = await _userManager.FindByIdAsync(id); 

      if (user == null) 
      { 
       return BadRequest(new OpenIdConnectResponse 
       { 
        Error = OpenIdConnectConstants.Errors.InvalidGrant, 
        ErrorDescription = "The refresh token is no longer valid." 
       }); 
      } 

      if (!await _signInManager.CanSignInAsync(user)) 
      { 
       return BadRequest(new OpenIdConnectResponse 
       { 
        Error = OpenIdConnectConstants.Errors.InvalidGrant, 
        ErrorDescription = "The user is no longer allowed to sign in." 
       }); 
      } 
      var identity = new ClaimsIdentity(
       OpenIdConnectServerDefaults.AuthenticationScheme, 
       OpenIdConnectConstants.Claims.Name, null); 

      identity.AddClaim(OpenIdConnectConstants.Claims.Subject, 
       user.Id, 
       OpenIdConnectConstants.Destinations.AccessToken); 

      identity.AddClaim(OpenIdConnectConstants.Claims.Name, 
       user.DisplayName ?? user.UserName, 
       OpenIdConnectConstants.Destinations.AccessToken); 

      // ... add other claims, if necessary. 

      var principal = new ClaimsPrincipal(identity); 
      var ticket = await CreateTicketAsync(principal,request, info.Properties); 

      // Ask OpenIddict to generate a new token and return an OAuth2 token response. 
      return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); 
     } 

     // Return bad request if the request is not for password grant type 
     return BadRequest(new OpenIdConnectResponse 
     { 
      Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, 
      ErrorDescription = "The specified grant type is not supported." 
     }); 
    } 
    private async Task<AuthenticationTicket> CreateTicketAsync(ClaimsPrincipal principal, 
     OpenIdConnectRequest request, 
     AuthenticationProperties properties = null) 
    { 

     // Create a new authentication ticket holding the user identity. 
     var ticket = new AuthenticationTicket(principal, properties, 
      OpenIdConnectServerDefaults.AuthenticationScheme); 

     if (!request.IsRefreshTokenGrantType()) 
     { 
      //TODO : // Include resources and scopes, **as APPROPRIATE** 
      // Set the list of scopes granted to the client application. 
      // Note: the offline_access scope must be granted 
      // to allow OpenIddict to return a refresh token. 
      ticket.SetScopes(new[] 
      { 
       /* openid: */ OpenIdConnectConstants.Scopes.OpenId, 
       /* email: */ OpenIdConnectConstants.Scopes.Email, 
       /* profile: */ OpenIdConnectConstants.Scopes.Profile, 
       /* offline_access: */ OpenIdConnectConstants.Scopes.OfflineAccess, 
       /* roles: */ OpenIddictConstants.Scopes.Roles 
      }.Intersect(request.GetScopes())); 
     } 
     return ticket; 
    } 

    #region Authorization code, implicit and implicit flows 

    // Note: to support interactive flows like the code flow, 
    // you must provide your own authorization endpoint action: 

    [Authorize, HttpGet("authorize")] 
    public IActionResult Authorize(OpenIdConnectRequest request) 
    { 
     return Ok(); 
    } 

    #endregion 
} 

这是我发送请求的方式:

https://localhost:44380/api/Accounts/ExternalLogin/Google?returnUrl=https://localhost:44380

它成功地返回到我ExternalLoginCallback行动AccountsController但没有JWT令牌正常PasswordGrantFlow发送回用户。

如果可能的话,请将代码发送给我,并且不要将其重定向到其他地方,因为我对服务器端是全新的,而且我之前也完成了我的搜索。

回答

2

尝试authorization code flow sample

你可以调整你的授权端点,如果你想马上你的用户重定向到指定的社会提供商,而不是将其返回到登录页面:

[HttpGet("~/connect/authorize")] 
public async Task<IActionResult> Authorize(OpenIdConnectRequest request) 
{ 
    Debug.Assert(request.IsAuthorizationRequest(), 
     "The OpenIddict binder for ASP.NET Core MVC is not registered. " + 
     "Make sure services.AddOpenIddict().AddMvcBinders() is correctly called."); 

    if (!User.Identity.IsAuthenticated) 
    { 
     // Resolve the optional provider name from the authorization request. 
     // If no provider is specified, call Challenge() to redirect the user 
     // to the login page defined in the ASP.NET Core Identity options. 
     var provider = (string) request.GetParameter("identity_provider"); 
     if (string.IsNullOrEmpty(provider)) 
     { 
      return Challenge(); 
     } 

     // Ensure the specified provider is supported. 
     if (!HttpContext.Authentication.GetAuthenticationSchemes() 
      .Where(description => !string.IsNullOrEmpty(description.DisplayName)) 
      .Any(description => description.AuthenticationScheme == provider)) 
     { 
      return Challenge(); 
     } 

     // When using ASP.NET Core Identity and its default AccountController, 
     // the user must be redirected to the ExternalLoginCallback action 
     // before being redirected back to the authorization endpoint. 
     var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, 
      Url.Action("ExternalLoginCallback", "Account", new 
      { 
       ReturnUrl = Request.PathBase + Request.Path + Request.QueryString 
      })); 

     return Challenge(properties, provider); 
    } 

    // Retrieve the application details from the database. 
    var application = await _applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted); 
    if (application == null) 
    { 
     return View("Error", new ErrorViewModel 
     { 
      Error = OpenIdConnectConstants.Errors.InvalidClient, 
      ErrorDescription = "Details concerning the calling client application cannot be found in the database" 
     }); 
    } 

    // Flow the request_id to allow OpenIddict to restore 
    // the original authorization request from the cache. 
    return View(new AuthorizeViewModel 
    { 
     ApplicationName = application.DisplayName, 
     RequestId = request.RequestId, 
     Scope = request.Scope 
    }); 
} 
+0

我所见到的样品它的客户端是MVC,不是本机移动应用程序,这意味着它与我的问题无关,但无论如何,我只是复制粘贴这些代码表单示例,现在我得到404 NotFound连接/令牌,我检查了每一个没有api段开始的调用都记录为https/localhost:44380/index .html !!!但那些以它开始的是401!真奇怪!我想知道为什么没有教程来创建支持TokenBasedAuth(本地+谷歌)的原生移动应用程序的API,尽管这些日子很常见!并没有真正的文件,只是老无用的不workingsamples! –

+0

@HesamKashefi客户端是MVC应用程序并不会改变代码流的工作方式:协议是相同的。我在发布之前使用代码流示例对此代码进行了测试,因此它可行(尽管您声称)。我想你只是做错了什么。 – Pinpoint

+0

非常感谢,我发现有一个UseWhenExtentionMethod,我没有复制你的实现,但它使用的Microsoft Equivalent不起作用。作为一名MVC客户端只会影响像我这样的初学者,他们不知道如何将其更改为移动应用程序API和所有这些ValidateAntiForgeryTokens,我唯一的解决方案就是将其删除,还有一件事!我不知道如何处理这个接受页面的API方法,以及我应该在哪里坚持新认证的用户,因为我在Accept Action上收到异常,因为它找不到用户 –