web-dev-qa-db-fra.com

Sécurisation d'un SPA par un serveur d'autorisation avant le premier chargement

J'utilise les "nouveaux" modèles de projet pour les applications angular SPA dans dotnet core 2.1 comme écrit dans l'article tilisez le modèle de projet Angular avec ASP.NET Core .

Mais cet article ne mentionne rien sur la sécurisation du SPA lui-même. Toutes les informations que je trouve concernent la sécurisation d'un WEBAPI mais tout d'abord je suis intéressé par la sécurisation du SPA.

Cela signifie: lorsque j'ouvre mon SPA, par exemple https: // localhost: 44329 / je voudrais être redirigé vers le serveur d'autorisation immédiatement au lieu de cliquer sur un bouton qui fera l'authentification.

Contexte:

  • Je dois m'assurer que seuls les utilisateurs authentifiés sont autorisés à voir le SPA.
  • Je veux utiliser Octroi de code d'autorisation pour obtenir des jetons d'actualisation de mon serveur d'autorisation.
  • Je ne peux pas utiliser Octroi implicite car les jetons d'actualisation ne peuvent pas rester confidentiels sur le navigateur

L'approche actuelle consiste à appliquer une politique MVC qui nécessite un utilisateur authentifié. Mais cela ne peut être appliqué qu'à un contrôleur MVC. C'est pourquoi j'ai ajouté HomeController pour répondre à la première demande.

Voir la structure du projet:

enter image description here

Mon Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "CustomScheme";
        })
        .AddCookie()
        .AddOAuth("CustomScheme", options =>
        {
            // Removed for brevity
        });

    services.AddMvc(config =>
    {
        // Require a authenticated user
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseAuthentication();

    app.UseStaticFiles();
    app.UseSpaStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
}

Comportement actuel: Lorsque je lance mon SPA, je suis immédiatement redirigé vers mon serveur d'autorisation en raison de la politique MVC. Après l'authentification réussie, je vois la méthode Index du contrôleur domestique mais pas mon SPA.

La question est donc de savoir comment dois-je servir mon SPA après avoir été redirigé depuis le serveur d'authentification?

18
Daniel

J'ai quelque chose qui semble fonctionner.

Dans mes recherches, je suis tombé sur apon ce post suggérant d'utiliser un middleware au lieu de l'attribut Authorize.

Maintenant, la méthode utilisée dans ce post authService ne semble pas fonctionner dans mon cas (aucune idée pourquoi, je vais continuer l'enquête et poster whaterver que je trouve plus tard).

J'ai donc décidé de choisir une solution plus simple. Voici ma config

        app.Use(async (context, next) =>
        {
            if (!context.User.Identity.IsAuthenticated)
            {
                await context.ChallengeAsync("oidc");
            }
            else
            {
                await next();
            }
        });

Dans ce cas, oidc entre en action AVANT l'application Spa et le flux fonctionne correctement. Pas besoin d'un contrôleur du tout.

HTH

16
Georges Legros

L'utilisation du logiciel intermédiaire de @ George nécessitera une authentification sur toutes les demandes. Si vous souhaitez l'exécuter uniquement pour localhost, ajoutez-le sous UseSpa enveloppé dans un bloc env.IsDevelopment ().

Une autre option qui fonctionne également bien pour les environnements déployés consiste à renvoyer le fichier index.html de la route de secours de votre spa.

Commencez:

        if (!env.IsDevelopment())
        {
            builder.UseMvc(routes =>
            {
                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new { controller = "Home", action = "AuthorizedSpaFallBack" });
            });
        }

HomeController:

[Authorize]
public IActionResult AuthorizedSpaFallBack()
{
    var file = _env.ContentRootFileProvider.GetFileInfo("ClientApp/dist/index.html");
    return PhysicalFile(file.PhysicalPath, "text/html");
}

Si vous avez besoin du fichier base.href pour correspondre à l'URL de demande du navigateur (par exemple, un cookie qui a une valeur Path), vous pouvez le modéliser avec une expression régulière (ou utiliser une vue rasoir comme les autres exemples).

    [Authorize]
    public IActionResult SpaFallback()
    {
        var fileInfo = _env.ContentRootFileProvider.GetFileInfo("ClientApp/dist/index.html");
        using (var reader = new StreamReader(fileInfo.CreateReadStream()))
        {
            var fileContent = reader.ReadToEnd();
            var basePath = !string.IsNullOrWhiteSpace(Url.Content("~")) ? Url.Content("~") + "/" : "/";

            //Note: basePath needs to match request path, because cookie.path is case sensitive
            fileContent = Regex.Replace(fileContent, "<base.*", $"<base href=\"{basePath}\">");
            return Content(fileContent, "text/html");
        }
    }
7
Cirem

Apportez cette modification à votre startup.cs:

app.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/home/index";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }
});

Mettez ensuite la référence à l'application angular dans l'index.cshtml:

<app-root></app-root>

et assurez-vous d'inclure tous les fichiers nécessaires dans le fichier index.cshtml ou votre mise en page:

<link href="~/styles.bundle.css" rel="stylesheet" />

<script type="text/javascript" src="~/inline.bundle.js" asp-append-version="true"></script>
<script type="text/javascript" src="~/polyfills.bundle.js" asp-append-version="true"></script>
<script type="text/javascript" src="~/vendor.bundle.js" asp-append-version="true"></script>
<script type="text/javascript" src="~/main.bundle.js" asp-append-version="true"></script>

Nous travaillons toujours sur les problèmes avec tous nos packages référencés, mais cela fera fonctionner le SPA de base derrière asp.net auth.

4
chris1out

Sur la base du Georges Legros, j'ai réussi à faire fonctionner cela pour .Net Core 3 avec Identity Server 4 (le projet VS prêt à l'emploi) afin que le pipeline app.UseSpa ne soit pas atteint si l'utilisateur n'est pas authentifié via le serveur d'identité d'abord. C'est beaucoup plus agréable car vous n'avez pas à attendre que le SPA se charge pour être redirigé vers la connexion.

Vous devez vous assurer que les autorisations/rôles fonctionnent correctement, sinon User.Identity.IsAuthenticated sera toujours faux.

public void ConfigureServices(IServiceCollection services)
{
    ...

    //Change the following pre-fab lines from

    //services.AddDefaultIdentity<ApplicationUser>()
    //    .AddEntityFrameworkStores<ApplicationDbContext>();

    //To

    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddRoles<IdentityRole>()
            //You might not need the following two settings
            .AddDefaultUI()
            .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

    ...
}

Ajoutez ensuite les éléments suivants pour configurer le canal suivant:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller}/{action=Index}/{id?}");
    });

    //Added this to redirect to Identity Server auth prior to loading SPA    
    app.Use(async (context, next) =>
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            await context.ChallengeAsync("Identity.Application");
        }
        else
        {
            await next();
        }
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
} 
1
Rob

On dirait qu'il n'y a pas de VRAIE solution quand on parle de SPA.

Afin d'exécuter une certaine logique dans le SPA, le SPA doit être initialement chargé.

Mais il y a une sorte d'astuce: dans le RouterModule, vous pouvez empêcher la navigation initiale comme indiqué:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'about',
    pathMatch: 'full'
  },
  {
    path: '**',
    redirectTo: 'about'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { initialNavigation: false })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Alors en toi app.component.ts vous pouvez prendre soin de votre authentification:

@Component({
  selector: 'flight-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private router: Router, private oauthService: OAuthService) {
    if (this.oauthService.isAuthenticated()) {
      this.router.navigate(['/home']);
    } else {
      // login Logic
    }
  }
}
1
Daniel