8
votes

Connexion Google dans Angular 7 avec l'API .NET Core

J'essaie d'implémenter la connexion Google dans mon application Angular. Si j'essaie d'appeler le point de terminaison de l'API pour le serveur de connexion externe, renvoie le code d'erreur 405 comme ceci:

Accès à XMLHttpRequest à 'https: //accounts.google.com/o/oauth2/v2/auth? response_type = code & client_id = ...' (redirigé depuis 'http: // localhost: 5000 / api / authentication / externalLogin? provider = Google ') de l'origine' null 'a été bloqué par la stratégie CORS: la réponse à la demande de contrôle en amont ne passe pas le contrôle d'accès: Non' Access-Control L'en-tête -Allow-Origin est présent sur la ressource demandée.

Si j'appelle api / authentication / externalLogin? provider = Google dans le nouvel onglet du navigateur, tout fonctionne correctement. Je pense que le problème est dans le code angulaire.

Mon api fonctionne sur localhost: 5000 . L'application angulaire fonctionne sur localhost: 4200 . J'utilise .net core 2.1 et Angular 7

Code C #

Startup.cs

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

AuthenticationController.cs

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

Code angulaire

login.component.html

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

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

authentication.service.ts

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

J'imagine le schéma suivant: Angular -> Mon API -> redirection vers Google -> google renvoie les données utilisateur à mon api -> Mon jeton JWT de retour API -> Jeton d'utilisation angulaire

Pourriez-vous m'aider ce problème.


9 commentaires

Le problème concerne vos paramètres d'authentification Google, vous n'avez pas fourni localhost: 5000 comme URL de rappel. Les en-têtes de contrôle d'accès sont des en-têtes de réponse et non de demande.


Votre application est-elle dans le même hébergeur que votre API?


Merci pour la réponse monsieur. J'ai ajouté localhost: 5000 dans la console google. Api endpoin et la configuration de la connexion Google fonctionnent correctement, car si j'appelle localhost: 5000 / api / authentication / externalLogin? Provider = Goo‌ gle du navigateur, j'obtiens une réponse réussie. Je pense que la raison de ce comportement est dans le code angulaire. L'envoi de cette demande est-il correct?


non ce n'est pas. Vous devriez obtenir le jeton de google 1st, puis transmettre le jeton dans l'en-tête d'autorisation


Ou utilisez le cookie envoyé par votre site Web après l'authentification


avez-vous activé les cors sur votre API Web .net. vous devez activer cors sur votre application .net: docs.microsoft.com/en-us/aspnet/core/security/…


Oui, je suis activé CORS. Les autres points de terminaison fonctionnent correctement.


J'ai exactement le même problème dans mon application. J'utilise l'API Web angulaire 7 + .net core et j'essaie de me connecter avec LinkedIn :(


Même problème ici :(


4 Réponses :


-1
votes

J'ai eu un problème similaire, et comme vous avez dit que vous avez déjà configuré CORS dans le back-end, Angular n'ajoute pas d'informations d'identification dans les requêtes API peut être le problème. Le navigateur fait quelque chose lorsque vous tapez le point de terminaison de l'API dans la barre d'URL. Vous pouvez utiliser des intercepteurs angulaires pour ajouter des informations d'identification dans chaque demande. Vérifiez ceci: https://angular.io/guide/http#intercepting-requests -et-réponses

Et pour votre cas particulier, cela peut fonctionner:

export class CookieInterceptor implements HttpInterceptor {

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


0 commentaires

-1
votes

J'ai quelques éléments à ajouter:

  1. J'ai vérifié la réponse @Nehuen Antiman et cela fonctionne partiellement pour moi.

  2. C'est une bonne pratique de créer un tel intercepteur comme il l'a suggéré, mais ce serait également correct si vous ajoutez simplement le drapeau "withCredentials" à votre service.ts:

    services.AddCors(options =>
    {
        options.AddPolicy(AllowedOriginsPolicy,
        builder =>
        {
            builder.WithOrigins("http://localhost:4200")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });
    });
    
  3. N'oubliez pas d'ajouter la méthode AllowCredentials () à vos CorsOptions . Voici l'exemple de mon code:

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


0 commentaires

9
votes

Le problème semble être que bien que le serveur envoie une réponse 302 (redirection d'url) Angular effectue une requête XMLHttpRequest, il ne redirige pas. Il y a plus de gens qui ont ce problème ...

Pour moi, j'essaie d'intercepter la réponse dans le frontend pour faire une redirection manuelle ou de changer le code de réponse sur le serveur (c'est une réponse 'Challenge' ..) n'a pas

Donc, ce que j'ai fait pour que cela fonctionne, c'est de changer dans Angular l'emplacement window.location au service backend afin que le navigateur puisse gérer la réponse et effectuer la redirection correctement.

REMARQUE : À la fin de l'article, j'explique une solution plus simple pour les applications SPA sans l'utilisation de cookies ou d'authentification AspNetCore.

Le flux complet serait le suivant:

(1) Angular définit l'emplacement du navigateur sur l'API -> (2) L'API envoie 302 réponses -> (3) Le navigateur redirige vers Google -> (4) Google renvoie les données de l'utilisateur sous forme de cookie à l'API -> (5) L'API renvoie le jeton JWT -> (6) Le jeton d'utilisation angulaire

1.- Angular définit l'emplacement du navigateur sur l'API . Nous passons le fournisseur et la returnURL où nous voulons que l'API renvoie le jeton JWT lorsque le processus est terminé.

 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.- L'API envoie une réponse Challenge 302. Nous créons la redirection avec le fournisseur et l'URL où nous voulons que Google nous rappelle.

  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.- L'API reçoit les données utilisateur de Google et les renvoie Jeton JWT. Dans la chaîne de requête, nous aurons l'URL de retour angulaire. Dans mon cas, si l'utilisateur n'est pas enregistré, je faisais une étape supplémentaire pour demander la permission.

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

API pour l'étape supplémentaire d'enregistrement (pour cet appel, Angular doit faire la demande avec 'WithCredentials' afin de recevoir le cookie):

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

Approche différente pour les applications SPA:

Juste au moment où j'ai fini de le faire fonctionner, j'ai trouvé ça pour SPA applications, il existe une meilleure façon de le faire ( https: // développeurs .google.com / identity / sign-in / web / server-side-flow , Authentification Google JWT avec AspNet Core 2.0 , https://medium.com/mickeysden/react-and-google-oauth-with-net-core-backend-4faaba25ead0 )

Pour cette approche le flux serait:

(1) Angular ouvre l'authentification google -> (2) L'utilisateur s'authentifie -> (3) Google envoie googleToken à angular -> (4) Angular l'envoie à l'API -> (5) L'API le valide contre Google et renvoie le jeton JWT -> (6) Angular utilise le jeton

Pour cela, nous devons installer le package npm ' angularx-social-login ' dans Angular et le ' Google.Apis.Auth 'Package NuGet dans le backend netcore

1. et 4. - Angular ouvre l'authentification Google . Nous utiliserons la bibliothèque angularx-social-login. Une fois que l'utilisateur a chanté dans Angular envoie le googletoken à l'API .

Sur le login.module.ts , nous ajoutons:

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

Sur notre login.component.ts

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

Sur notre authentication.service.ts strong>:

// 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.- L'API le valide par rapport à google et renvoie le jeton JWT . Nous utiliserons le package NuGet "Google.Apis.Auth". Je ne vais pas mettre le code complet pour cela, mais assurez-vous que lorsque vous validez le jeton, vous ajoutez l'audience aux paramètres pour une connexion sécurisée:

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


5 commentaires

J'aime vraiment cette idée alternative, j'essayais de faire en sorte que cela fonctionne mais je reste bloqué côté serveur après avoir connecté l'utilisateur via Authentication.SignIn etc. Je ne peux pas lui faire renvoyer les informations de jeton comme il le fait quand en utilisant le GrantResourceOwnerCredentials par défaut


@Javi J'ai tout fait de la même manière que vous l'avez décrit dans la première approche, mais je continue à obtenir La corrélation a échoué , à partir d'ici Microsoft.AspNetCore.Authentication.RemoteAuthenticationHand‌ ler .Handle‌ RequestAsync () . Savez-vous peut-être ce qui ne va pas?


@Javi J'essaie de mettre en œuvre la deuxième approche mais j'ai également le support jwt mis en œuvre pour mon propre identifiant d'utilisateur, mon mot de passe et je ne l'obtiens pas une fois que j'ai idToken de Google dans l'application angulaire, comment puis-je authentifier l'utilisateur à l'aide de Google identifiants dans les demandes ultérieures.Dans mon application, l'utilisateur a le choix de créer son propre identifiant, pwd ou de s'inscrire à l'aide de google. Donc, dans mon démarrage, j'ai mentionné jwtauthentication. et si je passe le jeton créé par jwt dans l'en-tête, il s'authentifie, mais comment dire à l'API que pour authentifier le jeton Google également.


Salut @ Mr.Jay, avec cette approche, vous ne validez qu'une seule fois le google idToken et s'il est valide, vous renvoyez votre propre application JWT, donc dans les demandes suivantes, vous n'utilisez que votre propre jeton d'application. Si votre jeton dure suffisamment longtemps, il peut arriver que l'utilisateur ait changé ses informations d'identification Google tout en restant connecté à votre application. Quoi qu'il en soit, gardez à l'esprit que le jeton Google ne dure qu'une heure, si votre application doit accéder à l'API Google après cela, vous devez utiliser le jeton d'actualisation.


@ Mr.Jay, en ce qui concerne le jeton d'actualisation avec google, je ne l'ai pas mis en œuvre mais le flux change. Maintenant, dans l'application Angular, vous devriez obtenir un code d'authentification (vous devez définir le paramètre de configuration offline_access sur true dans la configuration du fournisseur -> ´provider: new GoogleLoginProvider (environment.googleClientId, {offline_access: true}) ´) puis dans le backend utilise GoogleAuthorizationCodeFlow comme Dmitry Komar l'a publié, ce qui vous donnera un jeton d'accès et un jeton d'actualisation qui devraient vous donner accès à plus de jetons.



0
votes

Je veux juste clarifier la partie 5 de la réponse de Jevi, car il m'a fallu un certain temps pour comprendre comment obtenir google access_token avec un code d'accès. Voici une méthode serveur complète. redirectUrl doit être égal à l'un des "Origines JavaScript autorisées" de l'API Google Console. Les "URI de redirection autorisés" peuvent être vides.

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


0 commentaires