Skip to content

Commit

Permalink
Merge pull request #10857 from abpframework/abpio/device
Browse files Browse the repository at this point in the history
Add device login flow to CLI.
  • Loading branch information
ebicoglu authored Jan 10, 2022
2 parents 9decaab + 1d996e1 commit b5b9fb3
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 72 deletions.
15 changes: 15 additions & 0 deletions framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ public async Task LoginAsync(string userName, string password, string organizati
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,52 +42,70 @@ public LoginCommand(AuthService authService,

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)
Expand Down Expand Up @@ -178,6 +196,7 @@ public string GetUsageInfo()
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("");
Expand Down
1 change: 1 addition & 0 deletions framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ private static async Task Main(string[] args)
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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;
Expand Down Expand Up @@ -114,53 +113,46 @@ protected virtual void SetAccessToken(HttpClient client, string accessToken)
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
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(configuration.CacheAbsoluteExpiration)
});
}

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);
Expand All @@ -169,25 +161,30 @@ protected virtual async Task<TokenResponse> GetTokenResponse(IdentityClientConfi
{
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,
Expand All @@ -197,27 +194,90 @@ protected virtual Task<PasswordTokenRequest> CreatePasswordTokenRequestAsync(str

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)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit b5b9fb3

Please sign in to comment.