我有一个使用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发送回用户。
如果可能的话,请将代码发送给我,并且不要将其重定向到其他地方,因为我对服务器端是全新的,而且我之前也完成了我的搜索。
我所见到的样品它的客户端是MVC,不是本机移动应用程序,这意味着它与我的问题无关,但无论如何,我只是复制粘贴这些代码表单示例,现在我得到404 NotFound连接/令牌,我检查了每一个没有api段开始的调用都记录为https/localhost:44380/index .html !!!但那些以它开始的是401!真奇怪!我想知道为什么没有教程来创建支持TokenBasedAuth(本地+谷歌)的原生移动应用程序的API,尽管这些日子很常见!并没有真正的文件,只是老无用的不workingsamples! –
@HesamKashefi客户端是MVC应用程序并不会改变代码流的工作方式:协议是相同的。我在发布之前使用代码流示例对此代码进行了测试,因此它可行(尽管您声称)。我想你只是做错了什么。 – Pinpoint
非常感谢,我发现有一个UseWhenExtentionMethod,我没有复制你的实现,但它使用的Microsoft Equivalent不起作用。作为一名MVC客户端只会影响像我这样的初学者,他们不知道如何将其更改为移动应用程序API和所有这些ValidateAntiForgeryTokens,我唯一的解决方案就是将其删除,还有一件事!我不知道如何处理这个接受页面的API方法,以及我应该在哪里坚持新认证的用户,因为我在Accept Action上收到异常,因为它找不到用户 –