ASP.NET Core и Angular: проверка подлинности Microsoft - PullRequest
0 голосов
/ 22 марта 2019

В данный момент я пытаюсь добавить стороннюю аутентификацию в мое веб-приложение ASP.NET Core.Сегодня я успешно внедрил аутентификацию в Facebook.Это уже было проблемой, поскольку в документах упоминается только аутентификация Facebook для приложения ASP.NET с бритвенными страницами (https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins?view=aspnetcore-2.2).). В документах ничего не написано о реализации этого для приложений Angular.

Это былонаиболее полное пошаговое руководство, которое я нашел для 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

Я использую Microsoft.AspNetCore.Identity, этот пакет уже многим вам помогает. Но я не могуузнайте, как начать внедрение входа в систему Microsoft, Google или даже Twitter в веб-приложении. Документы, похоже, не охватывают эту часть ...

Мой репозиторий GitHub: https://github.com/MusicDemons/MusicDemons-ASP-NET

Кто-нибудь имел опыт работы с этим?

1 Ответ

1 голос
/ 03 апреля 2019

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

Материал, который имеет значение в 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();
}

Реализация

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

И, наконец, представление, которое обрабатывает обратный вызов и отправляет сообщение обратно в главное окно браузера (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"];
        });

    ...
}

Стоит также отметить, что вам необходимо получить разрешения от сайтов социальных сетей:

  • Facebook: https://developers.facebook.com
    • Продукты-> входы в Facebook -> добавить URI перенаправления OAuth: это URI вашего приложения (= https://localhost:44385/signin-facebook)
    • Пользователь должен установить, что он хочет, чтобы его адрес электронной почты передавался приложениям
  • Twitter: https://developer.twitter.com/en/apps
    • Открыть сведения о приложении
    • Включить Войти через Twitter
    • URL обратного вызова: https://localhost:44385/signin-twitter
    • Ключи и токены: сгенерируйте их
    • Разрешения: Запросите адрес электронной почты
    • Вы должны добавить опцию в c #: options.RetrieveUserDetails = true;
  • Google: https://console.developers.google.com/apis
    • Вы должны включить Google+ API и People API
    • Учетные данные для входа
      • Создание учетных данных -> Client-ID OAuth -> Webapp
      • Авторизованные источники JavaScript: https://localhost:44385
      • Авторизованные URI перенаправления: https://localhost:44385/signin-google
      • Скопируйте Client-ID иsecret
    • OAuth-разрешения
      • Bereiken voor Google API's: автоматически настраивается на электронную почту, профиль и openid
      • Авторизованные домены: публичный домен, где вынамереваться разместить ваш сайт
    • Microsoft: https://apps.dev.microsoft.com
      • Конвергентные приложения -> добавить
      • Добавить платформу -> Веб
      • URL-адреса перенаправления: https://localhost:44385/signin-microsoft
      • График Microsoft Разрешения: User.Read
    • GitHub:
      • Пользователь имеетустановить общедоступный адрес электронной почты: Настройки -> Профиль -> Общедоступный адрес электронной почты
...