Ma question est un peu complexe, alors restez avec moi alors que j'essaie de bien expliquer ce avec quoi je me bats.
Objectif
Avoir un site Web ASP.NET qui permet aux utilisateurs de s'inscrire et de se connecter via un nom d'utilisateur/mot de passe ou un réseau social (Facebook, Twitter, Google, etc.) qui dispose également d'une API. Cette API doit être verrouillée avec [Authorize]
. L'API doit pouvoir être accessible par les clients mobiles (Android, iOS, etc.) qui peuvent être connectés via un nom d'utilisateur/mot de passe ou social (Facebook, Twitter, Google, etc.).
Contexte
J'ai donc fait des sites qui peuvent faire une ou deux choses de mon objectif mais pas tous ensemble. Il existe d'excellents exemples en ligne et des exemples intégrés dans les projets VS qui montrent comment permettre à l'utilisateur de s'inscrire et de se connecter via des applications sociales, mais ils ne sont destinés qu'au site Web et non aux mobiles. J'ai créé un site Web pour lequel une application Android Android utilise un nom d'utilisateur/mot de passe pour s'authentifier avec cette API, mais rien avec OAuth ou informations d'identification sociales).
J'ai commencé à utiliser cette page comme référence, mais je ne sais pas comment prendre cela et le faire fonctionner pour la connexion à mon site Web et pour la connexion à mon application mobile.
Ce gars rend le son si facile mais n'affiche aucun code pour cela.
Question
Existe-t-il un didacticiel ou un exemple GitHub quelque part qui peut me permettre d'atteindre mon objectif? Je veux essentiellement un site Web où les gens peuvent enregistrer un nom d'utilisateur/mot de passe ou utiliser leur compte social ET laisser également l'utilisateur faire la même chose (s'inscrire et se connecter) via un appareil mobile. L'appareil mobile utilisera simplement l'API pour pousser/tirer des données, mais je ne sais pas comment incorporer les connexions sociales à mon API. Je suppose que je dois utiliser OAuth et suivre cette voie, mais je ne trouve aucun bon exemple qui montre comment le faire pour le Web et le mobile.
Ou peut-être que la bonne solution consiste à faire en sorte que la page Web soit entièrement authentifiée par les cookies et que l'API soit un "site Web" distinct et soit entièrement authentifié par jeton et qu'ils soient tous deux liés à la même base de données?
J'ai réussi cette tâche avec ma propre application ASP.NET MVC en utilisant ASP.NET Identity, mais j'ai ensuite rencontré le problème que vous mentionnez: j'ai besoin que cela fonctionne également en utilisant l'API Web pour que mon application mobile puisse interagir nativement.
Je ne connaissais pas l'article que vous avez lié, mais après l'avoir lu, j'ai remarqué qu'une grande partie du travail et du code n'est pas nécessaire et complique les fonctionnalités qui existent déjà dans ASP.NET Identity.
Voici mes recommandations, et je suppose que vous utilisez ASP.NET Identity V2 qui est équivalent aux packages entourant MVC5 (pas le nouveau MVC6 vNext). Cela permettra à votre site Web ET à votre application mobile via l'API de s'authentifier à la fois avec une connexion locale (nom d'utilisateur/mot de passe) et un fournisseur externe OAuth) à la fois à partir des vues Web MVC sur votre site Web et via les appels d'API Web depuis votre application mobile:
Étape 1. Lors de la création de votre projet, assurez-vous que les packages requis pour MVC et API Web sont inclus. Dans la boîte de dialogue Sélection de projet ASP.NET, vous aurez la possibilité de sélectionner les cases à cocher, assurez-vous que MVC et l'API Web sont tous deux cochés. Si vous ne l'avez pas déjà fait lorsque vous avez créé votre projet, je recommanderais de créer un nouveau projet et de migrer votre code existant plutôt que de rechercher et d'ajouter manuellement les dépendances et le code de modèle.
Étape 2. Dans votre fichier Startup.Auth.cs, vous aurez besoin d'un code pour dire à OWIN d'utiliser l'authentification par cookie, d'autoriser les cookies de connexion externe et de prendre en charge OAuth jetons au porteur (c'est ainsi que l'API Web Les appels seront authentifiés). Voici des extraits pertinents de la base de code de mon projet de travail:
Startup.Auth.cs
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/account/externallogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
//AllowInsecureHttp = false
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
app.UseTwitterAuthentication(
consumerKey: "Twitter API Key",
consumerSecret: "Twitter API Secret");
app.UseFacebookAuthentication(
appId: "Facebook AppId",
appSecret: "Facebook AppSecret");
Dans le code ci-dessus, je soutiens actuellement Twitter et Facebook en tant que fournisseurs d'authentification externes; cependant, vous pouvez ajouter des fournisseurs externes supplémentaires avec les appels app.UserXYZProvider et des bibliothèques supplémentaires et ils se connecteront et joueront avec le code que je fournis ici.
Étape 3. Dans votre fichier WebApiConfig.cs, vous devez configurer la configuration HttpConfiguration pour supprimer l'authentification par défaut de l'hôte et prendre en charge OAuth jetons de support. Pour expliquer, cela indique à votre application de différencier les types d'authentification entre MVC et Web API, de cette façon, vous pouvez utiliser le flux de cookies typique pour le site Web, tandis que votre application acceptera les jetons au porteur sous la forme OAuth de l'API Web sans se plaindre ou d'autres problèmes.
WebApiConfig.cs
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Étape 4. Vous avez besoin d'un AccountController (ou d'un contrôleur équivalent) pour MVC et l'API Web. Dans mon projet, j'ai deux fichiers AccountController, un contrôleur MVC héritant de la classe Controller de base et un autre AccountController héritant d'ApiController qui se trouve dans un espace de noms Controllers.API pour garder les choses propres. J'utilise le modèle de code AccountController standard des projets Web API et MVC. Voici la version API du Account Controller:
AccountController.cs (espace de noms Controllers.API)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Disco.Models.API;
using Disco.Providers;
using Disco.Results;
using Schloss.AspNet.Identity.Neo4j;
using Disco.Results.API;
namespace Disco.Controllers.API
{
[Authorize]
[RoutePrefix("api/account")]
public class AccountController : ApiController
{
private const string LocalLoginProvider = "Local";
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
// GET account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("userinfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
// POST account/Logout
[Route("logout")]
public IHttpActionResult Logout()
{
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return Ok();
}
// GET account/ManageInfo?returnUrl=%2F&generateState=true
[Route("manageinfo")]
public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
{
IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (UserLoginInfo linkedAccount in await UserManager.GetLoginsAsync(User.Identity.GetUserId()))
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
// POST account/ChangePassword
[Route("changepassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/SetPassword
[Route("setpassword")]
public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/AddExternalLogin
[Route("addexternallogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
if (ticket == null || ticket.Identity == null || (ticket.Properties != null
&& ticket.Properties.ExpiresUtc.HasValue
&& ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
{
return BadRequest("External login failure.");
}
ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
if (externalData == null)
{
return BadRequest("The external login is already associated with an account.");
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RemoveLogin
[Route("removelogin")]
public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result;
if (model.LoginProvider == LocalLoginProvider)
{
result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
}
else
{
result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(model.LoginProvider, model.ProviderKey));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// GET account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("externallogin", Name = "ExternalLoginAPI")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
// GET account/ExternalLogins?returnUrl=%2F&generateState=true
[AllowAnonymous]
[Route("externallogins")]
public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationDescription description in descriptions)
{
ExternalLoginViewModel login = new ExternalLoginViewModel
{
Name = description.Caption,
Url = Url.Route("ExternalLogin", new
{
provider = description.AuthenticationType,
response_type = "token",
client_id = Startup.PublicClientId,
redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
state = state
}),
State = state
};
logins.Add(login);
}
return logins;
}
// POST account/Register
[AllowAnonymous]
[Route("register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("registerexternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing && _userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
base.Dispose(disposing);
}
#region Helpers
private IAuthenticationManager Authentication
{
get { return Request.GetOwinContext().Authentication; }
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string 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;
}
private class ExternalLoginData
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string UserName { get; set; }
public IList<Claim> GetClaims()
{
IList<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
if (UserName != null)
{
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
}
return claims;
}
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
|| String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
}
private static class RandomOAuthStateGenerator
{
private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
public static string Generate(int strengthInBits)
{
const int bitsPerByte = 8;
if (strengthInBits % bitsPerByte != 0)
{
throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits");
}
int strengthInBytes = strengthInBits / bitsPerByte;
byte[] data = new byte[strengthInBytes];
_random.GetBytes(data);
return HttpServerUtility.UrlTokenEncode(data);
}
}
#endregion
}
}
Étape 5. Vous devez également créer un ApplicationOAuthProvider afin que le serveur puisse générer et valider des jetons OAuth. Ceci est fourni dans l'exemple de projet WebAPI. Voici ma version du fichier:
ApplicationOAuthProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Butler.Models;
using Schloss.AspNet.Identity.Neo4j;
namespace Butler.Providers
{
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
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);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
//Uri expectedRootUri = new Uri(context.Request.Uri, "/");
//if (expectedRootUri.AbsoluteUri == context.RedirectUri)
//{
context.Validated();
//}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
}
Le ChallengeResult est également inclus, que le bras API Web de votre application utilisera pour gérer les défis fournis par les fournisseurs de connexion externes pour authentifier votre utilisateur:
ChallengeResult.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
namespace Butler.Results
{
public class ChallengeResult : IHttpActionResult
{
public ChallengeResult(string loginProvider, ApiController controller)
{
LoginProvider = loginProvider;
Request = controller.Request;
}
public string LoginProvider { get; set; }
public HttpRequestMessage Request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
Request.GetOwinContext().Authentication.Challenge(LoginProvider);
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
Avec cet ensemble de code, vous pourrez HTTP GET et HTTP POST les routes sur la version API du AccountController pour enregistrer un utilisateur, connectez-vous en utilisant un nom d'utilisateur et un mot de passe pour recevoir un jeton Bearer, ajoutez/supprimez des connexions externes, gérez les connexions externes et, plus important encore, pour votre problème, authentifiez-vous en transmettant un jeton de connexion externe en échange d'un jeton porteur OAuth pour votre application).
Vous voudrez peut-être jeter un œil à cette série d'articles pour voir si elle couvre votre objectif:
Authentification basée sur les jetons utilisant ASP.NET Web API 2, Owin et Identity par Taiseer Joudeh (qui répond également fréquemment aux questions sur SO)
Les articles concernent la création d'un service d'authentification basé sur des jetons à l'aide d'OWIN et l'une des parties couvre l'utilisation de connexions externes (telles que Facebook et Google+). Les exemples sont principalement centrés sur une application Web en tant que consommateur du service Web, mais cela devrait également fonctionner sur les applications mobiles. Les articles ont un projet GitHub associé et une section de commentaires très active, où pratiquement aucune question n'est restée sans réponse.
J'espère que cela peut vous conduire à votre objectif.
J'ajoute ceci comme une réponse séparée à la deuxième partie de votre question pour dire que OUI vous pouvez avoir deux projets distincts liés à la même base de données et simplement avoir le projet de site Web MVC/Web Forms utilise toute l'authentification par cookie, puis possède un projet d'API Web distinct qui est entièrement authentifié par jeton.
Dans ma réponse plus longue avec des exemples de code source, ce que j'ai essentiellement fait est de combiner les deux projets séparés en un seul projet afin d'éviter le code modèle redondant et le code contrôleur. Dans mon cas, cela avait plus de sens pour moi; Cependant, je suis enclin à dire que c'est à vos préférences personnelles et aux besoins de votre projet de dicter s'il faut maintenir deux projets distincts, un site Web et un point de terminaison d'API Web, ou les combiner.
ASP.NET a été conçu pour être très flexible et plug and play en tant que middleware et je peux attester que mon projet a existé et a fonctionné exactement comme prévu avec le code dans deux projets distincts et maintenant comme un projet combiné.