Browse Source

Merge pull request #10857 from abpframework/abpio/device

Add device login flow to CLI.
pull/11254/head
albert 4 years ago
committed by GitHub
parent
commit
b5b9fb3293
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs
  2. 89
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs
  3. 1
      framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs
  4. 132
      framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs
  5. 5
      framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelDiscoveryDocumentCacheItem.cs

15
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;

89
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<bool> HasMultipleOrganizationAndThisNotSpecified(CommandLineArgs commandLineArgs, string organization)
@ -178,6 +196,7 @@ public class LoginCommand : IConsoleCommand, ITransientDependency
sb.AppendLine("Usage:");
sb.AppendLine(" abp login <username>");
sb.AppendLine(" abp login <username> -p <password>");
sb.AppendLine(" abp login <username> --device");
sb.AppendLine("");
sb.AppendLine("Example:");
sb.AppendLine("");

1
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

132
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<string> GetTokenEndpoint(IdentityClientConfiguration configuration)
protected virtual async Task<IdentityModelDiscoveryDocumentCacheItem> 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<DiscoveryDocumentResponse> 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<TokenResponse> 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<PasswordTokenRequest> CreatePasswordTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration)
protected virtual async Task<PasswordTokenRequest> 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<ClientCredentialsTokenRequest> CreateClientCredentialsTokenRequestAsync(string tokenEndpoint, IdentityClientConfiguration configuration)
protected virtual async Task<ClientCredentialsTokenRequest> 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<TokenResponse> 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)))

5
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)

Loading…
Cancel
Save