问题
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