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 diff --git a/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs b/src/Microsoft.Graph.Cli.Core/Commands/Authentication/LoginCommand.cs index 23542c99..bf35f0ae 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,28 @@ 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 options = host.Services.GetRequiredService>().CurrentValue; - var authService = await this.authenticationServiceFactory.GetAuthenticationServiceAsync(strategy, options?.TenantId, options?.ClientId, cancellationToken); + var authUtil = host.Services.GetRequiredService(); + var authService = await this.authenticationServiceFactory.GetAuthenticationServiceAsync(strategy, tenantId, 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/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 new file mode 100644 index 00000000..7cdbb1c3 --- /dev/null +++ b/src/Microsoft.Graph.Cli.Core/IO/AuthenticationCacheUtility.cs @@ -0,0 +1,63 @@ +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; + + public AuthenticationCacheUtility(IPathUtility pathUtility) + { + this.pathUtility = pathUtility; + } + + public string GetAuthenticationCacheFilePath() + { + return Path.Join(pathUtility.GetApplicationDataDirectory(), Constants.AuthenticationIdCachePath); + } + + public async Task ReadAuthenticationIdentifiersAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var path = this.GetAuthenticationCacheFilePath(); + if (!File.Exists(path)) { + throw new FileNotFoundException(); + } + + 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 configRoot.AuthenticationOptions; + } + + public async Task SaveAuthenticationIdentifiersAsync(string? clientId, string? tenantId, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + 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 + { + 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..3086cc9d --- /dev/null +++ b/src/Microsoft.Graph.Cli.Core/IO/IAuthenticationCacheUtility.cs @@ -0,0 +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 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; }