Google login in Angular 7 with .NET Core API
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;
}