使用.NET Core API在Angular 7中进行Google登录

tot*_*kov 6 google-authentication cors asp.net-web-api asp.net-core angular

我正在尝试在我的Angular应用程序中实现Google登录。如果我尝试为外部登录服务器调用api端点,则返回405错误代码,如下所示:

从源“空” 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=...'(从重定向'http://localhost:5000/api/authentication/externalLogin?provider=Google')对XMLHttpRequest的访问已被CORS策略阻止:对预检请求的响应未通过访问控制检查:所请求的资源上不存在“ Access-Control-Allow-Origin”标头。

如果我api/authentication/externalLogin?provider=Google在新的浏览器选项卡中调用,则所有功能均正常运行。我认为问题出在角度代码中。

我的api适用于localhost:5000。Angular应用程序可在上使用localhost:4200。我使用.net core 2.1和Angular 7

C#代码

启动文件

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false
    };
})
.AddCookie()
.AddGoogle(options => {
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ClientId = "xxx";
    options.ClientSecret = "xxx";
    options.Scope.Add("profile");
    options.Events.OnCreatingTicket = (context) =>
    {
        context.Identity.AddClaim(new Claim("image", context.User.GetValue("image").SelectToken("url").ToString()));

        return Task.CompletedTask;
    };
});
Run Code Online (Sandbox Code Playgroud)

AuthenticationController.cs

[HttpGet]
public IActionResult ExternalLogin(string provider)
{
    var callbackUrl = Url.Action("ExternalLoginCallback");
    var authenticationProperties = new AuthenticationProperties { RedirectUri = callbackUrl };
    return this.Challenge(authenticationProperties, provider);
}

[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
    var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);

    return this.Ok(new
    {
        NameIdentifier = result.Principal.FindFirstValue(ClaimTypes.NameIdentifier),
        Email = result.Principal.FindFirstValue(ClaimTypes.Email),
        Picture = result.Principal.FindFirstValue("image")
    });
}
Run Code Online (Sandbox Code Playgroud)

角度代码

login.component.html

<button (click)="googleLogIn()">Log in with Google</button>
Run Code Online (Sandbox Code Playgroud)

login.component.ts

googleLogIn() {
  this.authenticationService.loginWithGoogle()
  .pipe(first())
  .subscribe(
    data => console.log(data)
  );
}
Run Code Online (Sandbox Code Playgroud)

身份验证服务

public loginWithGoogle() {
  return this.http.get<any>(`${environment.api.apiUrl}${environment.api.authentication}externalLogin`,
  {
    params: new HttpParams().set('provider', 'Google'),
    headers: new HttpHeaders()
      .set('Access-Control-Allow-Headers', 'Content-Type')
      .set('Access-Control-Allow-Methods', 'GET')
      .set('Access-Control-Allow-Origin', '*')
  })
  .pipe(map(data => {
    return data;
  }));
}
Run Code Online (Sandbox Code Playgroud)

我想象以下方案: Angular->我的API->重定向到Google-> google将用户数据返回到我的api->我的API返回JWT令牌-> Angular使用令牌

您能帮我解决这个问题吗?

小智 10

问题似乎是,尽管服务器正在发送 302 响应(url 重定向),但 Angular 正在发出 XMLHttpRequest,但它并没有重定向。有这个问题的人越来越多了...

对我来说,试图拦截前端的响应以进行手动重定向或更改服务器上的响应代码(这是一个“挑战”响应..)没有奏效。

因此,我为使其工作所做的是将 Angular 中的 window.location 更改为后端服务,以便浏览器可以管理响应并正确进行重定向。

注意:在帖子的最后,我解释了一个更直接的解决方案,用于 SPA 应用程序,而不使用 cookie 或 AspNetCore 身份验证。

完整的流程是这样的:

(1) Angular 将浏览器位置设置为 API -> (2) API 发送 302 响应 --> (3) 浏览器重定向到 Google -> (4) Google 将用户数据作为 cookie 返回给 API -> (5) API 返回 JWT令牌 -> (6) Angular 使用令牌

1.- Angular 将浏览器位置设置为 API。我们将提供程序和 returnURL 传递给我们希望 API 在流程结束时返回 JWT 令牌的位置。

import { DOCUMENT } from '@angular/common';
...
 constructor(@Inject(DOCUMENT) private document: Document, ...) { }
...
  signInExternalLocation() {
    let provider = 'provider=Google';
    let returnUrl = 'returnUrl=' + this.document.location.origin + '/register/external';

    this.document.location.href = APISecurityRoutes.authRoutes.signinexternal() + '?' + provider + '&' + returnUrl;
  }
Run Code Online (Sandbox Code Playgroud)

2.- API 发送 302 Challenge 响应。我们使用提供商和我们希望 Google 回电的 URL 创建重定向。

// GET: api/auth/signinexternal
[HttpGet("signinexternal")]
public IActionResult SigninExternal(string provider, string returnUrl)
{
    // Request a redirect to the external login provider.
    string redirectUrl = Url.Action(nameof(SigninExternalCallback), "Auth", new { returnUrl });
    AuthenticationProperties properties = _signInMgr.ConfigureExternalAuthenticationProperties(provider, redirectUrl);

    return Challenge(properties, provider);
}
Run Code Online (Sandbox Code Playgroud)

5.- API 接收谷歌用户数据并返回 JWT 令牌。在查询字符串中,我们将有 Angular 返回 URL。在我的情况下,如果用户未注册,我正在做一个额外的步骤来请求许可。

// GET: api/auth/signinexternalcallback
[HttpGet("signinexternalcallback")]
public async Task<IActionResult> SigninExternalCallback(string returnUrl = null, string remoteError = null)
{
    //string identityExternalCookie = Request.Cookies["Identity.External"];//do we have the cookie??

    ExternalLoginInfo info = await _signInMgr.GetExternalLoginInfoAsync();

    if (info == null)  return new RedirectResult($"{returnUrl}?error=externalsigninerror");

    // Sign in the user with this external login provider if the user already has a login.
    Microsoft.AspNetCore.Identity.SignInResult result = 
        await _signInMgr.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);

    if (result.Succeeded)
    {
        CredentialsDTO credentials = _authService.ExternalSignIn(info);
        return new RedirectResult($"{returnUrl}?token={credentials.JWTToken}");
    }

    if (result.IsLockedOut)
    {
        return new RedirectResult($"{returnUrl}?error=lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.

        string loginprovider = info.LoginProvider;
        string email = info.Principal.FindFirstValue(ClaimTypes.Email);
        string name = info.Principal.FindFirstValue(ClaimTypes.GivenName);
        string surname = info.Principal.FindFirstValue(ClaimTypes.Surname);

        return new RedirectResult($"{returnUrl}?error=notregistered&provider={loginprovider}" +
            $"&email={email}&name={name}&surname={surname}");
    }
}
Run Code Online (Sandbox Code Playgroud)

用于注册额外步骤的 API(对于此调用,Angular 必须使用“WithCredentials”发出请求才能接收 cookie):

[HttpPost("registerexternaluser")]
public async Task<IActionResult> ExternalUserRegistration([FromBody] RegistrationUserDTO registrationUser)
{
    //string identityExternalCookie = Request.Cookies["Identity.External"];//do we have the cookie??

    if (ModelState.IsValid)
    {
        // Get the information about the user from the external login provider
        ExternalLoginInfo info = await _signInMgr.GetExternalLoginInfoAsync();

        if (info == null) return BadRequest("Error registering external user.");

        CredentialsDTO credentials = await _authService.RegisterExternalUser(registrationUser, info);
        return Ok(credentials);
    }

    return BadRequest();
}
Run Code Online (Sandbox Code Playgroud)

SPA 应用的不同方法:

只是当我完成使它工作,我发现对于SPA的应用有做(一个更好的方式https://developers.google.com/identity/sign-in/web/server-side-flow与谷歌JWT认证AspNet 核心 2.0https: //medium.com/mickeysden/react-and-google-oauth-with-net-core-backend-4faaba25ead0 )

对于这种方法,流程将是:

(1) Angular 打开 google 身份验证 -> (2) 用户身份验证 --> (3) Google 将 googleToken 发送到 angular -> (4) Angular 将其发送到 API -> (5) API 针对 google 对其进行验证并返回 JWT 令牌-> (6) Angular 使用令牌

为此,我们需要在 Angular 中安装“ angularx-social-login ”npm 包,在 netcore 后端安装“ Google.Apis.Auth ”NuGet 包

1. 和 4. - Angular 打开谷歌身份验证。我们将使用 angularx-social-login 库。用户在Angular 中唱歌后,将 googletoken 发送到 API

login.module.ts我们添加:

let config = new AuthServiceConfig([
  {
    id: GoogleLoginProvider.PROVIDER_ID,
    provider: new GoogleLoginProvider('Google ClientId here!!')
  }
]);

export function provideConfig() {
  return config;
}

@NgModule({
  declarations: [
...
  ],
  imports: [
...
  ],
  exports: [
...
  ],
  providers: [
    {
      provide: AuthServiceConfig,
      useFactory: provideConfig
    }
  ]
})
Run Code Online (Sandbox Code Playgroud)

在我们的login.component.ts 上

import { AuthService, GoogleLoginProvider } from 'angularx-social-login';
...
  constructor(...,  private socialAuthService: AuthService)
...

  signinWithGoogle() {
    let socialPlatformProvider = GoogleLoginProvider.PROVIDER_ID;
    this.isLoading = true;

    this.socialAuthService.signIn(socialPlatformProvider)
      .then((userData) => {
        //on success
        //this will return user data from google. What you need is a user token which you will send it to the server
        this.authenticationService.googleSignInExternal(userData.idToken)
          .pipe(finalize(() => this.isLoading = false)).subscribe(result => {

            console.log('externallogin: ' + JSON.stringify(result));
            if (!(result instanceof SimpleError) && this.credentialsService.isAuthenticated()) {
              this.router.navigate(['/index']);
            }
        });
      });
  }
Run Code Online (Sandbox Code Playgroud)

在我们的authentication.service.ts 上

  googleSignInExternal(googleTokenId: string): Observable<SimpleError | ICredentials> {

    return this.httpClient.get(APISecurityRoutes.authRoutes.googlesigninexternal(), {
      params: new HttpParams().set('googleTokenId', googleTokenId)
    })
      .pipe(
        map((result: ICredentials | SimpleError) => {
          if (!(result instanceof SimpleError)) {
            this.credentialsService.setCredentials(result, true);
          }
          return result;

        }),
        catchError(() => of(new SimpleError('error_signin')))
      );

  }
Run Code Online (Sandbox Code Playgroud)

5.- API 针对 google 对其进行验证并返回 JWT 令牌。我们将使用“Google.Apis.Auth”NuGet 包。我不会为此提供完整代码,但请确保在验证令牌时将受众添加到安全登录的设置中:

 private async Task<GoogleJsonWebSignature.Payload> ValidateGoogleToken(string googleTokenId)
    {
        GoogleJsonWebSignature.ValidationSettings settings = new GoogleJsonWebSignature.ValidationSettings();
        settings.Audience = new List<string>() { "Google ClientId here!!" };
        GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(googleTokenId, settings);
        return payload;
    }
Run Code Online (Sandbox Code Playgroud)