Asp.net

帶有 Facebook 訪問令牌的 MVC 5 Web API 到 RegisterExternal,無需 Cookie

  • May 29, 2019

設置: 只有 Web API 的新 MVC5 項目。添加了 Facebook AppId 和 Secret。我可以通過傳入使用者名和密碼從端點

獲取我的 Web API 的令牌。Token然後使用該令牌進行進一步呼叫。

我想在 iOS 應用程序中藉助 Facebook SDK 註冊新使用者。我正在使用 Facebook SDK 獲取訪問令牌。(假設此時,我有一個訪問令牌)。

我知道的下一件事是通過在標頭中api/Account/RegisterExternal傳遞這個令牌來呼叫端點,但這會導致 500 伺服器錯誤。Authorization``Bearer [Access Token]

我想我知道原因,Cookie 不見了。我用 Fidler 的 cookie 打了同樣的電話,它奏效了。(通過訪問ExternalLogins端點提供的 URL 接收 Cookie)。await Authentication.GetExternalLoginInfoAsync();由於RegisterExternal 操作中缺少 cookie,因此返回 null。

// POST api/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();
}

我不想對我的 Web API 進行 3 次呼叫以請求外部登錄,然後轉到該 URL 並在 Web 瀏覽器中對 Facebook 訪問令牌進行身份驗證,然後使用我需要收集的訪問令牌和 Cookie 呼叫 RegisterExternal 端點在這些呼叫之間。

正如我所說,除了 Facebook Id,我沒有更改模板中的任何內容。程式碼仍然如下。

public partial class Startup
{
   public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

   public static string PublicClientId { get; private set; }

   // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
   public void ConfigureAuth(IAppBuilder app)
   {
       // Configure the db context and user manager to use a single instance per request
       app.CreatePerOwinContext(ApplicationDbContext.Create);
       app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

       // 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.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 = true
       };

       // Enable the application to use bearer tokens to authenticate users
       app.UseOAuthBearerTokens(OAuthOptions);

       app.UseFacebookAuthentication(
           appId: "xxxxxxxxxxxxxxx",
           appSecret: "xxxxxxxxxxxxxxxxxxxxxxxx");
   }
}

據我所知,Web API 不需要 Cookie,當我從Token端點獲得本地令牌時這似乎是正確的,但是為什麼在執行ExternalRegister WebApiConfig 類時它首先需要 Cookie 看起來像這樣並且不應該config.SuppressDefaultHostAuthentication();避免任何 Cookie 需求

public static class WebApiConfig
{
   public static void Register(HttpConfiguration config)
   {
       // Web API configuration and services
       // Configure Web API to use only bearer token authentication.
       config.SuppressDefaultHostAuthentication();
       config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

       // Web API routes
       config.MapHttpAttributeRoutes();

       config.Routes.MapHttpRoute(
           name: "DefaultApi",
           routeTemplate: "api/{controller}/{id}",
           defaults: new { id = RouteParameter.Optional }
       );
   }
}

我不知道我是否錯過了這裡的重點。我的意圖是不需要在本機 iOS 應用程序中使用 Web 瀏覽器來獲取令牌。那是 Facebook SDK 獲取訪問令牌並使用該呼叫RegisterExternal獲取本地令牌並創建該使用者身份。

我做了功課,我堅持這個想法。想法讚賞!

我誤認為它接受帶有 cookie 的社交令牌!它不直接接受任何外部令牌。

問題是.. MVC 5 正在為我們處理一切,即從社交媒體收集令牌並驗證/處理它。之後,它會生成一個本地令牌。

RegisterExternal方法還需要維護cookies,解決方案不需要。

我寫了一篇博文,會詳細解釋。在下面添加了直截了當的答案。我的目標是讓它融合併成為預設 MVC Web API 的登錄/註冊流程不可或缺的一部分,以確保它易於理解。

在以下解決方案之後,授權屬性必須如下所示才能工作,否則您將獲得未經授權的響應。

[Authorize]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalBearer)]
[HostAuthentication(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie)]

如果ExternalBearer您希望僅允許令牌使用 API,則使用,ApplicationCookie如果您希望僅允許 Logged cookie 使用 API,即來自網站的 API,請使用。如果您想為兩者都允許 API,請同時使用兩者。

將此操作添加到AccountController.cs

// POST api/Account/RegisterExternalToken
[OverrideAuthentication]
[AllowAnonymous]
[Route("RegisterExternalToken")]
public async Task<IHttpActionResult> RegisterExternalToken(RegisterExternalTokenBindingModel model)
{
   if (!ModelState.IsValid)
   {
       return BadRequest(ModelState);
   }

   ExternalLoginData externalLogin = await ExternalLoginData.FromToken(model.Provider, model.Token);

   if (externalLogin == null)
   {
       return InternalServerError();
   }

   if (externalLogin.LoginProvider != model.Provider)
   {
       Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
       return InternalServerError();
   }

   ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
       externalLogin.ProviderKey));

   bool hasRegistered = user != null;
   ClaimsIdentity identity = null;
   IdentityResult result;

   if (hasRegistered)
   {
       identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
       IEnumerable<Claim> claims = externalLogin.GetClaims();
       identity.AddClaims(claims);
       Authentication.SignIn(identity);
   }
   else
   {
       user = new ApplicationUser() { Id = Guid.NewGuid().ToString(), UserName = model.Email, Email = model.Email };

       result = await UserManager.CreateAsync(user);
       if (!result.Succeeded)
       {
           return GetErrorResult(result);
       }

       var info = new ExternalLoginInfo()
       {
           DefaultUserName = model.Email,
           Login = new UserLoginInfo(model.Provider, externalLogin.ProviderKey)
       };

       result = await UserManager.AddLoginAsync(user.Id, info.Login);
       if (!result.Succeeded)
       {
           return GetErrorResult(result);
       }

       identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
       IEnumerable<Claim> claims = externalLogin.GetClaims();
       identity.AddClaims(claims);
       Authentication.SignIn(identity);
   }

   AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
   var currentUtc = new Microsoft.Owin.Infrastructure.SystemClock().UtcNow;
   ticket.Properties.IssuedUtc = currentUtc;
   ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromDays(365));
   var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
   Request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

   // Create the response building a JSON object that mimics exactly the one issued by the default /Token endpoint
   JObject token = new JObject(
       new JProperty("userName", user.UserName),
       new JProperty("id", user.Id),
       new JProperty("access_token", accessToken),
       new JProperty("token_type", "bearer"),
       new JProperty("expires_in", TimeSpan.FromDays(365).TotalSeconds.ToString()),
       new JProperty(".issued", currentUtc.ToString("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'")),
       new JProperty(".expires", currentUtc.Add(TimeSpan.FromDays(365)).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
   );
   return Ok(token);
}

將此輔助方法添加到ExternalLoginData輔助區域中的類中AccountController.cs

public static async Task<ExternalLoginData> FromToken(string provider, string accessToken)
{
   string verifyTokenEndPoint = "", verifyAppEndpoint = "";

   if (provider == "Facebook")
   {
       verifyTokenEndPoint = string.Format("https://graph.facebook.com/me?access_token={0}", accessToken);
       verifyAppEndpoint = string.Format("https://graph.facebook.com/app?access_token={0}", accessToken);
   }
   else if (provider == "Google")
   {
       return null; // not implemented yet
       //verifyTokenEndPoint = string.Format("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={0}", accessToken);
   }
   else
   {
       return null;
   }

   HttpClient client = new HttpClient();
   Uri uri = new Uri(verifyTokenEndPoint);
   HttpResponseMessage response = await client.GetAsync(uri);
   ClaimsIdentity identity = null;
   if (response.IsSuccessStatusCode)
   {
       string content = await response.Content.ReadAsStringAsync();
       dynamic iObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);

       uri = new Uri(verifyAppEndpoint);
       response = await client.GetAsync(uri);
       content = await response.Content.ReadAsStringAsync();
       dynamic appObj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);

       identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);

       if (provider == "Facebook")
       {
           if (appObj["id"] != Startup.facebookAuthOptions.AppId)
           {
               return null;
           }

           identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, iObj["id"].ToString(), ClaimValueTypes.String, "Facebook", "Facebook"));

       }
       else if (provider == "Google")
       {
           //not implemented yet
       }
   }

   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)
   };
}

最後,RegisterExternalTokenBindingModel被動作使用的。

public class RegisterExternalTokenBindingModel
{
   [Required]
   [Display(Name = "Email")]
   public string Email { get; set; }

   [Required]
   [Display(Name = "Token")]
   public string Token { get; set; }

   [Required]
   [Display(Name = "Provider")]
   public string Provider { get; set; }
}

是的,我們在註冊時將電子郵件與令牌詳細資訊一起傳遞,這不會導致您在使用 Twitter 時更改程式碼,因為 Twitter 不提供使用者電子郵件。我們驗證令牌來自我們的應用程序。一旦電子郵件註冊、被黑或其他人的令牌不能用於更改電子郵件或獲取該電子郵件的本地令牌,因為無論發送的電子郵件如何,它都會始終為傳遞的社交令牌的實際使用者返回本地令牌。

RegisterExternalToken端點以兩種方式獲取令牌,即註冊使用者並發送本地令牌,或者如果使用者已經註冊,則發送令牌。

引用自:https://stackoverflow.com/questions/27754171