ASP.NET Core and Angular: Microsoft Authentication

谁说胖子不能爱 提交于 2021-02-08 10:36:28

问题


For the moment I'm trying to add third party authentication to my ASP.NET Core web application. Today I've successfully implemented Facebook authentication. This was already a struggle since the docs only mention Facebook authentication for a ASP.NET application with razor pages (https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins?view=aspnetcore-2.2). Nothing has been written in the docs about implementing this for Angular apps.

This was the most complete walkthrough I found for ASP.NET Core + Angular + FB auth: https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login

I'm using Microsoft.AspNetCore.Identity, this package already manages a lot for you. But I can't find how to get started implementing Microsoft, Google or even Twitter login in a web app. The docs don't seem to cover that part...

My GitHub repo: https://github.com/MusicDemons/MusicDemons-ASP-NET

Anyone had any experience with this?


回答1:


google-login.component.html

<button class="btn btn-secondary google-login-btn" [disabled]="isOpen" (click)="launchGoogleLogin()">
    <i class="fa fa-google"></i>
    Login with Google
</button>

google-login.component.scss

.google-login-btn {
    background: #fff;
    color: #333;
    padding: 5px 10px;

    &:not([disabled]):hover {
      background: #eee;
    }
}

google-login.component.ts

import { Component, Output, EventEmitter, Inject } from '@angular/core';
import { AuthService } from '../../../services/auth.service';
import { Router } from '@angular/router';
import { LoginResult } from '../../../entities/loginResult';

@Component({
  selector: 'app-google-login',
  templateUrl: './google-login.component.html',
  styleUrls: [
    './google-login.component.scss'
  ]
})
export class GoogleLoginComponent {

  private authWindow: Window;
  private isOpen: boolean = false;

  @Output() public LoginSuccessOrFailed: EventEmitter<LoginResult> = new EventEmitter();

  launchGoogleLogin() {
    this.authWindow = window.open(`${this.baseUrl}/api/Account/connect/Google`, null, 'width=600,height=400');
    this.isOpen = true;
    var timer = setInterval(() => {
      if (this.authWindow.closed) {
        this.isOpen = false;
        clearInterval(timer);
      }
    });
  }

  constructor(private authService: AuthService, private router: Router, @Inject('BASE_URL') private baseUrl: string) {
    if (window.addEventListener) {
      window.addEventListener("message", this.handleMessage.bind(this), false);
    } else {
      (<any>window).attachEvent("onmessage", this.handleMessage.bind(this));
    }
  }

  handleMessage(event: Event) {
    const message = event as MessageEvent;
    // Only trust messages from the below origin.
    if (message.origin !== "https://localhost:44385") return;
    // Filter out Augury
    if (message.data.messageSource != null)
      if (message.data.messageSource.indexOf("AUGURY_") > -1) return;
    // Filter out any other trash
    if (message.data == "") return;

    const result = <LoginResult>JSON.parse(message.data);
    if (result.platform == "Google") {
      this.authWindow.close();
      this.LoginSuccessOrFailed.emit(result);
    }
  }
}

auth.service.ts

import { Injectable, Inject } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { RegistrationData } from "../helpers/registrationData";
import { User } from "../entities/user";
import { LoginResult } from "../entities/loginResult";

@Injectable({
  providedIn: 'root'
})

export class AuthService {
  constructor(private httpClient: HttpClient, @Inject('BASE_URL') private baseUrl: string) {
  }

  public getToken() {
    return localStorage.getItem('auth_token');
  }

  public register(data: RegistrationData) {
    return this.httpClient.post(`${this.baseUrl}/api/account/register`, data);
  }

  public login(email: string, password: string) {
    return this.httpClient.post<LoginResult>(`${this.baseUrl}/api/account/login`, { email, password });
  }

  public logout() {
    return this.httpClient.post(`${this.baseUrl}/api/account/logout`, {});
  }

  public loginProviders() {
    return this.httpClient.get<string[]>(`${this.baseUrl}/api/account/providers`);
  }

  public currentUser() {
    return this.httpClient.get<User>(`${this.baseUrl}/api/account/current-user`);
  }
}

AccountController.cs

[Route("api/[controller]")]
public class AccountController : Controller
{
    private IEmailSender emailSender;
    private IAccountRepository accountRepository;
    private IConfiguration configuration;
    private IAuthenticationSchemeProvider authenticationSchemeProvider;
    public AccountController(IConfiguration configuration, IEmailSender emailSender, IAuthenticationSchemeProvider authenticationSchemeProvider, IAccountRepository accountRepository)
    {
        this.configuration = configuration;
        this.emailSender = emailSender;
        this.accountRepository = accountRepository;
        this.authenticationSchemeProvider = authenticationSchemeProvider;
    }

    ...

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody]LoginVM loginVM)
    {
        var login_result = await accountRepository.LocalLogin(loginVM.Email, loginVM.Password, true);
        return Ok(login_result);
    }

    [AllowAnonymous]
    [HttpGet("providers")]
    public async Task<List<string>> Providers()
    {
        var result = await authenticationSchemeProvider.GetRequestHandlerSchemesAsync();
        return result.Select(s => s.DisplayName).ToList();
    }


    [HttpGet("connect/{provider}")]
    [AllowAnonymous]
    public async Task<ActionResult> ExternalLogin(string provider, string returnUrl = null)
    {
        var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { provider });
        var properties = accountRepository.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return Challenge(properties, provider);
    }

    [HttpGet("connect/{provider}/callback")]
    public async Task<ActionResult> ExternalLoginCallback([FromRoute]string provider)
    {
        var model = new TokenMessageVM();
        try
        {
            var login_result = await accountRepository.PerfromExternalLogin();
            if(login_result.Status)
            {
                model.AccessToken = login_result.Token;
                model.Platform = login_result.Platform;
                return View(model);
            }
            else
            {
                model.Error = login_result.Error;
                model.ErrorDescription = login_result.ErrorDescription;
                model.Platform = login_result.Platform;
                return View(model);
            }
        }
        catch (OtherAccountException other_account_ex)
        {
            model.Error = "Could not login";
            model.ErrorDescription = other_account_ex.Message;
            model.Platform = provider;
            return View(model);
        }
        catch (Exception ex)
        {
            model.Error = "Could not login";
            model.ErrorDescription = "There was an error with your social login";
            model.Platform = provider;
            return View(model);
        }
    }
}

Stuff that matters in the AccountRepository

public interface IAccountRepository
{
    ...
    Task<LoginResult> LocalLogin(string email, string password, bool remember);
    Task Logout();

    Task<User> GetUser(string id);
    Task<User> GetCurrentUser(ClaimsPrincipal userProperty);
    Task<List<User>> GetUsers();

    Microsoft.AspNetCore.Authentication.AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl);
    Task<LoginResult> PerfromExternalLogin();
}

Implementation

public class AccountRepository : IAccountRepository
{
    private YourDbContext your_db_context;
    private UserManager<Entities.User> user_manager;
    private SignInManager<Entities.User> signin_manager;
    private FacebookOptions facebookOptions;
    private JwtIssuerOptions jwtIssuerOptions;
    private IEmailSender email_sender;
    public AccountRepository(
        IEmailSender email_sender,
        UserManager<Entities.User> user_manager,
        SignInManager<Entities.User> signin_manager,
        IOptions<FacebookOptions> facebookOptions,
        IOptions<JwtIssuerOptions> jwtIssuerOptions,
        YourDbContext your_db_context)
    {
        this.user_manager = user_manager;
        this.signin_manager = signin_manager;
        this.email_sender = email_sender;
        this.your_db_context = your_db_context;
        this.facebookOptions = facebookOptions.Value;
        this.jwtIssuerOptions = jwtIssuerOptions.Value;
    }

    ...

    public async Task<LoginResult> LocalLogin(string email, string password, bool remember)
    {
        var user = await user_manager.FindByEmailAsync(email);
        var result = await signin_manager.PasswordSignInAsync(user, password, remember, false);
        if (result.Succeeded)
        {
            return new LoginResult {
                Status = true,
                Platform = "local",
                User = ToDto(user),
                Token = CreateToken(email)
            };
        }
        else
        {
            return new LoginResult {
                Status = false,
                Platform = "local",
                Error = "Login attempt failed",
                ErrorDescription = "Username or password incorrect"
            };
        }
    }

    public async Task Logout()
    {
        await signin_manager.SignOutAsync();
    }

    private string CreateToken(string email)
    {
        var token_descriptor = new SecurityTokenDescriptor
        {
            Issuer = jwtIssuerOptions.Issuer,
            IssuedAt = jwtIssuerOptions.IssuedAt,
            Audience = jwtIssuerOptions.Audience,
            NotBefore = DateTime.UtcNow,
            Expires = DateTime.UtcNow.AddDays(7),
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name, email)
            }),
            SigningCredentials = jwtIssuerOptions.SigningCredentials
        };
        var token_handler = new JwtSecurityTokenHandler();
        var token = token_handler.CreateToken(token_descriptor);
        var str_token = token_handler.WriteToken(token);
        return str_token;
    }
    private string CreateToken(ExternalLoginInfo info)
    {
        var identity = (ClaimsIdentity)info.Principal.Identity;

        var token_descriptor = new SecurityTokenDescriptor
        {
            Issuer = jwtIssuerOptions.Issuer,
            IssuedAt = jwtIssuerOptions.IssuedAt,
            Audience = jwtIssuerOptions.Audience,
            NotBefore = DateTime.UtcNow,
            Expires = DateTime.UtcNow.AddDays(7),
            Subject = identity,
            SigningCredentials = jwtIssuerOptions.SigningCredentials
        };
        var token_handler = new JwtSecurityTokenHandler();
        var token = token_handler.CreateToken(token_descriptor);
        var str_token = token_handler.WriteToken(token);
        return str_token;
    }

    public Microsoft.AspNetCore.Authentication.AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl)
    {
        var properties = signin_manager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return properties;
    }

    public async Task<LoginResult> PerfromExternalLogin()
    {
        var info = await signin_manager.GetExternalLoginInfoAsync();
        if (info == null)
            throw new UnauthorizedAccessException();

        var user = await user_manager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
        if(user == null)
        {
            string username = info.Principal.FindFirstValue(ClaimTypes.Name);
            string email = info.Principal.FindFirstValue(ClaimTypes.Email);

            var new_user = new Entities.User
            {
                UserName = username,
                FacebookId = null,
                Email = email,
                PictureUrl = null
            };
            var id_result = await user_manager.CreateAsync(new_user);
            if (!id_result.Succeeded)
            {
                // User creation failed, probably because the email address is already present in the database
                if (id_result.Errors.Any(e => e.Code == "DuplicateEmail"))
                {
                    var existing = await user_manager.FindByEmailAsync(email);
                    var existing_logins = await user_manager.GetLoginsAsync(existing);

                    if (existing_logins.Any())
                    {
                        throw new OtherAccountException(existing_logins);
                    }
                    else
                    {
                        throw new Exception("Could not create account from social profile");
                    }
                }
            }
            await user_manager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.ProviderDisplayName));
            user = new_user;
        }

        var result = await signin_manager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
        if (result.Succeeded)
        {
            return new LoginResult {
                Status = true,
                Platform = info.LoginProvider,
                User = ToDto(user),
                Token = CreateToken(info)
            };
        }
        else if (result.IsLockedOut)
        {
            throw new UnauthorizedAccessException();
        }
        else
        {
            throw new UnauthorizedAccessException();
        }
    }
}

And finally the view that handles the callback and sends the message back to the main browser window (Views/Account/ExternalLoginCallback)

@model Project.Web.ViewModels.Account.TokenMessageVM
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Bezig met verwerken...</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <script src="/util/util.js"></script>
</head>
<body>
    <script>
        // if we don't receive an access token then login failed and/or the user has not connected properly
        var accessToken = "@Model.AccessToken";
        var message = {};
        if (accessToken) {
            message.status = true;
            message.platform = "@Model.Platform";

            message.token = accessToken;
        } else {
            message.status = false;
            message.platform = "@Model.Platform";

            message.error = "@Model.Error";
            message.errorDescription = "@Model.ErrorDescription";
        }
        window.opener.postMessage(JSON.stringify(message), "https://localhost:44385");
    </script>
</body>
</html>

ViewModel:

public class TokenMessageVM
{
    public string AccessToken { get; set; }
    public string Platform { get; set; }

    public string Error { get; set; }
    public string ErrorDescription { get; set; }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    var connection_string = @"Server=(localdb)\mssqllocaldb;Database=DbName;Trusted_Connection=True;ConnectRetryCount=0";
        services
            .AddDbContext<YourDbContext>(
                options => options.UseSqlServer(connection_string, b => b.MigrationsAssembly("EntitiesProjectAssembly"))
            )

    var connection_string = @"Server=(localdb)\mssqllocaldb;Database=DbName;Trusted_Connection=True;ConnectRetryCount=0";

    var app_settings = new Data.Helpers.JwtIssuerOptions();
    Configuration.GetSection(nameof(Data.Helpers.JwtIssuerOptions)).Bind(app_settings);

    services
        .AddDbContext<YourDbContext>(
            options => options.UseSqlServer(connection_string, b => b.MigrationsAssembly("EntitiesProjectAssembly"))
        )
        .AddScoped<IAccountRepository, AccountRepository>()
        .AddTransient<IEmailSender, EmailSender>()
        .AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services
        .AddIdentity<Data.Entities.User, Data.Entities.Role>()
        .AddEntityFrameworkStores<YourDbContext>()
        .AddDefaultTokenProviders();

    services.AddDataProtection();
    services.Configure<IdentityOptions>(options =>
    {
        // Password settings
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = false;
        options.Password.RequiredUniqueChars = 6;

        // Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = System.TimeSpan.FromMinutes(30);
        options.Lockout.MaxFailedAccessAttempts = 10;
        options.Lockout.AllowedForNewUsers = true;

        // User settings
        options.User.RequireUniqueEmail = true;
        options.User.AllowedUserNameCharacters = string.Empty;
    })
    .Configure<Data.Helpers.JwtIssuerOptions>(options =>
    {
        options.Issuer = app_settings.Issuer;
        options.Audience = app_settings.Audience;
        options.SigningCredentials = app_settings.SigningCredentials;
    })
    .ConfigureApplicationCookie(options =>
    {
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.Cookie.Expiration = System.TimeSpan.FromDays(150);
        // If the LoginPath isn't set, ASP.NET Core defaults 
        // the path to /Account/Login.
        options.LoginPath = "/Account/Login";
        // If the AccessDeniedPath isn't set, ASP.NET Core defaults 
        // the path to /Account/AccessDenied.
        options.AccessDeniedPath = "/Account/AccessDenied";
        options.SlidingExpiration = true;
    });

    services.AddAuthentication()
        .AddFacebook(options => {
            options.AppId = Configuration["FacebookAuthSettings:AppId"];
            options.AppSecret = Configuration["FacebookAuthSettings:AppSecret"];
        })
        .AddMicrosoftAccount(options => {
            options.ClientId = Configuration["MicrosoftAuthSettings:AppId"];
            options.ClientSecret = Configuration["MicrosoftAuthSettings:AppSecret"];
        })
        .AddGoogle(options => {
            options.ClientId = Configuration["GoogleAuthSettings:AppId"];
            options.ClientSecret = Configuration["GoogleAuthSettings:AppSecret"];
        })
        .AddTwitter(options => {
            options.ConsumerKey = Configuration["TwitterAuthSettings:ApiKey"];
            options.ConsumerSecret = Configuration["TwitterAuthSettings:ApiSecret"];
            options.RetrieveUserDetails = true;
        })
        .AddLinkedin(options => {
            options.ClientId = Configuration["LinkedInAuthSettings:AppId"];
            options.ClientSecret = Configuration["LinkedInAuthSettings:AppSecret"];
        })
        .AddGitHub(options => {
            options.ClientId = Configuration["GitHubAuthSettings:AppId"];
            options.ClientSecret = Configuration["GitHubAuthSettings:AppSecret"];
        })
        .AddPinterest(options => {
            options.ClientId = Configuration["PinterestAuthSettings:AppId"];
            options.ClientSecret = Configuration["PinterestAuthSettings:AppSecret"];
        });

    ...
}

It's also worth mentioning that you have to get permissions from the social-media sites:

  • Facebook: https://developers.facebook.com
    • Products -> facebook logins -> Add an OAuth redirect URI: this is the uri of your application (= https://localhost:44385/signin-facebook)
    • The user must set the option that he wants his email address to be shared with apps
  • Twitter: https://developer.twitter.com/en/apps
    • Open app details
    • Enable Sign in with Twitter
    • Callback urls: https://localhost:44385/signin-twitter
    • Keys and tokens: generate those
    • Permissions: Request email address
    • You have to add the option in c#: options.RetrieveUserDetails = true;
  • Google: https://console.developers.google.com/apis
    • You have to enable the Google+ API and People API
    • Signin credentials
      • Create credentials -> Client-ID OAuth -> Webapp
      • Authorized javascript sources: https://localhost:44385
      • Authorized redirect URIs: https://localhost:44385/signin-google
      • Copy the Client-ID and secret
    • OAuth-permissions
      • Bereiken voor Google API's: Is automatically set to email, profile and openid
      • Authorized domains: a public domain where you intend to host your website
    • Microsoft: https://apps.dev.microsoft.com
      • Converged applications -> add
      • Add platform -> Web
      • Redirect URLs: https://localhost:44385/signin-microsoft
      • Microsoft Graph Permissions: User.Read
    • GitHub:
      • The user has to set a public email address: Settings -> profile -> Public email


来源:https://stackoverflow.com/questions/55303781/asp-net-core-and-angular-microsoft-authentication

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!