Skip to content

Commit

Permalink
#152 If -ClientId/-ClientSecret are passed CredentialFlow works. Syst…
Browse files Browse the repository at this point in the history
…em Browser crashes... Next is still interactive mode then deep dive into folder cmdlets based on OpenApi
  • Loading branch information
ddemeyer committed Dec 30, 2022
1 parent cc26a0a commit 435ef7c
Show file tree
Hide file tree
Showing 5 changed files with 508 additions and 10 deletions.
3 changes: 3 additions & 0 deletions Doc/TheExecution-ISHRemote-7.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ Add (nested binary module) AMRemote that could offer cmdlets like
# Next

* Align `Test-IshSession` with `New-IshSession` plus both need tests: `NewIshSession.Tests.ps1` and `TestIshSession.Tests.ps1`

* Go to async model, might be big investment, but theoretically is better, inspiration is on https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/blob/main/NetCoreConsoleClient/src/NetCoreConsoleClient/Program.cs

* Update github ticket that Access Management part of Tridion Docs 15/15.0.0 has an improvement where unattended *Service accounts* have to be explicitly created. Note that interactive logins are still allowed.
* Describe what Tridion Docs User Profile disable means, and when it kicks in.
* Describe when Last Log On is valid. Always on Access Management (ISHAM) User Profiles, even when logged in over Tridion Docs Identity Provider (ISHID) or any other federated Secure Token Service (STS). On Tridion Docs User Profile, so visible in Organize Space or through `Find-IShUser` cmdlet, only if you used Tridion Docs Identity Provider (ISHID).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,30 @@
* limitations under the License.
*/

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using IdentityModel.Client;
using IdentityModel.OidcClient;
using Trisoft.ISHRemote.Interfaces;
using Trisoft.ISHRemote.Objects;
using Trisoft.ISHRemote.OpenApiISH30;
using System.Diagnostics;

namespace Trisoft.ISHRemote.Connection
{
internal sealed class InfoShareOpenApiConnection : IDisposable
{
/// <summary>
/// Gets or sets when access token should be refreshed (relative to its expiration time).
/// </summary>
public TimeSpan RefreshBeforeExpiration { get; set; } = TimeSpan.FromMinutes(1);

#region Private Members
/// <summary>
/// Logger
Expand All @@ -50,7 +58,13 @@ internal sealed class InfoShareOpenApiConnection : IDisposable
private bool disposedValue;
#endregion


public class Tokens
{
internal string AccessToken { get; set; }
internal string IdentityToken { get; set; }
internal string RefreshToken { get; set; }
internal DateTime AccessTokenExpiration { get; set; }
}

#region Constructors
/// <summary>
Expand All @@ -69,8 +83,21 @@ public InfoShareOpenApiConnection(ILogger logger, HttpClient httpClient, InfoSha
_logger.WriteDebug($"InfoShareOpenApiConnection InfoShareWSUrl[{_connectionParameters.InfoShareWSUrl}] IssuerUrl[{_connectionParameters.IssuerUrl}] AuthenticationType[{_connectionParameters.AuthenticationType}]");
if (string.IsNullOrEmpty(_connectionParameters.BearerToken))
{
_logger.WriteDebug($"InfoShareOpenApiConnection ClientId[{_connectionParameters.ClientId}] ClientSecret[{new string('*', _connectionParameters.ClientSecret.Length)}]");
_connectionParameters.BearerToken = GetNewBearerToken();
if ((string.IsNullOrEmpty(_connectionParameters.ClientId)) || (string.IsNullOrEmpty(_connectionParameters.ClientSecret)))
{
// attempt System Browser retrieval of Access/Bearer Token
_logger.WriteDebug($"InfoShareOpenApiConnection System Browser");
Tokens tokens = GetTokensOverSystemBrowserAsync().GetAwaiter().GetResult();
}
else
{
// Raw method without OidcClient works
_logger.WriteDebug($"InfoShareOpenApiConnection ClientId[{_connectionParameters.ClientId}] ClientSecret[{new string('*', _connectionParameters.ClientSecret.Length)}]");
_connectionParameters.BearerToken = GetNewBearerToken();
// OidcClient fails
// Tokens tokens = GetTokensOverClientCredentialsAsync(null).GetAwaiter().GetResult();
// _connectionParameters.BearerToken = tokens.AccessToken;
}
}
else
{
Expand All @@ -86,6 +113,10 @@ public InfoShareOpenApiConnection(ILogger logger, HttpClient httpClient, InfoSha


#region Private Methods
/// <summary>
/// Rough get Bearer/Access token based on class parameters
/// </summary>
/// <returns>Bearer Token</returns>
private string GetNewBearerToken()
{
var requestUri = new Uri(_connectionParameters.IssuerUrl, "connect/token");
Expand All @@ -108,7 +139,122 @@ private string GetNewBearerToken()
return tokenObject.access_Token;
}

private void Dispose(bool disposing)
/// <summary>
/// OidcClient-based get Bearer/Access based on class parameters. Will refresh if possible.
/// </summary>
/// <param name="tokens">Incoming tokens, can be null. Forcing new Access Token, or attempt Refresh</param>
/// <param name="cancellationToken">Default</param>
/// <returns>New Tokens with new or refreshed valeus</returns>
private async Task<Tokens> GetTokensOverClientCredentialsAsync(Tokens tokens, CancellationToken cancellationToken = default)
{
var requestUri = new Uri(_connectionParameters.IssuerUrl, "connect/token");
Tokens returnTokens = null;
if ((tokens != null) && (tokens.AccessTokenExpiration.Add(RefreshBeforeExpiration) > DateTime.Now)) // skew 60 seconds
{
_logger.WriteDebug($"GetTokensOverClientCredentialsAsync from requestUri[{requestUri}] using ClientId[{_connectionParameters.ClientId}] RefreshToken[{new string('*', tokens.RefreshToken.Length)}]");
var refreshTokenRequest = new RefreshTokenRequest
{
Address = requestUri.ToString(),
ClientId = _connectionParameters.ClientId,
RefreshToken = tokens.RefreshToken
};
TokenResponse response = await _httpClient.RequestRefreshTokenAsync(refreshTokenRequest, cancellationToken).ConfigureAwait(false);
// initial usage response.IsError throws error about System.Runtime.CompilerServices.Unsafe v5 required, but OidcClient needs v6
if (response.HttpStatusCode != System.Net.HttpStatusCode.OK)
{
throw new ApplicationException($"GetTokensOverClientCredentialsAsync Refresh Error[{response.Error}]");
}
returnTokens = new Tokens
{
AccessToken = response.AccessToken,
IdentityToken = response.IdentityToken,
RefreshToken = response.RefreshToken,
AccessTokenExpiration = DateTime.Now.AddSeconds(response.ExpiresIn)
};
}
else // tokens where null, or expired
{
_logger.WriteDebug($"GetTokensOverClientCredentialsAsync from requestUri[{requestUri}] using ClientId[{_connectionParameters.ClientId}] ClientSecret[{new string('*', _connectionParameters.ClientSecret.Length)}]");
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = requestUri.ToString(),
ClientId = _connectionParameters.ClientId,
ClientSecret = _connectionParameters.ClientSecret
};
TokenResponse response = await _httpClient.RequestClientCredentialsTokenAsync(tokenRequest, cancellationToken).ConfigureAwait(false);

// initial usage response.IsError throws error about System.Runtime.CompilerServices.Unsafe v5 required, but OidcClient needs v6
if (response.HttpStatusCode != System.Net.HttpStatusCode.OK)
{
throw new ApplicationException($"GetTokensOverClientCredentialsAsync Access Error[{response.Error}]");
}

returnTokens = new Tokens
{
AccessToken = response.AccessToken,
RefreshToken = response.RefreshToken,
AccessTokenExpiration = DateTime.Now.AddSeconds(response.ExpiresIn)
};
}
return returnTokens;
}

private async Task<Tokens> GetTokensOverSystemBrowserAsync(CancellationToken cancellationToken = default)
{

using (var localHttpEndpoint = new InfoShareOpenIdConnectLocalHttpEndpoint())
{
var oidcClientOptions = new OidcClientOptions
{
Authority = _connectionParameters.IssuerUrl.ToString(),
ClientId = _connectionParameters.ClientId,
Scope = "openid profile email role forwarded offline_access",
RedirectUri = localHttpEndpoint.BaseUrl,
Policy = new Policy()
{
Discovery = new DiscoveryPolicy
{
ValidateIssuerName = false,
RequireHttps = false
}
}
};
var oidcClient = new OidcClient(oidcClientOptions);

AuthorizeState state = await oidcClient.PrepareLoginAsync(cancellationToken: cancellationToken);

localHttpEndpoint.StartListening();
// Open system browser to start the OIDC authentication flow
Process.Start(state.StartUrl);
// Wait for HTTP POST signalling end of authentication flow
localHttpEndpoint.AwaitHttpRequest(cancellationToken);
string formdata = localHttpEndpoint.GetHttpRequestBody();

// Send an HTTP Redirect to Access Management logged in page.
await localHttpEndpoint.SendHttpRedirectAsync($"{_connectionParameters.IssuerUrl}/Account/LoggedIn?clientId={_connectionParameters.ClientId}", cancellationToken);

LoginResult loginResult = await oidcClient.ProcessResponseAsync(formdata, state, cancellationToken: cancellationToken);
if (loginResult.IsError)
{
throw new ApplicationException($"GetTokensOverSystemBrowserAsync Error[{loginResult.Error}]");
}
if (string.IsNullOrEmpty(loginResult.AccessToken))
{
throw new ApplicationException($"GetTokensOverSystemBrowserAsync No Access Token received.");
}

var result = new Tokens
{
AccessToken = loginResult.AccessToken,
IdentityToken = loginResult.IdentityToken,
RefreshToken = loginResult.RefreshToken,
AccessTokenExpiration = loginResult.AccessTokenExpiration.LocalDateTime
};
return result;
}
}

private void Dispose(bool disposing)
{
if (!disposedValue)
{
Expand Down
Loading

0 comments on commit 435ef7c

Please sign in to comment.