1

J'ai une API avec ASP.NET Core qui sera consommée par les applications mobiles natives (actuellement UWP, Android) et j'essaie de mettre en place un moyen pour les clients de s'inscrire et connectez-vous avec un nom d'utilisateur/mot de passe et des fournisseurs externes tels que Google et Facebook. maintenant j'utilise openIddict et mon ExternalProviderCallback doit renvoyer des jetons locaux qui, je suppose, retourne actuellement un cookie! (J'ai copié la plupart des codes de quelque part) et aussi il semble que ce n'est pas le AuthorizationCodeFlow que je suppose que c'est la bonne façon!Obtenir des jetons en vous connectant avec des fournisseurs externes utilisant OpenIddict

maintenant voici ma classe de démarrage

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

et voici AccountController Ce qui fait des fournisseurs externes d'emploi:

[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 
} 

Enfin ConnectController qui généreront Tokens:

[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 
} 

et voici comment j'envoie la demande:

https://localhost:44380/api/Accounts/ExternalLogin/Google?returnUrl=https://localhost:44380

qui retourne avec succès à mon action ExternalLoginCallback en AccountsController mais aucun jeton de JWT sont renvoyés à l'utilisateur comme PasswordGrantFlow normal.

S'il vous plaît, s'il vous plaît envoyez-moi le code ici et ne me redirigez pas ailleurs parce que je suis totalement nouveau sur le côté serveur et j'ai aussi fait mes recherches avant.

Répondre

2

Essayez le authorization code flow sample.

Vous pouvez modifier votre point final d'autorisation si vous souhaitez rediriger immédiatement vos utilisateurs à un fournisseur sociale spécifié au lieu de les retourner à la page de connexion:

[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 
    }); 
} 
+0

j'avais vu cet échantillon, il est client est MVC, et non natif application mobile ce qui signifie que ce n'est pas pertinent pour ma question mais de toute façon je copie simplement ces codes forme échantillon et maintenant je reçois 404 NotFound sur connexion/jeton, j'ai vérifié que chaque appel qui démarre sans segment api est enregistré https/localhost: 44380/index .html !!! mais ceux qui commencent avec ça reçoivent 401! c'est étrange! Je me demande pourquoi il n'y a pas de tutoriel pour créer une API pour les applications Native Mobile qui supporte TokenBasedAuth (local + google) bien que ce soit très courant ces jours-ci! et pas de vraie documentation, juste de vieux inutiles pas des échantillons! –

+0

@HesamKashefi le fait que le client soit une application MVC ne change rien au fonctionnement du flux de code: le protocole est le même. J'ai testé ce code avec l'échantillon de flux de code avant de le poster, donc ça marche (malgré ce que vous prétendez). Je suppose que vous faites juste quelque chose de mal. – Pinpoint

+0

merci beaucoup, j'ai découvert qu'il y a un UseWhenExtentionMethod que je n'ai pas copié votre mise en œuvre mais il utilisait l'équivalent de Microsoft qui n'a pas fonctionné. être un MVC Client a seulement un impact sur un débutant comme moi qui ne sait pas comment le changer en API pour les applications mobiles et tous ces ValidateAntiForgeryTokens dont ma seule solution est de les supprimer, et encore une chose! Je ne sais pas comment traiter cette page Accepter dans l'API et où dois-je persister le nouvel utilisateur authentifié parce que je reçois une exception sur Accepter l'action parce qu'il ne peut pas trouver l'utilisateur –