我有一个带有ASP.NET Core的API,它将由本机移动应用程序(目前是UWP,Android)使用,我正在尝试实现一种方式,客户端可以注册并使用用户名/密码和外部登录谷歌和Facebook等提供商。现在我正在使用openIddict
而我的ExternalProviderCallback
必须返回我认为当前返回cookie的本地令牌! (我已从某处复制了大部分代码)而且似乎不是AuthorizationCodeFlow,我认为这是正确的方法!
现在这是我的启动课
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
成功返回到AccountsController中的ExternalLoginCallback Action但没有JWT令牌像普通的PasswordGrantFlow一样发回给用户。
如果可以在这里发送代码,请不要将我的代码重定向到其他地方,因为我对服务器端全新,并且我之前已经完成了我的搜索。
答案 0 :(得分: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
});
}