Google login in Angular 7 with .NET Core API

泄露秘密 提交于 2019-12-09 11:55:53

问题


I'm trying to implement Google login in my Angular application. If I try to call api endpoint for external login server return 405 error code like this:

Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=...' (redirected from 'http://localhost:5000/api/authentication/externalLogin?provider=Google') from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

If I call api/authentication/externalLogin?provider=Google in new browser tab all work correctly. I thing that the problem is in angular code.

My api works on localhost:5000. Angular app works on localhost:4200. I use .net core 2.1 and Angular 7

C# code

Startup.cs

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

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

Angular code

login.component.html

<button (click)="googleLogIn()">Log in with Google</button>

login.component.ts

googleLogIn() {
  this.authenticationService.loginWithGoogle()
  .pipe(first())
  .subscribe(
    data => console.log(data)
  );
}

authentication.service.ts

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

I imagine the following scheme: Angular -> My API -> redirect to Google -> google return user data to my api -> My API return JWT token -> Angular use token

Could you help me with this problem.


回答1:


The problem seems to be that although the server is sending a 302 response (url redirection) Angular is making an XMLHttpRequest, it's not redirecting. There is more people having this issue...

For me trying to intercept the response in the frontend to make a manual redirection or changing the response code on the server (it is a 'Challenge' response..) didn't work.

So what I did to make it work was change in Angular the window.location to the backend service so the browser can manage the response and make the redirection properly.

NOTE: At the end of the post I explain a more straightforward solution for SPA applications without the use of cookies or AspNetCore Authentication.

The complete flow would be this:

(1) Angular sets browser location to the API -> (2) API sends 302 response --> (3) Browser redirects to Google -> (4) Google returns user data as cookie to API -> (5) API returns JWT token -> (6) Angular use token

1.- Angular sets browser location to the API. We pass the provider and the returnURL where we want the API to return the JWT token when the process has ended.

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

2.- API sends 302 Challenge response. We create the redirection with the provider and the URL where we want Google call us back.

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

5.- API receives google user data and returns JWT token. In the querystring we will have the Angular return URL. In my case if the user is not registered I was doing an extra step to ask for permission.

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

API for the registration extra step (for this call Angular has to make the request with 'WithCredentials' in order to receive the 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();
}

Different approach for SPA applications:

Just when i finished making it work i found that for SPA applications there is a better way of doing it (https://developers.google.com/identity/sign-in/web/server-side-flow, Google JWT Authentication with AspNet Core 2.0, https://medium.com/mickeysden/react-and-google-oauth-with-net-core-backend-4faaba25ead0 )

For this approach the flow would be:

(1) Angular opens google authentication -> (2) User authenticates --> (3) Google sends googleToken to angular -> (4) Angular sends it to the API -> (5) API validates it against google and returns JWT token -> (6) Angular uses token

For this we need to install the 'angularx-social-login' npm package in Angular and the 'Google.Apis.Auth' NuGet package in the netcore backend

1. and 4. - Angular opens google authentication. We will use the angularx-social-login library. After user sings in Angular sends the googletoken to the API.

On the login.module.ts we add:

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

On our 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']);
            }
        });
      });
  }

On our 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')))
      );

  }

5.- API validates it against google and returns JWT token. We will be using the 'Google.Apis.Auth' NuGet package. I won't put the full code for this but make sure that when you validate de token you add the audience to the settings for a secure signin:

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



回答2:


Just want to clarify part 5 from Jevi's answer, because it took some time for me to figure out how to get google access_token with an access_code. Here is a full server method. redirectUrl should be equal to one from 'Authorized JavaScript origins' from Google Console API. 'Authorized redirect URIs' can be empty.

[HttpPost("ValidateGoogleToken")]
    public async Task<GoogleJsonWebSignature.Payload> ValidateGoogleToken(string code)
    {
        IConfigurationSection googleAuthSection = _configuration.GetSection("Authentication:Google");

        var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
        {
            ClientSecrets = new ClientSecrets
            {
                ClientId = googleAuthSection["ClientId"],
                ClientSecret = googleAuthSection["ClientSecret"]
            }
        });

        var redirectUrl = "http://localhost:6700";
        var response = await flow.ExchangeCodeForTokenAsync(string.Empty, code, redirectUrl, CancellationToken.None);

        GoogleJsonWebSignature.ValidationSettings settings = new GoogleJsonWebSignature.ValidationSettings
        {
            Audience = new List<string>() {googleAuthSection["ClientId"]}
        };

        var payload = await GoogleJsonWebSignature.ValidateAsync(response.IdToken, settings);
        return payload;
    }



回答3:


I had a similar problem, and since you said you already have CORS all set up in the back end, Angular not adding credentials in the API requests might be the problem. Something the browser does when you type the api endpoint in the url bar. You can use angular interceptors for adding credentials in every request. Check this: https://angular.io/guide/http#intercepting-requests-and-responses

And for your particular case, this may work:

export class CookieInterceptor implements HttpInterceptor {

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    request = request.clone({
      withCredentials: true
    });
    return next.handle(request);
  }
}



回答4:


I have a few things to add:

  1. I've checked @Nehuen Antiman answer and it partially works for me.

  2. It is good practice to create such intereptor as he suggested, but it would be also ok if you just add the "withCredentials" flag to your service.ts:

    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', '*'),
        withCredentials: true
      })
      .pipe(map(data => {
        return data;
      }));
    }
    
  3. Please also remember to add AllowCredentials() method to your CorsOptions. Here is the example from my code:

    services.AddCors(options =>
    {
        options.AddPolicy(AllowedOriginsPolicy,
        builder =>
        {
            builder.WithOrigins("http://localhost:4200")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });
    });
    


来源:https://stackoverflow.com/questions/54694466/google-login-in-angular-7-with-net-core-api

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