Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

asp .net core + OpenIdDict + angular 2 + social login #1355

Closed
AlexOliinyk1 opened this issue Aug 10, 2017 · 5 comments
Closed

asp .net core + OpenIdDict + angular 2 + social login #1355

AlexOliinyk1 opened this issue Aug 10, 2017 · 5 comments

Comments

@AlexOliinyk1
Copy link

AlexOliinyk1 commented Aug 10, 2017

I have project which use "asp .net core 1.1" + "OpenIdDict" + "angular 2" + "social login (twitter and Facebook)"

I have some part of code already is finished and it seems just last step.
I need to find way to authorize my client angular 2 app.

My front-end through the back-end refers to Facebook, but there goes then a full redirect to Facebook and then he makes a request on the callback to the back-end, but the front-end does not know anything about it.
AuthorizationExternalController.cs

public class AuthorizationExternalController : BaseController
  {
    private readonly IAccountManager _accountManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<AuthorizationExternalController> _logger;

    public AuthorizationExternalController(
      SignInManager<ApplicationUser> signInManager,
      UserManager<ApplicationUser> userManager,
      IAccountManager accountManager,
      ILogger<AuthorizationExternalController> logger)
    {
      this._signInManager = signInManager;
      this._userManager = userManager;
      this._accountManager = accountManager;
      this._logger = logger;
    }

    [HttpGet("api/account/ExternalLogin")]
    public IActionResult ExternalLogin(string provider, string returnUrl = null)
    {
      var redirectUrl = Url.Link("externalLoginCallback", new { ReturnUrl = returnUrl });// Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
      //var redirectUrl = Url.Link("externalLoginCallback", new { ReturnUrl = "/connect/authorize" });// Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
      //var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl });
      var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
      return Challenge(properties, provider);
    }

    [HttpGet("account/ExternalLoginCallback", Name = "externalLoginCallback")]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
      try
      {
        if (remoteError != null)
        {
          return RedirectStatus(ExternalLoginStatus.Error);
        }

        ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
        if (info == null)
        {
          return RedirectStatus(ExternalLoginStatus.Invalid);
        }

        var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
        var email = info.Principal.FindFirst(ClaimTypes.Email).Value;

        if (user == null)
        {
          //var name = info.Principal.FindFirst(ClaimTypes.Name).Value;
          user = await _userManager.FindByEmailAsync(email);
          if (user == null)
          {
            //var username = String.Format("{0}{1}", info.LoginProvider, info.Principal.FindFirst(ClaimTypes.Name).Value);
            user = new ApplicationUser()
            {
              UserName = email,
              Email = email
            };

            await _userManager.CreateAsync(user, "Pass4External");
            await _userManager.AddToRoleAsync(user, "user");

            user.EmailConfirmed = true;
            user.LockoutEnabled = false;
          }

          var result = await _userManager.AddLoginAsync(user, info);
        }

        await _signInManager.SignInAsync(user, false);


        var auth = new
        {
          type = "External",
          providerName = info.LoginProvider
        };

        return Content(
        "<script type='text/javascript'>"
        + "window.opener.externalProviderLogin(" + JsonConvert.SerializeObject(auth) + ");"
        + "window.close();"
        + "</script>",
        "text/html");
      }
      catch (Exception exc)
      {
        return BadRequest(new { Error = exc.Message });
      }

    }

    [HttpPost("api/account/logout")]
    public IActionResult Logout()
    {
      if (User.Identity.IsAuthenticated)
      {
        _signInManager.SignOutAsync().Wait();
      }
      return Ok();
    }

    [HttpPost("api/account/ExternalLoginRegistration")]
    public async Task<IActionResult> ExternalLoginConfirmation([FromBody] ExternalLoginConfirmationViewModel model)
    {
      if (ModelState.IsValid)
      {
        // Get the information about the user from the external login provider
        ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
        if (info == null)
        {
          return BadRequest("Registraion failure");
          //return View("ExternalLoginFailure");
        }

        var user = await _accountManager.GetUserByEmailAsync(model.Email);
        if (user == null)
        {
          user = new ApplicationUser { UserName = model.Email, Email = model.Email };
          var createResult = await _userManager.CreateAsync(user);
          if (!createResult.Succeeded)
          {
            AddErrors(createResult);
          }
          user = await _accountManager.GetUserByEmailAsync(model.Email);
        }

        var result = await _userManager.AddLoginAsync(user, info);
        if (result.Succeeded)
        {
          await _signInManager.SignInAsync(user, isPersistent: false);
          _logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);

          string finishUrl = string.IsNullOrEmpty(model.ReturnUrl) ? "/" : model.ReturnUrl;
          return Ok(finishUrl);
        }

        AddErrors(result);
      }

      string error = string.Empty;
      foreach (string key in ModelState.Keys)
      {
        if (ModelState[key].Errors.Count > 0)
        {
          error = ModelState[key].Errors[0].ErrorMessage;
          break;
        }
      }
      return BadRequest(error);
    }

    private IActionResult RedirectStatus(ExternalLoginStatus status, string loginProvider = null, string email = null, string returnUrl = null)
    {
      string url = $"/login?externalLoginStatus={(int)status}";
      url += string.IsNullOrEmpty(loginProvider) ? string.Empty : $"&loginProvider={loginProvider}";
      url += string.IsNullOrEmpty(email) ? string.Empty : $"&email={email}";
      url += string.IsNullOrEmpty(returnUrl) ? string.Empty : $"&returnUrl={returnUrl}";

      return Redirect(url);
    }

  }

Startup.cs

public class Startup
  {
    public IConfigurationRoot Configuration { get; }
    private IHostingEnvironment _hostingEnvironment;

    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)
          .AddEnvironmentVariables();

      if(env.IsDevelopment())
      {
        builder.AddUserSecrets<Startup>();
      }

      Configuration = builder.Build();
      _hostingEnvironment = env;
    }

    public void ConfigureServices(IServiceCollection services)
    {
         services.AddDbContext<ApplicationDbContext>(options => {
        options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"], b => b.MigrationsAssembly("SoleLondon.Domain"));
        options.UseOpenIddict();
      });

      // add identity
      services.AddIdentity<ApplicationUser, ApplicationRole>()
          .AddEntityFrameworkStores<ApplicationDbContext>()
          .AddDefaultTokenProviders();

      services.Configure<IdentityOptions>(options => {
        // User settings
        options.User.RequireUniqueEmail = true;

        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;

        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
      });

      services.AddOpenIddict(options => {
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
        options.AddMvcBinders();
        options.EnableTokenEndpoint("/connect/token");
        options.AllowPasswordFlow();
        options.AllowRefreshTokenFlow();
        options.DisableHttpsRequirement();
        options.AddSigningKey(new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(Configuration["STSKey"])));
      });
 
      // Add framework services.
      services.AddMvc().AddJsonOptions(options => {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
      });

          services.AddScoped<IUnitOfWork, UnitOfWork>();
      services.AddScoped<IAccountManager, AccountManager>();

      // Auth Policies
      services.AddSingleton<IAuthorizationHandler, ViewUserByIdHandler>();
      services.AddSingleton<IAuthorizationHandler, ManageUserByIdHandler>();
      services.AddSingleton<IAuthorizationHandler, ViewRoleByNameHandler>();
      services.AddSingleton<IAuthorizationHandler, AssignRolesHandler>();

      // DB Creation and Seeding
      services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();

      ConfigureStripeServices(services);
      ConfigureAppRepositories(services);
      ConfigureAppServices(services);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IDatabaseInitializer databaseInitializer, ILoggerFactory loggerFactory)
    {
  
      Utilities.ConfigureLogger(loggerFactory);
 
      app.UseIdentity();
      app.UseTwitterAuthentication(new TwitterOptions() {
        ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"],
        ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"],
      });
      app.UseFacebookAuthentication(new FacebookOptions {
        AppId = Configuration["Authentication:Facebook:AppId"],
        AppSecret = Configuration["Authentication:Facebook:AppSecret"]
      });

      app.UseOAuthValidation();
      app.UseOpenIddict();

      app.UseExceptionHandler(builder => {
        builder.Run(async context => {
          context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
          context.Response.ContentType = MediaTypeNames.ApplicationJson;

          var error = context.Features.Get<IExceptionHandlerFeature>();

          if(error != null)
          {
            string errorMsg = JsonConvert.SerializeObject(new { error = error.Error.Message });
            await context.Response.WriteAsync(errorMsg).ConfigureAwait(false);
          }
        });
      });

      StripeConfiguration.SetApiKey(Configuration.GetSection("Stripe")["SecretKey"]);

      //  UseMvc must be last
      app.Use(async (context, next) => {
        await next();
        if(context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
        {
          context.Request.Path = "/index.html";
          await next();
        }
      })
        .UseDefaultFiles(new DefaultFilesOptions { DefaultFileNames = new List<string> { "index.html" } })
        .UseStaticFiles()        
        .UseMvc(routes => {
          routes.MapRoute("signin-external", "{controller=AuthorizationExternal}/{action=ExternalLoginCallback}");
       });

      //  Try create default db entities
      try
      {
        databaseInitializer.SeedAsync().Wait();
      }
      catch(Exception ex)
      {
        Utilities.CreateLogger<Startup>().LogCritical(LoggingEvents.InitDatabase, ex, LoggingEvents.InitDatabase.Name);
        throw;
      }

    }
   
  }

AuthorizationController.cs

public class AuthorizationController : Controller
  {
    private readonly IOptions<IdentityOptions> _identityOptions;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public AuthorizationController(
        IOptions<IdentityOptions> identityOptions,
        SignInManager<ApplicationUser> signInManager,
        UserManager<ApplicationUser> userManager)
    {
      _identityOptions = identityOptions;
      _signInManager = signInManager;
      _userManager = userManager;
    }

    [HttpGet("~/connect/authorize")]
    public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
    {
      if(!User.Identity.IsAuthenticated)
      {
        // If the client application request promptless authentication,
        // return an error indicating that the user is not logged in.
        if(request.HasPrompt(OpenIdConnectConstants.Prompts.None))
        {
          var properties = new AuthenticationProperties(new Dictionary<string, string> {
            [OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired,
            [OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in."
          });

          // Ask OpenIddict to return a login_required error to the client application.
          return Forbid(properties, OpenIdConnectServerDefaults.AuthenticationScheme);
        }

        return Challenge();
      }

      // Retrieve the profile of the logged in user.
      var user = await _userManager.GetUserAsync(User);
      if(user == null)
      {
        return View("Error", new {
          Error = OpenIdConnectConstants.Errors.ServerError,
          ErrorDescription = "An internal error has occurred"
        });
      }

      // Create a new authentication ticket.
      var ticket = await CreateTicketAsync(request, user);

      // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
      return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
    }



    [HttpPost("~/connect/token")]
    [Produces("application/json")]
    public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
    {
      if(request.IsPasswordGrantType())
      {
        var user = await _userManager.FindByEmailAsync(request.Username) ?? await _userManager.FindByNameAsync(request.Username);
        if(user == null)
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "Please check that your email and password is correct"
          });
        }

        // Ensure the user is allowed to sign in.
        if(!await _signInManager.CanSignInAsync(user))
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The specified user is not allowed to sign in"
          });
        }
 

        // Reject the token request if two-factor authentication has been enabled by the user.
        if(_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The specified user is not allowed to sign in"
          });
        }

        // Ensure the user is not already locked out.
        if(_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The specified user account has been suspended"
          });
        }

        // Ensure the user is enabled.
        if(!user.IsEnabled)
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The specified user account is disabled"
          });
        }

        // Ensure the password is valid.
        if(!await _userManager.CheckPasswordAsync(user, request.Password))
        {
          if(_userManager.SupportsUserLockout)
          {
            await _userManager.AccessFailedAsync(user);
          }

          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "Please check that your email and password is correct"
          });
        }

        if(_userManager.SupportsUserLockout)
        {
          await _userManager.ResetAccessFailedCountAsync(user);
        }

        // Create a new authentication ticket.
        var ticket = await CreateTicketAsync(request, user);

        return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
      }
      else if(request.IsRefreshTokenGrantType())
      {
        // Retrieve the claims principal stored in the refresh token.
        var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
            OpenIdConnectServerDefaults.AuthenticationScheme);

        // Retrieve the user profile corresponding to the refresh token.
        // Note: if you want to automatically invalidate the refresh token
        // when the user password/roles change, use the following line instead:
        // var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
        var user = await _userManager.GetUserAsync(info.Principal);
        if(user == null)
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The refresh token is no longer valid"
          });
        }

        // Ensure the user is still allowed to sign in.
        if(!await _signInManager.CanSignInAsync(user))
        {
          return BadRequest(new OpenIdConnectResponse {
            Error = OpenIdConnectConstants.Errors.InvalidGrant,
            ErrorDescription = "The user is no longer allowed to sign in"
          });
        }

        // Create a new authentication ticket, but reuse the properties stored
        // in the refresh token, including the scopes originally granted.
        var ticket = await CreateTicketAsync(request, user, info.Properties);

        return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
      }
      return BadRequest(new OpenIdConnectResponse {
        Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
        ErrorDescription = "The specified grant type is not supported"
      });
    }
     

    private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user, AuthenticationProperties properties = null)
    {
      // Create a new ClaimsPrincipal containing the claims that
      // will be used to create an id_token, a token or a code.
      var principal = await _signInManager.CreateUserPrincipalAsync(user);

      // Create a new authentication ticket holding the user identity.
      var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme);
      ticket.SetResources(request.GetResources());

      //if (!request.IsRefreshTokenGrantType())
      //{
      // 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[]
      {
                    OpenIdConnectConstants.Scopes.OpenId,
                    OpenIdConnectConstants.Scopes.Email,
                    OpenIdConnectConstants.Scopes.Profile,
                    OpenIdConnectConstants.Scopes.OfflineAccess,
                    OpenIddictConstants.Scopes.Roles
                }.Intersect(request.GetScopes()));
      //}


      // Note: by default, claims are NOT automatically included in the access and identity tokens.
      // To allow OpenIddict to serialize them, you must attach them a destination, that specifies
      // whether they should be included in access tokens, in identity tokens or in both.

      foreach(var claim in ticket.Principal.Claims)
      {
        // Never include the security stamp in the access and identity tokens, as it's a secret value.
        if(claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
          continue;

        claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
      }


      var identity = principal.Identity as ClaimsIdentity;

      if(!string.IsNullOrWhiteSpace(user.Email))
        identity.AddClaim(CustomClaimTypes.Email, user.Email, OpenIdConnectConstants.Destinations.IdentityToken);

      //if (!string.IsNullOrWhiteSpace(user.FullName))
      //    identity.AddClaim(CustomClaimTypes.FullName, user.FullName, OpenIdConnectConstants.Destinations.IdentityToken);

      if(!user.EmailConfirmed)
        identity.AddClaim(CustomClaimTypes.EmailConfirmed, user.EmailConfirmed.ToString(), OpenIdConnectConstants.Destinations.IdentityToken);

      if(!string.IsNullOrWhiteSpace(user.PhoneNumber))
        identity.AddClaim(CustomClaimTypes.Phone, user.PhoneNumber, OpenIdConnectConstants.Destinations.IdentityToken);

      if(!string.IsNullOrWhiteSpace(user.Configuration))
        identity.AddClaim(CustomClaimTypes.Configuration, user.Configuration, OpenIdConnectConstants.Destinations.IdentityToken);


      return ticket;
    }
  }
@Tratcher
Copy link
Member

That's a bit too much code for a github issue, and it doesn't seem directly related to your question. If you want to share a lot of code then link to a github repo. If you have a question about a specific piece of code then either link directly to that section or scope down what you've posted.

For a question as broad as yours, you're looking for a how-to guide, you're not actually asking about an issue with your existing code. Something like https://code.msdn.microsoft.com/How-to-authorization-914d126b?

@AlexOliinyk1
Copy link
Author

@Tratcher I resolved my problem, thank you

One additional question it is any exist reason to not use OpenIdDict, for example because they doesn't have any release version ?
Which tools we have to use for native authorization with asp.net core and angular 2 ?

@Tratcher
Copy link
Member

https://github.com/IdentityServer/IdentityServer4 is the only released one I'm aware of.

@kevinchalet
Copy link
Contributor

One additional question it is any exist reason to not use OpenIdDict, for example because they doesn't have any release version ?

I'm working on it.

@Tratcher just so you know, ASOS was officially released 3 months ago (and I pushed the 1st servicing release last week) 😅

@Tratcher
Copy link
Member

Good to hear :-)

@Eilon Eilon closed this as completed Aug 24, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants