Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .azure-pipelines/generate-auth-module-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ parameters:
displayName: 'Build Number'
type: string
default: $[format('{0:yyMMddHH}', pipeline.startTime)]
- name: AZURESUBSCRIPTION
default: "Microsoft Graph Build Agents (Win+Lin)"
displayName: Azure Subscription

- name: KEYVAULT
default: "msgraph-build-keyvault"
displayName: Build Key vault


jobs:
- job: MsGraphPSSDKAuthModuleGeneration
Expand All @@ -29,6 +37,26 @@ jobs:
steps:
- template: ./install-tools-template.yml

- task: AzureKeyVault@1
inputs:
azureSubscription: $(AZURESUBSCRIPTION)
KeyVaultName: $(KEYVAULT)
SecretsFilter: '*'
RunAsPreJob: true

- task: PowerShell@2
displayName: 'Install Test Certificate'
inputs:
targetType: 'inline'
script: |
$kvSecretBytes = [System.Convert]::FromBase64String('$(MsGraphPSSDKCertificate)')
$certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$certCollection.Import($kvSecretBytes,$null,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My", "CurrentUser")
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$store.AddRange($certCollection)
$store.Close()

- task: PowerShell@2
displayName: 'Generate and Build Auth Module'
inputs:
Expand Down
2 changes: 2 additions & 0 deletions .azure-pipelines/integrated-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ stages:
AUTH_MODULE_PATH: $(AUTH_MODULE_PATH)
EnableSigning: true
BUILDNUMBER: $(BUILDNUMBER)
KEYVAULT: $(KEYVAULT)
AZURESUBSCRIPTION: $(AZURESUBSCRIPTION)

- stage: GenerateBetaModules
displayName: 'Generate Beta Modules (Microsoft.Graph.*)'
Expand Down
80 changes: 57 additions & 23 deletions src/Authentication/Authentication.Core/Authenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Graph.Authentication.Core
using Microsoft.Graph.PowerShell.Authentication.Core;
using Microsoft.Graph.PowerShell.Authentication.Helpers;
using Microsoft.Identity.Client;

using System;
using System.Collections.Generic;
using System.Globalization;
Expand All @@ -28,24 +29,41 @@ public static class Authenticator
/// <param name="authContext">The <see cref="IAuthContext"/> to authenticate.</param>
/// <param name="forceRefresh">Whether or not to force refresh a token if one exists.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="fallBackWarning">Callback to report FallBack to DeviceCode Authentication</param>
/// <returns></returns>
public static async Task<IAuthContext> AuthenticateAsync(IAuthContext authContext, bool forceRefresh, CancellationToken cancellationToken)
public static async Task<IAuthContext> AuthenticateAsync(IAuthContext authContext, bool forceRefresh, CancellationToken cancellationToken, Action fallBackWarning = null)
{
try
// Gets a static instance of IAuthenticationProvider when the client app hasn't changed.
var authProvider = AuthenticationHelpers.GetAuthProvider(authContext);
IClientApplicationBase clientApplication = null;
switch (authContext.AuthProviderType)
{
// Gets a static instance of IAuthenticationProvider when the client app hasn't changed.
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(authContext);
IClientApplicationBase clientApplication = null;
if (authContext.AuthType == AuthenticationType.Delegated)
{
case AuthProviderType.DeviceCodeProvider:
case AuthProviderType.DeviceCodeProviderFallBack:
clientApplication = (authProvider as DeviceCodeProvider).ClientApplication;
}
if (authContext.AuthType == AuthenticationType.AppOnly)
{
clientApplication = (authProvider as ClientCredentialProvider).ClientApplication;
}

// Incremental scope consent without re-instantiating the auth provider. We will use a static instance.
break;
case AuthProviderType.InteractiveAuthenticationProvider:
{
var interactiveProvider = (authProvider as InteractiveAuthenticationProvider).ClientApplication;
//When User is not Interactive, Pre-Emptively Fallback and warn, to DeviceCode
if (!interactiveProvider.IsUserInteractive())
{
authContext.AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack;
fallBackWarning?.Invoke();
var fallBackAuthContext= await AuthenticateAsync(authContext, forceRefresh, cancellationToken, fallBackWarning);
return fallBackAuthContext;
}
break;
}
case AuthProviderType.ClientCredentialProvider:
{
clientApplication = (authProvider as ClientCredentialProvider).ClientApplication;
break;
}
}
try
{
// Incremental scope consent without re-instantiating the auth provider. We will use provided instance.
GraphRequestContext graphRequestContext = new GraphRequestContext();
graphRequestContext.CancellationToken = cancellationToken;
graphRequestContext.MiddlewareOptions = new Dictionary<string, IMiddlewareOption>
Expand Down Expand Up @@ -81,18 +99,27 @@ public static async Task<IAuthContext> AuthenticateAsync(IAuthContext authContex
}
catch (AuthenticationException authEx)
{
if ((authEx.InnerException is TaskCanceledException) && cancellationToken.IsCancellationRequested)
//Interactive Authentication Failure: Could Not Open Browser, fallback to DeviceAuth
if (IsUnableToOpenWebPageError(authEx))
{
// DeviceCodeTimeout
throw new Exception(string.Format(
CultureInfo.CurrentCulture,
ErrorConstants.Message.DeviceCodeTimeout,
Constants.MaxDeviceCodeTimeOut));
authContext.AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack;
//ReAuthenticate using DeviceCode as fallback.
var fallBackAuthContext = await AuthenticateAsync(authContext, forceRefresh, cancellationToken);
//Indicate that this was a Fallback
if (fallBackWarning != null && fallBackAuthContext.AuthProviderType == AuthProviderType.DeviceCodeProviderFallBack)
{
fallBackWarning();
}
return fallBackAuthContext;
}
else
// DeviceCode Authentication Failure: Timeout
if (authEx.InnerException is TaskCanceledException && cancellationToken.IsCancellationRequested)
{
throw authEx.InnerException ?? authEx;
// DeviceCodeTimeout
throw new Exception(string.Format(CultureInfo.CurrentCulture, ErrorConstants.Message.DeviceCodeTimeout, Constants.MaxDeviceCodeTimeOut));
}
//Something Unknown Went Wrong
throw authEx.InnerException ?? authEx;
}
catch (Exception ex)
{
Expand All @@ -108,5 +135,12 @@ public static void LogOut(IAuthContext authContext)
{
AuthenticationHelpers.Logout(authContext);
}

private static bool IsUnableToOpenWebPageError(Exception exception)
{
return exception.InnerException is MsalClientException clientException &&
clientException?.ErrorCode == MsalError.LinuxXdgOpen ||
(exception.Message?.ToLower()?.Contains("unable to open a web page") ?? false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ public enum ContextScope
Process,
CurrentUser
}

public enum AuthProviderType
{
InteractiveAuthenticationProvider,
DeviceCodeProvider,
DeviceCodeProviderFallBack,
ClientCredentialProvider,
UserProvidedToken
}
public interface IAuthContext
{
string ClientId { get; set; }
string TenantId { get; set; }
string CertificateThumbprint { get; set; }
string[] Scopes { get; set; }
AuthenticationType AuthType { get; set; }
AuthProviderType AuthProviderType { get; set; }
string CertificateName { get; set; }
string Account { get; set; }
string AppName { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp2.1;net461</TargetFrameworks>
<RootNamespace>Microsoft.Graph.PowerShell.Authentication.Core</RootNamespace>
<Version>1.4.2</Version>
<Version>1.4.3</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.6" />
<PackageReference Include="Microsoft.Graph.Core" Version="1.23.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.23.0" />
<PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.7" />
<PackageReference Include="Microsoft.Graph.Core" Version="1.25.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.29.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="5.6.0" />
<PackageReference Include="Microsoft.IdentityModel.Logging" Version="5.6.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="5.6.0" />
<PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.19041.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
<!--Always ensure this version matches the versions of System.Security.Cryptography.* dependencies of Microsoft.Identity.Client when targeting PowerShell 6 and below.-->
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.3.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,44 +67,51 @@ public static IAuthenticationProvider GetAuthProvider(IAuthContext authContext)
{
case AuthenticationType.Delegated:
{
//Specify Default RedirectUri
//https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser
IPublicClientApplication publicClientApp = PublicClientApplicationBuilder
.Create(authContext.ClientId)
.WithTenantId(authContext.TenantId)
.WithAuthority(authorityUrl)
.WithClientCapabilities(new[] { "cp1" })
.Build();

.Create(authContext.ClientId)
.WithTenantId(authContext.TenantId)
.WithAuthority(authorityUrl)
.WithClientCapabilities(new[] { "cp1" })
.WithDefaultRedirectUri()
.Build();
ConfigureTokenCache(publicClientApp.UserTokenCache, authContext);
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes, async (result) =>
switch (authContext.AuthProviderType)
{
await Console.Out.WriteLineAsync(result.Message);
});
case AuthProviderType.DeviceCodeProvider:
case AuthProviderType.DeviceCodeProviderFallBack:
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes,
async result => { await Console.Out.WriteLineAsync(result.Message); });
break;
case AuthProviderType.InteractiveAuthenticationProvider:
authProvider = new InteractiveAuthenticationProvider(publicClientApp, authContext.Scopes);
break;
}
break;
}
case AuthenticationType.AppOnly:
{
IConfidentialClientApplication confidentialClientApp = ConfidentialClientApplicationBuilder
.Create(authContext.ClientId)
.WithTenantId(authContext.TenantId)
.WithAuthority(authorityUrl)
.WithCertificate(GetCertificate(authContext))
.Build();
.Create(authContext.ClientId)
.WithTenantId(authContext.TenantId)
.WithAuthority(authorityUrl)
.WithCertificate(GetCertificate(authContext))
.Build();

ConfigureTokenCache(confidentialClientApp.AppTokenCache, authContext);
string graphBaseUrl = GraphSession.Instance.Environment?.GraphEndpoint ?? "https://graph.microsoft.com";
authProvider = new ClientCredentialProvider(confidentialClientApp, $"{graphBaseUrl}/.default");
break;
}
case AuthenticationType.UserProvidedAccessToken:
authProvider = new DelegateAuthenticationProvider(requestMessage =>
{
authProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
new NetworkCredential(string.Empty, GraphSession.Instance.UserProvidedToken).Password);
return Task.CompletedTask;
});
break;
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
new NetworkCredential(string.Empty, GraphSession.Instance.UserProvidedToken).Password);
return Task.CompletedTask;
});
break;
}
return authProvider;
}
Expand Down Expand Up @@ -255,4 +262,4 @@ private static X509Certificate2 GetCertificateByName(string certificateName)
return xCertificate;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,36 @@ public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsyn
}

[Fact]
public void ShouldUseDeviceCodeProviderWhenDelegatedContextIsProvided()
public void ShouldUseDeviceCodeWhenSpecifiedByUser()
{
// Arrange
AuthContext delegatedAuthContext = new AuthContext
{
AuthType = AuthenticationType.Delegated,
Scopes = new string[] { "User.Read" },
ContextScope = ContextScope.Process
Scopes = new[] { "User.Read" },
ContextScope = ContextScope.Process,
AuthProviderType = AuthProviderType.DeviceCodeProvider
};

// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(delegatedAuthContext);

// Assert
Assert.IsType<DeviceCodeProvider>(authProvider);

// reset static instance.
GraphSession.Reset();
}
[Fact]
public void ShouldUseDeviceCodeWhenFallback()
{
// Arrange
AuthContext delegatedAuthContext = new AuthContext
{
AuthType = AuthenticationType.Delegated,
Scopes = new[] { "User.Read" },
ContextScope = ContextScope.Process,
AuthProviderType = AuthProviderType.DeviceCodeProviderFallBack
};

// Act
Expand All @@ -67,6 +89,26 @@ public void ShouldUseDeviceCodeProviderWhenDelegatedContextIsProvided()
// reset static instance.
GraphSession.Reset();
}
[Fact]
public void ShouldUseInteractiveProviderWhenDelegated()
{
// Arrange
AuthContext delegatedAuthContext = new AuthContext
{
AuthType = AuthenticationType.Delegated,
Scopes = new[] { "User.Read" },
ContextScope = ContextScope.Process
};

// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(delegatedAuthContext);

// Assert
Assert.IsType<InteractiveAuthenticationProvider>(authProvider);

// reset static instance.
GraphSession.Reset();
}

#if NETCORE
[Fact]
Expand Down
Loading