我有一個使用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上收到異常,因爲它找不到用戶 –