From 0ef544ad6ebb78af7f4645ca9a2361aac6c4c26f Mon Sep 17 00:00:00 2001 From: Caleb Kiage Date: Wed, 2 Mar 2022 15:02:24 +0300 Subject: [PATCH 1/5] Add client id and tenant id options to the login command --- .../Commands/Authentication/LoginCommand.cs | 25 ++++++--- .../IO/AuthenticationCacheUtility.cs | 56 +++++++++++++++++++ .../IO/IAuthenticationCacheUtility.cs | 7 +++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs create mode 100644 src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs diff --git a/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs b/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs index 23542c99..cd4dc214 100644 --- a/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs +++ b/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs @@ -1,8 +1,10 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Graph.Cli.Core.Authentication; using Microsoft.Graph.Cli.Core.Configuration; +using Microsoft.Graph.Cli.Core.IO; using Microsoft.Graph.Cli.Core.Utils; using System.CommandLine; @@ -18,20 +20,29 @@ public LoginCommand(AuthenticationServiceFactory authenticationServiceFactory) { public Command Build() { var loginCommand = new Command("login", "Login and store the session for use in subsequent commands"); - var scopes = new Option("--scopes", "The login scopes e.g. User.Read") { + var scopesOption = new Option("--scopes", "The login scopes e.g. User.Read") { Arity = ArgumentArity.OneOrMore }; - scopes.IsRequired = true; - loginCommand.AddOption(scopes); + scopesOption.IsRequired = true; + loginCommand.AddOption(scopesOption); - var strategy = new Option("--strategy", () => Constants.defaultAuthStrategy, "The authentication strategy to use."); - loginCommand.AddOption(strategy); - loginCommand.SetHandler(async (scopes, strategy, host, cancellationToken) => + var clientIdOption = new Option("--client-id", "The client id"); + loginCommand.AddOption(clientIdOption); + + var tenantIdOption = new Option("--tenant-id", "The tenant id"); + loginCommand.AddOption(tenantIdOption); + + var strategyOption = new Option("--strategy", () => Constants.defaultAuthStrategy, "The authentication strategy to use."); + loginCommand.AddOption(strategyOption); + + loginCommand.SetHandler(async (scopes, clientId, tenantId, strategy, host, cancellationToken) => { + var authUtil = host.Services.GetRequiredService(); var options = host.Services.GetRequiredService>().CurrentValue; var authService = await this.authenticationServiceFactory.GetAuthenticationServiceAsync(strategy, options?.TenantId, options?.ClientId, cancellationToken); await authService.LoginAsync(scopes, cancellationToken); - }, scopes, strategy); + await authUtil.SaveAuthenticationIdentifiersAsync(clientId, tenantId, cancellationToken); + }, scopesOption, clientIdOption, tenantIdOption, strategyOption); return loginCommand; } diff --git a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs new file mode 100644 index 00000000..5da53993 --- /dev/null +++ b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs @@ -0,0 +1,56 @@ +using System.Text; + +namespace Microsoft.Graph.Cli.Core.IO; + +public class AuthenticationCacheUtility : IAuthenticationCacheUtility { + private readonly IPathUtility pathUtility; + + private const string AUTHENTICATION_ID_FILE = "authentication-id"; + + public AuthenticationCacheUtility(IPathUtility pathUtility) + { + this.pathUtility = pathUtility; + } + + public async Task<(string, string)> ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var path = this.GetAuthenticationIdFilePath(); + if (!File.Exists(path)) { + throw new FileNotFoundException(); + } + + var text = await File.ReadAllTextAsync(path, cancellationToken); + if (string.IsNullOrWhiteSpace(text)) throw new AuthenticationIdentifierException("The authentication identifier cache file is empty"); + var split = text.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 2) throw new AuthenticationIdentifierException("The authentication identifier cache file cannot be parsed"); + + return (split[0], split[1]); + } + + public async Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)) { + cancellationToken.ThrowIfCancellationRequested(); + var path = this.GetAuthenticationIdFilePath(); + var data = $"{clientId}:{tenantId}"; + await File.WriteAllTextAsync(path, data, cancellationToken); + } + + private string GetAuthenticationIdFilePath() { + return Path.Join(pathUtility.GetApplicationDataDirectory(), AUTHENTICATION_ID_FILE); + } + + public class AuthenticationIdentifierException : Exception + { + public AuthenticationIdentifierException() + { + } + + public AuthenticationIdentifierException(string? message) : base(message) + { + } + + public AuthenticationIdentifierException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs new file mode 100644 index 00000000..1e77cc91 --- /dev/null +++ b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Graph.Cli.Core.IO; + +public interface IAuthenticationCacheUtility { + Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)); + + Task<(string, string)> ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default(CancellationToken)); +} \ No newline at end of file From 12187883c0c9d4f849d4de5ab9c99f960ddeaa77 Mon Sep 17 00:00:00 2001 From: Caleb Kiage Date: Wed, 2 Mar 2022 15:20:15 +0300 Subject: [PATCH 2/5] Update code owners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3d991e2a..87df9f6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @calebkiage @baywet @zengin @MichaelMainer @andrueastman @peombwa @jobala @samwelkanda \ No newline at end of file +* @calebkiage @baywet @zengin @MichaelMainer @andrueastman @peombwa @samwelkanda \ No newline at end of file From 2129cc9eaffab4f035f42d750afeb88503d6cde6 Mon Sep 17 00:00:00 2001 From: Caleb Kiage Date: Wed, 2 Mar 2022 17:07:03 +0300 Subject: [PATCH 3/5] Update src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs Co-authored-by: Eastman --- src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs index 5da53993..37d5de7b 100644 --- a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs +++ b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs @@ -28,7 +28,7 @@ public AuthenticationCacheUtility(IPathUtility pathUtility) return (split[0], split[1]); } - public async Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)) { + public async Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var path = this.GetAuthenticationIdFilePath(); var data = $"{clientId}:{tenantId}"; From 1aed0b73bd4641747ce33707298871aeb82c6928 Mon Sep 17 00:00:00 2001 From: Caleb Kiage Date: Wed, 2 Mar 2022 17:36:03 +0300 Subject: [PATCH 4/5] Store cached authentication identifiers as json --- .../Configuration/ConfigurationRoot.cs | 5 +++ .../IO/AuthenticationCacheUtility.cs | 39 +++++++++++-------- .../IO/IAuthenticationCacheUtility.cs | 6 ++- .../utils/Constants.cs | 4 +- 4 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 src/Microsoft.Graph.Cli.Core/Configuration/ConfigurationRoot.cs diff --git a/src/Microsoft.Graph.Cli.Core/Configuration/ConfigurationRoot.cs b/src/Microsoft.Graph.Cli.Core/Configuration/ConfigurationRoot.cs new file mode 100644 index 00000000..4c0f3ec8 --- /dev/null +++ b/src/Microsoft.Graph.Cli.Core/Configuration/ConfigurationRoot.cs @@ -0,0 +1,5 @@ +namespace Microsoft.Graph.Cli.Core.Configuration; + +public class ConfigurationRoot { + public AuthenticationOptions AuthenticationOptions { get; set; } = new AuthenticationOptions(); +} \ No newline at end of file diff --git a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs index 5da53993..29b9e0b3 100644 --- a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs +++ b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs @@ -1,42 +1,49 @@ using System.Text; +using System.Text.Json; +using Microsoft.Graph.Cli.Core.Configuration; +using Microsoft.Graph.Cli.Core.Utils; namespace Microsoft.Graph.Cli.Core.IO; public class AuthenticationCacheUtility : IAuthenticationCacheUtility { private readonly IPathUtility pathUtility; - private const string AUTHENTICATION_ID_FILE = "authentication-id"; - public AuthenticationCacheUtility(IPathUtility pathUtility) { this.pathUtility = pathUtility; } - public async Task<(string, string)> ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default) + public string GetAuthenticationCacheFilePath() + { + return Path.Join(pathUtility.GetApplicationDataDirectory(), Constants.AuthenticationIdCachePath); + } + + public async Task ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - var path = this.GetAuthenticationIdFilePath(); + var path = this.GetAuthenticationCacheFilePath(); if (!File.Exists(path)) { throw new FileNotFoundException(); } - var text = await File.ReadAllTextAsync(path, cancellationToken); - if (string.IsNullOrWhiteSpace(text)) throw new AuthenticationIdentifierException("The authentication identifier cache file is empty"); - var split = text.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (split.Length != 2) throw new AuthenticationIdentifierException("The authentication identifier cache file cannot be parsed"); + using var fileStream = File.OpenRead(path); + var configRoot = await JsonSerializer.DeserializeAsync(fileStream, cancellationToken: cancellationToken); + if (configRoot?.AuthenticationOptions is null) throw new AuthenticationIdentifierException("Cannot find cached authentication identifiers."); - return (split[0], split[1]); + return configRoot.AuthenticationOptions; } public async Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - var path = this.GetAuthenticationIdFilePath(); - var data = $"{clientId}:{tenantId}"; - await File.WriteAllTextAsync(path, data, cancellationToken); - } - - private string GetAuthenticationIdFilePath() { - return Path.Join(pathUtility.GetApplicationDataDirectory(), AUTHENTICATION_ID_FILE); + var path = this.GetAuthenticationCacheFilePath(); + var configuration = new Configuration.ConfigurationRoot { + AuthenticationOptions = new AuthenticationOptions { + ClientId = clientId, + TenantId = tenantId + } + }; + using FileStream fileStream = File.OpenWrite(path); + await JsonSerializer.SerializeAsync(fileStream, configuration, cancellationToken: cancellationToken); } public class AuthenticationIdentifierException : Exception diff --git a/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs index 1e77cc91..b09cbd93 100644 --- a/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs +++ b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs @@ -1,7 +1,11 @@ +using Microsoft.Graph.Cli.Core.Configuration; + namespace Microsoft.Graph.Cli.Core.IO; public interface IAuthenticationCacheUtility { + string GetAuthenticationCacheFilePath(); + Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)); - Task<(string, string)> ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default(CancellationToken)); + Task ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default(CancellationToken)); } \ No newline at end of file diff --git a/src/Microsoft.Graph.Cli.Core/utils/Constants.cs b/src/Microsoft.Graph.Cli.Core/utils/Constants.cs index 2ecc1eff..fff11a55 100644 --- a/src/Microsoft.Graph.Cli.Core/utils/Constants.cs +++ b/src/Microsoft.Graph.Cli.Core/utils/Constants.cs @@ -8,9 +8,9 @@ public class Constants public const string AuthRecordPath = "authRecord"; - public const string TokenCacheName = "MicrosoftGraph"; + public const string AuthenticationIdCachePath = "authentication-id-cache.json"; - public const string AuthenticationSection = "Authentication"; + public const string TokenCacheName = "MicrosoftGraph"; public const AuthenticationStrategy defaultAuthStrategy = AuthenticationStrategy.DeviceCode; } From df9a00049420e3fd61495e6296997bacf9333c69 Mon Sep 17 00:00:00 2001 From: Caleb Kiage Date: Wed, 2 Mar 2022 18:21:44 +0300 Subject: [PATCH 5/5] Allow nulls for client and tenant ids --- .../Commands/Authentication/LoginCommand.cs | 5 ++--- .../IO/AuthenticationCacheUtility.cs | 2 +- .../IO/IAuthenticationCacheUtility.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs b/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs index cd4dc214..bf35f0ae 100644 --- a/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs +++ b/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs @@ -35,11 +35,10 @@ public Command Build() { var strategyOption = new Option("--strategy", () => Constants.defaultAuthStrategy, "The authentication strategy to use."); loginCommand.AddOption(strategyOption); - loginCommand.SetHandler(async (scopes, clientId, tenantId, strategy, host, cancellationToken) => + loginCommand.SetHandler(async (scopes, clientId, tenantId, strategy, host, cancellationToken) => { var authUtil = host.Services.GetRequiredService(); - var options = host.Services.GetRequiredService>().CurrentValue; - var authService = await this.authenticationServiceFactory.GetAuthenticationServiceAsync(strategy, options?.TenantId, options?.ClientId, cancellationToken); + var authService = await this.authenticationServiceFactory.GetAuthenticationServiceAsync(strategy, tenantId, clientId, cancellationToken); await authService.LoginAsync(scopes, cancellationToken); await authUtil.SaveAuthenticationIdentifiersAsync(clientId, tenantId, cancellationToken); }, scopesOption, clientIdOption, tenantIdOption, strategyOption); diff --git a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs index 87537cac..7cdbb1c3 100644 --- a/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs +++ b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs @@ -33,7 +33,7 @@ public async Task ReadAuthenticationIdentifiersAsync(Canc return configRoot.AuthenticationOptions; } - public async Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default) { + public async Task SaveAuthenticationIdentifiersAsync(string? clientId, string? tenantId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var path = this.GetAuthenticationCacheFilePath(); var configuration = new Configuration.ConfigurationRoot { diff --git a/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs index b09cbd93..3086cc9d 100644 --- a/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs +++ b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs @@ -5,7 +5,7 @@ namespace Microsoft.Graph.Cli.Core.IO; public interface IAuthenticationCacheUtility { string GetAuthenticationCacheFilePath(); - Task SaveAuthenticationIdentifiersAsync(string clientId, string tenantId, CancellationToken cancellationToken = default(CancellationToken)); + Task SaveAuthenticationIdentifiersAsync(string? clientId, string? tenantId, CancellationToken cancellationToken = default(CancellationToken)); Task ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default(CancellationToken)); } \ No newline at end of file