diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs index 8997ee583c..c3ce0afb54 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs @@ -92,6 +92,21 @@ public class AuthService : IAuthService, ITransientDependency File.WriteAllText(CliPaths.AccessToken, accessToken, Encoding.UTF8); } + public async Task DeviceLoginAsync() + { + var configuration = new IdentityClientConfiguration( + CliUrls.AccountAbpIo, + "role email abpio abpio_www abpio_commercial openid offline_access", + "abp-cli", + "1q2w3e*", + OidcConstants.GrantTypes.DeviceCode + ); + + var accessToken = await AuthenticationService.GetAccessTokenAsync(configuration); + + File.WriteAllText(CliPaths.AccessToken, accessToken, Encoding.UTF8); + } + public async Task LogoutAsync() { string accessToken = null; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs index d98beb268d..a3b6d4a411 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs @@ -42,52 +42,70 @@ public class LoginCommand : IConsoleCommand, ITransientDependency public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { - if (commandLineArgs.Target.IsNullOrEmpty()) + if (!commandLineArgs.Options.ContainsKey("device")) { - throw new CliUsageException( - "Username name is missing!" + - Environment.NewLine + Environment.NewLine + - GetUsageInfo() - ); - } - - var organization = commandLineArgs.Options.GetOrNull(Options.Organization.Short, Options.Organization.Long); - - if (await HasMultipleOrganizationAndThisNotSpecified(commandLineArgs, organization)) - { - return; - } - - var password = commandLineArgs.Options.GetOrNull(Options.Password.Short, Options.Password.Long); - if (password == null) - { - Console.Write("Password: "); - password = ConsoleHelper.ReadSecret(); - if (password.IsNullOrWhiteSpace()) + if (commandLineArgs.Target.IsNullOrEmpty()) { throw new CliUsageException( - "Password is missing!" + + "Username name is missing!" + Environment.NewLine + Environment.NewLine + GetUsageInfo() ); } - } - try - { - await AuthService.LoginAsync( - commandLineArgs.Target, - password, - organization - ); + var organization = commandLineArgs.Options.GetOrNull(Options.Organization.Short, Options.Organization.Long); + + if (await HasMultipleOrganizationAndThisNotSpecified(commandLineArgs, organization)) + { + return; + } + + var password = commandLineArgs.Options.GetOrNull(Options.Password.Short, Options.Password.Long); + if (password == null) + { + Console.Write("Password: "); + password = ConsoleHelper.ReadSecret(); + if (password.IsNullOrWhiteSpace()) + { + throw new CliUsageException( + "Password is missing!" + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } + } + + try + { + await AuthService.LoginAsync( + commandLineArgs.Target, + password, + organization + ); + } + catch (Exception ex) + { + LogCliError(ex, commandLineArgs); + return; + } + + Logger.LogInformation($"Successfully logged in as '{commandLineArgs.Target}'"); } - catch (Exception ex) + else { - LogCliError(ex, commandLineArgs); - return; - } + try + { + await AuthService.DeviceLoginAsync(); + } + catch (Exception ex) + { + LogCliError(ex, commandLineArgs); + return; + } - Logger.LogInformation($"Successfully logged in as '{commandLineArgs.Target}'"); + var loginInfo = await AuthService.GetLoginInfoAsync(); + Logger.LogInformation($"Successfully logged in as '{loginInfo.Username}'"); + } } private async Task HasMultipleOrganizationAndThisNotSpecified(CommandLineArgs commandLineArgs, string organization) @@ -178,6 +196,7 @@ public class LoginCommand : IConsoleCommand, ITransientDependency sb.AppendLine("Usage:"); sb.AppendLine(" abp login "); sb.AppendLine(" abp login -p "); + sb.AppendLine(" abp login --device"); sb.AppendLine(""); sb.AppendLine("Example:"); sb.AppendLine(""); diff --git a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs index a9a143c7a7..44c809e697 100644 --- a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs +++ b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs @@ -18,6 +18,7 @@ public class Program .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) + .MinimumLevel.Override("Volo.Abp.IdentityModel", LogEventLevel.Information) #if DEBUG .MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Debug) #else diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs index 217bcb98ab..6e98a047bb 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; -using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -114,22 +113,34 @@ public class IdentityModelAuthenticationService : IIdentityModelAuthenticationSe client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } - protected virtual async Task GetTokenEndpoint(IdentityClientConfiguration configuration) + protected virtual async Task GetDiscoveryResponse(IdentityClientConfiguration configuration) { - //TODO: Can use (configuration.Authority + /connect/token) directly? - var tokenEndpointUrlCacheKey = CalculateDiscoveryDocumentCacheKey(configuration); var discoveryDocumentCacheItem = await DiscoveryDocumentCache.GetAsync(tokenEndpointUrlCacheKey); if (discoveryDocumentCacheItem == null) { - var discoveryResponse = await GetDiscoveryResponse(configuration); + DiscoveryDocumentResponse discoveryResponse; + using (var httpClient = HttpClientFactory.CreateClient(HttpClientName)) + { + var request = new DiscoveryDocumentRequest + { + Address = configuration.Authority, + Policy = + { + RequireHttps = configuration.RequireHttps + } + }; + IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage?.Invoke(request); + discoveryResponse = await httpClient.GetDiscoveryDocumentAsync(request); + } + if (discoveryResponse.IsError) { throw new AbpException($"Could not retrieve the OpenId Connect discovery document! " + $"ErrorType: {discoveryResponse.ErrorType}. Error: {discoveryResponse.Error}"); } - discoveryDocumentCacheItem = new IdentityModelDiscoveryDocumentCacheItem(discoveryResponse.TokenEndpoint); + discoveryDocumentCacheItem = new IdentityModelDiscoveryDocumentCacheItem(discoveryResponse.TokenEndpoint, discoveryResponse.DeviceAuthorizationEndpoint); await DiscoveryDocumentCache.SetAsync(tokenEndpointUrlCacheKey, discoveryDocumentCacheItem, new DistributedCacheEntryOptions { @@ -137,30 +148,11 @@ public class IdentityModelAuthenticationService : IIdentityModelAuthenticationSe }); } - return discoveryDocumentCacheItem.TokenEndpoint; - } - - protected virtual async Task GetDiscoveryResponse(IdentityClientConfiguration configuration) - { - using (var httpClient = HttpClientFactory.CreateClient(HttpClientName)) - { - var request = new DiscoveryDocumentRequest - { - Address = configuration.Authority, - Policy = - { - RequireHttps = configuration.RequireHttps - } - }; - IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage?.Invoke(request); - return await httpClient.GetDiscoveryDocumentAsync(request); - } + return discoveryDocumentCacheItem; } protected virtual async Task GetTokenResponse(IdentityClientConfiguration configuration) { - var tokenEndpoint = await GetTokenEndpoint(configuration); - using (var httpClient = HttpClientFactory.CreateClient(HttpClientName)) { AddHeaders(httpClient); @@ -169,25 +161,30 @@ public class IdentityModelAuthenticationService : IIdentityModelAuthenticationSe { case OidcConstants.GrantTypes.ClientCredentials: return await httpClient.RequestClientCredentialsTokenAsync( - await CreateClientCredentialsTokenRequestAsync(tokenEndpoint, configuration), + await CreateClientCredentialsTokenRequestAsync(configuration), CancellationTokenProvider.Token ); case OidcConstants.GrantTypes.Password: return await httpClient.RequestPasswordTokenAsync( - await CreatePasswordTokenRequestAsync(tokenEndpoint, configuration), + await CreatePasswordTokenRequestAsync(configuration), CancellationTokenProvider.Token ); + + case OidcConstants.GrantTypes.DeviceCode: + return await RequestDeviceAuthorizationAsync(httpClient, configuration); + default: throw new AbpException("Grant type was not implemented: " + configuration.GrantType); } } } - protected virtual Task CreatePasswordTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration) + protected virtual async Task CreatePasswordTokenRequestAsync(IdentityClientConfiguration configuration) { + var discoveryResponse = await GetDiscoveryResponse(configuration); var request = new PasswordTokenRequest { - Address = tokenEndpoint, + Address = discoveryResponse.TokenEndpoint, Scope = configuration.Scope, ClientId = configuration.ClientId, ClientSecret = configuration.ClientSecret, @@ -197,27 +194,90 @@ public class IdentityModelAuthenticationService : IIdentityModelAuthenticationSe IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage?.Invoke(request); - AddParametersToRequestAsync(configuration, request); + await AddParametersToRequestAsync(configuration, request); - return Task.FromResult(request); + return request; } - protected virtual Task CreateClientCredentialsTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration) + protected virtual async Task CreateClientCredentialsTokenRequestAsync(IdentityClientConfiguration configuration) { + var discoveryResponse = await GetDiscoveryResponse(configuration); var request = new ClientCredentialsTokenRequest { - Address = tokenEndpoint, + Address = discoveryResponse.TokenEndpoint, Scope = configuration.Scope, ClientId = configuration.ClientId, ClientSecret = configuration.ClientSecret }; IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage?.Invoke(request); - AddParametersToRequestAsync(configuration, request); + await AddParametersToRequestAsync(configuration, request); + + return request; + } + + protected virtual async Task RequestDeviceAuthorizationAsync(HttpClient httpClient, IdentityClientConfiguration configuration) + { + var discoveryResponse = await GetDiscoveryResponse(configuration); + var request = new DeviceAuthorizationRequest() + { + Address = discoveryResponse.DeviceAuthorizationEndpoint, + Scope = configuration.Scope, + ClientId = configuration.ClientId, + ClientSecret = configuration.ClientSecret, + }; + + IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage?.Invoke(request); + + await AddParametersToRequestAsync(configuration, request); + + var response = await httpClient.RequestDeviceAuthorizationAsync(request); + if (response.IsError) + { + throw new AbpException(response.ErrorDescription); + } + + Logger.LogInformation($"First copy your one-time code: {response.UserCode}"); + Logger.LogInformation($"Open {response.VerificationUri} in your browser..."); + + for (var i = 0; i < ((response.ExpiresIn ?? 300) / response.Interval + 1); i++) + { + await Task.Delay(response.Interval * 1000); + + var tokenResponse = await httpClient.RequestDeviceTokenAsync(new DeviceTokenRequest + { + Address = discoveryResponse.TokenEndpoint, + ClientId = configuration.ClientId, + ClientSecret = configuration.ClientSecret, + DeviceCode = response.DeviceCode + }); - return Task.FromResult(request); + if (tokenResponse.IsError) + { + switch (tokenResponse.Error) + { + case "slow_down": + case "authorization_pending": + break; + + case "expired_token": + throw new AbpException("This 'device_code' has expired. (expired_token)"); + + case "access_denied": + throw new AbpException("User denies the request(access_denied)"); + } + } + + if (!tokenResponse.IsError) + { + return tokenResponse; + } + } + + throw new AbpException("Timeout!"); } + protected virtual Task AddParametersToRequestAsync(IdentityClientConfiguration configuration, ProtocolRequest request) { foreach (var pair in configuration.Where(p => p.Key.StartsWith("[o]", StringComparison.OrdinalIgnoreCase))) diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs index 4810d5a3c4..0fbf50d020 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs @@ -9,14 +9,17 @@ public class IdentityModelDiscoveryDocumentCacheItem { public string TokenEndpoint { get; set; } + public string DeviceAuthorizationEndpoint { get; set; } + public IdentityModelDiscoveryDocumentCacheItem() { } - public IdentityModelDiscoveryDocumentCacheItem(string tokenEndpoint) + public IdentityModelDiscoveryDocumentCacheItem(string tokenEndpoint, string deviceAuthorizationEndpoint) { TokenEndpoint = tokenEndpoint; + DeviceAuthorizationEndpoint = deviceAuthorizationEndpoint; } public static string CalculateCacheKey(IdentityClientConfiguration configuration)