10

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.

totkov
  • 123
  • 1
  • 6
  • The issue is on your google auth settings, you didn't provide http://localhost:5000 as callback url. Access-Control headers are response not request headers. – agua from mars Feb 14 '19 at 16:41
  • Do you your application in the same host than your API ? – agua from mars Feb 14 '19 at 16:45
  • Thanks for answer sir. I'm added localhost:5000 in google console. Api endpoin and google login configuration work correctly, because if I call localhost:5000/api/authentication/externalLogin?provider=Google from browser I get successful response. I think that the reason of this behaivor is in angular code. Is this request sending is correct? – totkov Feb 14 '19 at 16:55
  • no it's not. You should get the token from google 1st and then pass the token in the authorization header – agua from mars Feb 14 '19 at 17:14
  • Or use the cookie sent by you web site after authentication – agua from mars Feb 14 '19 at 17:17
  • have you enabled cors on your .net Web API. you need to enable cors on your .net app: https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.2 – mukesh joshi Feb 15 '19 at 05:29
  • Yes, I'm enabled CORS. The another endpoints works correct. – totkov Feb 15 '19 at 10:17
  • I have exactly the same problem in my app. I am using angular 7 + .net core web api and I am trying to login with LinkedIn :( – tzm Mar 23 '19 at 02:45

5 Answers5

10

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;
    }
Javi
  • 345
  • 3
  • 7
  • I really like this alternative idea, I was trying to get that to work but I get stuck with on the server side after signing the user in via Authentication.SignIn etc. I can't get it to return the token information like it does when using the default GrantResourceOwnerCredentials – WtFudgE Nov 07 '19 at 12:23
  • @Javi I did everything the same as you described in first approach but I keep getting `Correlation failed`, from here `Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()`. Do you maybe know what is wrong? – Miloš Đošović Aug 08 '20 at 09:30
  • @Javi I am trying to implement the second approach but I also have jwt bearer implemented for my own userid, password authentication purpose and I am not getting that once I have idToken from google in the angular app then how can I authenticate the user using google credentials in subsequent requests.in my app user has choice to create their own userid, pwd or to sign-up using google. So in my startup, i have jwtauthentication mentioned. and if i pass jwt created token in header it authenticate but how to tell api that to authenticate google token also . – Mr. Jay Oct 27 '20 at 04:22
  • Hi @Mr.Jay , with this approach you only validate the google idToken one time and if it is valid you return your own app JWT, so in subsequent requests you only use your own app token. If your token lives long enough it may happen that the user could have changed his Google credentials but still be logged in to your app. Anyways keep in mind that the Google token only lives 1 hour, if your app needs to access the Google api after that you should use the refresh token. – Javi Oct 28 '20 at 10:26
  • @Mr.Jay, in regards to the refresh token with google I haven’t implemented it but the flow changes. Now in the Angular app you should get an authentication code (you have to set the offline_access config param to true in the provider configuration --> ´provider: new GoogleLoginProvider(environment.googleClientId, {offline_access: true})´) and then in the backend use the GoogleAuthorizationCodeFlow as Dmitry Komar has posted that will get you an access token and a refresh token that should give you access to more tokens. – Javi Oct 28 '20 at 10:27
0

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;
    }
Dmitry Komar
  • 101
  • 2
  • 9
0

I think it will help you guys.

import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable()
export class LoginService {

constructor(@Inject(DOCUMENT) private document: Document,...)
    login() {
         this.document.location.href = 'https://www.mywebsite.com/account/signInWithGoogle';
    }
}

https://www.blinkingcaret.com/2018/10/10/sign-in-with-an-external-login-provider-in-an-angular-application-served-by-asp-net-core/

Koon
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-ask). – Community Sep 17 '21 at 01:35
-1

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);
  }
}
nehuen
  • 1
-1

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();
        });
    });
    
painacle
  • 23
  • 6