OpenId Connect : ClientId , ClientSecret et l'application console d'enregistrement d'une application #OIDC
Dans l'épisode précédent de "Sécurisez vos API avec OpenId Connect" nous avions vu l'existence de ClientId et ClientSecret de part son héritage d'Oauth. Cet article fait un zoom sur ces deux concepts.
ClientId
Le ClientId n'est rien d'autre qu'un identifiant représentant votre application. Aucun format n'est imposé par la spécification mais il doit être unique : un seul par plateforme. Il est de bon goût de le rendre difficile à deviner pour rendre plus difficile son exploitation par un acteur malveillant mais cela peut aussi être quelque chose décrivant l'application enregistrée.
Quelques exemples :
- Windows Live : 00000000400EDD04
- Facebook : 803375973059704
- GoogleConsole : 30590984682
- Github : 6779ef20e75817b79602
- Inwink : identifiant-comprehensible-par-un-humain_bfa182e9-a09e-49c5-82fc-28905aae8bbf
ClientSecret
Le Client Secret est une sorte de mot de passe applicatif. Il est donc TRES important de ne pas le stocker à un emplacement visible de tous tel qu'une application Angular SPA ou dans le code d'une application native. Il est donc réservé à un usage où sa sécurité n'est pas compromise et donc principalement pour des "composants serveurs" (web app, azure functions, post-it sur votre bureau, etc. ).
Aussi on distinguera les applications publiques (sans client_secret) et des applications privées (avec un client_secret).
Il s'agit d'un mot de passe et celui-ci doit donc respecter les normes de sécurité qui lui sont attachées. L'organisme Oauth recommande de générer une valeur de 256 caractères à l'aide d'un algorithme de cryptographie et de le convertir sous la forme d'une chaîne de caractères hexadécimale. Pour ma part je préfère utiliser des mots de passes longs et faciles à retenir.
Au fait pourquoi on utilise cela ?
Il est tout à fait possible d'autoriser une application sans ClientId de s'authentifier sur votre serveur. L'inconvénient de cette pratique est qu'il est impossible de savoir d'où provient une requête ultérieurement émise avec l'acces_token généré initialement.
En créant une "identité" pour chaque application utilisant votre serveur OIDC, cela permet notamment :
- De révoquer complètement l'accès d'une application aux ressources protégées si elle a été compromise.
- De tracer l'origine des demandes de connexion.
- Et surtout de donner des périmètres d'accès aux ressources différents par app (les scopes déjà évoqués précédemment).
Dynamic Client Registration
Il est possible de proposer un enregistrement à la volée des applications souhaitant utiliser votre serveur d'authentification. Cela est défini dans la spécification même d'OpenId Connect et permet d'exposer une URL sur votre serveur OIDC pour enregistrer une application. Attention cependant car dans ce scénario, vous n'avez pas forcément de contrôle sur qui va utiliser votre serveur pour authentifier des utilisateurs.
A ma connaissance, OpennIddict ne propose rien par défaut mais cela reste assez simple à mettre en place (utilisez le code ci-dessous par exemple :)).
Une application Console pour enregistrer une application avec OpenIdConnect
La plupart du temps et faute de temps les différentes applications sont enregistrées à la main dans la base de données avec un script SQL par quelqu'un de l'équipe technique.
Je ne trouve pas cela idéal car :
- cela nécessite de se connecter à une base de données avec un outil permettant d'exécuter du SQL,
- cela ne permet pas la mise en place de règles automatiques de validation de critères de base que vous instaurez (pertinence du mot de passe, etc.),
- cela ne permet pas d'être dans une Release pour du déploiement automatisé,
- cela ne permet pas de déléguer cette tâche de gestion à une équipe moins proche du code mis en place et plus à l'aise avec un executable classique.
Bref, j'aime bien avoir une application console associée à une documentation que je peux facilement donner à une autre personne dédiée à l'environnement de production.
La documentation d'OpenIddict donne un exemple de code à mettre facilement dans votre API pour permettre l'inscription d'une application sur votre serveur OIDC. Cela permet facilement la mise en place d'une console d'administration dans un site web et je vais réutiliser le même code dans une application console.
Pour cela je vais tricher (certains diront que c'est vraiment ... pas très propre) un peu en hébergeant l'API dans une application console le temps de bénéficier de la pipeline d'injection de dépendance pour récupérer les composants nécessaires et faire le travail de création d'une application.
La technique pour mettre en place cela est très simple :
- On créé une app console .NET CORE.
- On référence le projet correspondant au site où OIDC est en place.
- On héberge ce site web à l'aide de WebHost.
- On récupère l'OpenIddictManager à l'aide du service d'injection de dépendance.
- On enregistre l'application.
Pour récupérer la configuration depuis les arguments passés en ligne de commande, j'utilise les outils ASP.Net core (merci de m'avoir donné l'idée Vivien !). La classe MaConfig étant uniquement un POCO définissant les propriétés à lire.
internal static async Task Main(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); var clientIdGeneratorConfig = config.Get<MaConfig>(); ... } public class MaConfig { public string ClientId { get; set; } public string ClientSecret { get; set; } public string DisplayName { get; set; } public string RedirectUrl { get; set; } public string SqlConnectionString { get; set; } }
L'hosting de l'API est ensuite réalisé après avoir reproduit une configuration "conforme" :
var inMemoryConfig = new Dictionary<string, string> { {"ConnectionStrings:DefaultConnection", connectionString} }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(inMemoryConfig) .Build(); var webHost = WebHost.CreateDefaultBuilder() .UseStartup<StartupDeMonApi>() .UseConfiguration(configuration) .Build(); // démarrage de l'API await webHost.StartAsync();
Une fois cela fait, on peut récupérer le service d'injection de dépendances depuis notre webHost :
using (var serviceScope = webHost.Services.CreateScope()) { }
On peut alors utiliser le code proposé en example sur le GitHub OpenIddict tel quel. Il sera bien sûr nécessaire de choisir les scopes selon les nécessités du projet.
var manager = serviceScope.ServiceProvider .GetRequiredService< OpenIddictApplicationManager<OpenIddictApplication<long>>>(); var descriptor = new OpenIddictApplicationDescriptor { ClientId = myConfig.ClientId, ClientSecret = myConfig.ClientSecret, DisplayName = myConfig.DisplayName, Permissions = { OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.Password, OpenIddictConstants.Permissions.Scopes.Email, OpenIddictConstants.Permissions.Scopes.Profile, } }; await manager.CreateAsync(descriptor);
Voici un exemple d'appel avec passage de paramètres :
--ClientId=Kikou --ClientSecret=Lol --DisplayName="Une application" --SqlConnectionString="Server=tcp:deded;Initial Catalog=dev;Persist Security Info=False;User ID=inwink-sql-admin;Password=dededed;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"
Happy coding !
Commentaires