6 votes

openid connect - identification du locataire lors de la connexion

J'ai une application multi-locataires (base de données unique) qui permet d'utiliser le même nom d'utilisateur/email dans les différents locataires.

Au moment de la connexion (flux implicite), comment puis-je identifier le locataire ? J'ai pensé aux possibilités suivantes :

  1. Au moment de l'enregistrement, demandez à l'utilisateur de créer un compte. slug (nom de l'entreprise/du locataire) et, lors de la connexion, l'utilisateur doit fournir l'identifiant slug ainsi que username y password .

    Mais il n'y a pas de paramètre dans la requête open id pour envoyer le slug.

  2. Créer un OAuth au moment de l'enregistrement et de l'utilisation slug como client_id . Au moment du login pass slug en client_id que j'utiliserai pour récupérer l'identifiant du locataire et procéder à la validation de l'utilisateur.

Cette approche est-elle satisfaisante ?

Editer :

J'ai également essayé de faire en sorte que l'expression "slug" fasse partie du paramètre "route".

.EnableTokenEndpoint("/connect/{slug}/token");

mais openiddict ne le permet pas.

13voto

Pinpoint Points 486

L'approche suggérée par McGuire fonctionnera avec OpenIddict (vous pouvez accéder au fichier acr_values propriété par l'intermédiaire de OpenIdConnectRequest.AcrValues ) mais ce n'est pas l'option recommandée (ce n'est pas idéal du point de vue de la sécurité : comme l'émetteur est le même pour tous les locataires, ils finissent par partager les mêmes clés de signature).

En revanche, vous pouvez envisager de gérer un émetteur par locataire. Pour cela, vous avez au moins 2 options :

  • Donner Module OpenID d'OrchardCore un essai Il est basé sur OpenIddict et supporte nativement le multi-tenant. Il est encore en beta mais il est activement développé.

  • Remplacer le moniteur d'options utilisé par OpenIddict pour utiliser des options par locataire. .

Voici un exemple simplifié de la deuxième option, qui utilise un moniteur personnalisé et une résolution de locataire basée sur le chemin d'accès :

Mettez en œuvre la logique de résolution des problèmes des locataires. Par exemple

public class TenantProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantProvider(IHttpContextAccessor httpContextAccessor)
        => _httpContextAccessor = httpContextAccessor;

    public string GetCurrentTenant()
    {
        // This sample uses the path base as the tenant.
        // You can replace that by your own logic.
        string tenant = _httpContextAccessor.HttpContext.Request.PathBase;
        if (string.IsNullOrEmpty(tenant))
        {
            tenant = "default";
        }

        return tenant;
    }
}

public void Configure(IApplicationBuilder app)
{
    app.Use(next => context =>
    {
        // This snippet uses a hardcoded resolution logic.
        // In a real world app, you'd want to customize that.
        if (context.Request.Path.StartsWithSegments("/fabrikam", out PathString path))
        {
            context.Request.PathBase = "/fabrikam";
            context.Request.Path = path;
        }

        return next(context);
    });

    app.UseAuthentication();

    app.UseMvc();
}

Mettre en œuvre un IOptionsMonitor<OpenIddictServerOptions> :

public class OpenIddictServerOptionsProvider : IOptionsMonitor<OpenIddictServerOptions>
{
    private readonly ConcurrentDictionary<(string name, string tenant), Lazy<OpenIddictServerOptions>> _cache;
    private readonly IOptionsFactory<OpenIddictServerOptions> _optionsFactory;
    private readonly TenantProvider _tenantProvider;

    public OpenIddictServerOptionsProvider(
        IOptionsFactory<OpenIddictServerOptions> optionsFactory,
        TenantProvider tenantProvider)
    {
        _cache = new ConcurrentDictionary<(string, string), Lazy<OpenIddictServerOptions>>();
        _optionsFactory = optionsFactory;
        _tenantProvider = tenantProvider;
    }

    public OpenIddictServerOptions CurrentValue => Get(Options.DefaultName);

    public OpenIddictServerOptions Get(string name)
    {
        var tenant = _tenantProvider.GetCurrentTenant();

        Lazy<OpenIddictServerOptions> Create() => new Lazy<OpenIddictServerOptions>(() => _optionsFactory.Create(name));
        return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
    }

    public IDisposable OnChange(Action<OpenIddictServerOptions, string> listener) => null;
}

Mettre en œuvre un IConfigureNamedOptions<OpenIddictServerOptions> :

public class OpenIddictServerOptionsInitializer : IConfigureNamedOptions<OpenIddictServerOptions>
{
    private readonly IDataProtectionProvider _dataProtectionProvider;
    private readonly TenantProvider _tenantProvider;

    public OpenIddictServerOptionsInitializer(
        IDataProtectionProvider dataProtectionProvider,
        TenantProvider tenantProvider)
    {
        _dataProtectionProvider = dataProtectionProvider;
        _tenantProvider = tenantProvider;
    }

    public void Configure(string name, OpenIddictServerOptions options) => Configure(options);

    public void Configure(OpenIddictServerOptions options)
    {
        var tenant = _tenantProvider.GetCurrentTenant();

        // Create a tenant-specific data protection provider to ensure authorization codes,
        // access tokens and refresh tokens can't be read/decrypted by the other tenants.
        options.DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant);

        // Other tenant-specific options can be registered here.
    }
}

Enregistrez les services dans votre conteneur DI :

public void ConfigureServices(IServiceCollection services)
{
    // ...

    // Register the OpenIddict services.
    services.AddOpenIddict()
        .AddCore(options =>
        {
            // Register the Entity Framework stores.
            options.UseEntityFrameworkCore()
                   .UseDbContext<ApplicationDbContext>();
        })

        .AddServer(options =>
        {
            // Register the ASP.NET Core MVC binder used by OpenIddict.
            // Note: if you don't call this method, you won't be able to
            // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
            options.UseMvc();

            // Note: the following options are registered globally and will be applicable
            // to all the tenants. They can be overridden from OpenIddictServerOptionsInitializer.
            options.AllowAuthorizationCodeFlow();

            options.EnableAuthorizationEndpoint("/connect/authorize")
                   .EnableTokenEndpoint("/connect/token");

            options.DisableHttpsRequirement();
        });

    services.AddSingleton<TenantProvider>();
    services.AddSingleton<IOptionsMonitor<OpenIddictServerOptions>, OpenIddictServerOptionsProvider>();
    services.AddSingleton<IConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOptionsInitializer>();
}

Pour confirmer que cela fonctionne correctement, naviguez jusqu'à http://localhost :[port]/fabrikam/.well-known/openid-configuration (vous devriez obtenir une réponse JSON avec les métadonnées OpenID Connect).

2voto

McGuireV10 Points 380

Vous êtes sur la bonne voie avec le processus OAuth. Lorsque vous enregistrez le schéma OpenID Connect dans le code de démarrage de votre application web cliente, ajoutez un gestionnaire pour la fonction OnRedirectToIdentityProvider et l'utiliser pour ajouter votre valeur "slug" comme valeur ACR "locataire" (ce que l'OIDC appelle le "Référence de la classe de contexte d'authentification" ).

Voici un exemple de transmission au serveur :

.AddOpenIdConnect("tenant", options =>
{
    options.CallbackPath = "/signin-tenant";
    // other options omitted
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProvider = async context =>
        {
            string slug = await GetCurrentTenantAsync();
            context.ProtocolMessage.AcrValues = $"tenant:{slug}";
        }
    };
}

Vous n'avez pas précisé à quel type de serveur il est destiné, mais l'ACR (et la valeur "tenant") sont des éléments standard de l'OIDC. Si vous utilisez Identity Server 4, vous pouvez simplement injecter l'élément Service d'interaction dans la classe qui traite le login et lit le Tenant qui est automatiquement analysée à partir des valeurs ACR. Cet exemple est un code non fonctionnel pour plusieurs raisons, mais il démontre les parties importantes :

public class LoginModel : PageModel
{
    private readonly IIdentityServerInteractionService interaction;
    public LoginModel(IIdentityServerInteractionService interaction)
    {
        this.interaction = interaction;
    }

    public async Task<IActionResult> PostEmailPasswordLoginAsync()
    {
        var context = await interaction.GetAuthorizationContextAsync(returnUrl);
        if(context != null)
        {
            var slug = context.Tenant;
            // etc.
        }
    }
}

En ce qui concerne l'identification des comptes d'utilisateurs individuels, votre vie sera beaucoup plus facile si vous vous en tenez à la norme de l'OIDC qui consiste à utiliser "l'ID du sujet" comme identifiant unique de l'utilisateur. (En d'autres termes, faites-en la clé où vous stockez vos données utilisateur comme le "slug" du locataire, l'adresse électronique de l'utilisateur, le sel et le hachage du mot de passe, etc.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X