diff --git a/.editorconfig b/.editorconfig index 8a5ba82f..94224d0e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,6 @@ [*] end_of_line = crlf +trim_trailing_whitespace = true [*.xml] indent_style = space @@ -28,4 +29,4 @@ dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_qualification_for_event = true:suggestion dotnet_style_qualification_for_field = true:suggestion dotnet_style_qualification_for_method = true:suggestion -dotnet_style_qualification_for_property = true:suggestion \ No newline at end of file +dotnet_style_qualification_for_property = true:suggestion diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d4d64c6..4949edfa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,12 +1,12 @@ - + true - true + true - + @@ -23,6 +23,7 @@ + @@ -31,4 +32,4 @@ - \ No newline at end of file + diff --git a/src/Common/Commands.Common.Test/TestProfile.cs b/src/Common/Commands.Common.Test/TestProfile.cs index db4855c5..7720a389 100644 --- a/src/Common/Commands.Common.Test/TestProfile.cs +++ b/src/Common/Commands.Common.Test/TestProfile.cs @@ -33,5 +33,7 @@ public class TestProfile : IPowerBIProfile public string Thumbprint { get; set; } public PowerBIProfileType LoginType { get; set; } = PowerBIProfileType.User; + + public string AccessToken { get; set; } } -} +} \ No newline at end of file diff --git a/src/Common/Commands.Common/AuthenticationFactorySelector.cs b/src/Common/Commands.Common/AuthenticationFactorySelector.cs index 59b18d07..5ed543d3 100644 --- a/src/Common/Commands.Common/AuthenticationFactorySelector.cs +++ b/src/Common/Commands.Common/AuthenticationFactorySelector.cs @@ -20,7 +20,7 @@ public class AuthenticationFactorySelector : IAuthenticationFactory private static IAuthenticationUserFactory UserAuthFactory; private static IAuthenticationServicePrincipalFactory ServicePrincipalAuthFactory; private static IAuthenticationBaseFactory BaseAuthFactory; - + private void InitializeUserAuthenticationFactory(IPowerBILogger logger, IPowerBISettings settings) { if (UserAuthFactory == null) @@ -76,6 +76,8 @@ public async Task Authenticate(IPowerBIProfile profile, IPowerBILo return await this.Authenticate(profile.UserName, profile.Password, profile.Environment, logger, settings); case PowerBIProfileType.Certificate: return await this.Authenticate(profile.UserName, profile.Thumbprint, profile.Environment, logger, settings); + case PowerBIProfileType.BringYourOwnToken: + return new PowerBIAccessToken { AccessToken = profile.AccessToken, }; default: throw new NotSupportedException(); } diff --git a/src/Common/Common.Abstractions/Interfaces/IPowerBIProfile.cs b/src/Common/Common.Abstractions/Interfaces/IPowerBIProfile.cs index 7dc8831a..4e7de861 100644 --- a/src/Common/Common.Abstractions/Interfaces/IPowerBIProfile.cs +++ b/src/Common/Common.Abstractions/Interfaces/IPowerBIProfile.cs @@ -41,5 +41,10 @@ public interface IPowerBIProfile /// Type of login used to create profile. /// PowerBIProfileType LoginType { get; } + + /// + /// Access token that was brought by user. + /// + string AccessToken { get; } } } \ No newline at end of file diff --git a/src/Common/Common.Abstractions/PowerBIProfile.cs b/src/Common/Common.Abstractions/PowerBIProfile.cs index 09fa6cda..1b46148e 100644 --- a/src/Common/Common.Abstractions/PowerBIProfile.cs +++ b/src/Common/Common.Abstractions/PowerBIProfile.cs @@ -22,9 +22,14 @@ public class PowerBIProfile : IPowerBIProfile public string Thumbprint { get; } + public string AccessToken { get; } + public PowerBIProfile(IPowerBIEnvironment environment, IAccessToken token) => (this.Environment, this.TenantId, this.UserName, this.LoginType) = (environment, token.TenantId, token.UserName, PowerBIProfileType.User); + public PowerBIProfile(IPowerBIEnvironment environment, string accessToken) => + (this.Environment, this.AccessToken, this.LoginType) = (environment, accessToken, PowerBIProfileType.BringYourOwnToken); + public PowerBIProfile(IPowerBIEnvironment environment, string userName, SecureString password, IAccessToken token, bool servicePrincipal = true) => (this.Environment, this.TenantId, this.UserName, this.Password, this.LoginType) = (environment, token.TenantId, userName, password, servicePrincipal ? PowerBIProfileType.ServicePrincipal : PowerBIProfileType.UserAndPassword); diff --git a/src/Common/Common.Abstractions/PowerBIProfileType.cs b/src/Common/Common.Abstractions/PowerBIProfileType.cs index 66881662..e5898492 100644 --- a/src/Common/Common.Abstractions/PowerBIProfileType.cs +++ b/src/Common/Common.Abstractions/PowerBIProfileType.cs @@ -13,6 +13,7 @@ public enum PowerBIProfileType User = 0, ServicePrincipal = 1, Certificate = 2, - UserAndPassword = 3 + UserAndPassword = 3, + BringYourOwnToken = 4, } } \ No newline at end of file diff --git a/src/Modules/Profile/Commands.Profile.Test/ConnectPowerBIServiceAccountTests.cs b/src/Modules/Profile/Commands.Profile.Test/ConnectPowerBIServiceAccountTests.cs index 51276b2f..22034c64 100644 --- a/src/Modules/Profile/Commands.Profile.Test/ConnectPowerBIServiceAccountTests.cs +++ b/src/Modules/Profile/Commands.Profile.Test/ConnectPowerBIServiceAccountTests.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Management.Automation; using System.Security; +using Microsoft.IdentityModel.Tokens; using Microsoft.PowerBI.Commands.Common.Test; using Microsoft.PowerBI.Common.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -144,13 +145,15 @@ public void ConnectPowerBIServiceWithDiscoveryUrl() public void ConnectPowerBIServiceAccountServiceWithTenantId_PrincipalParameterSet() { // Arrange + using var secureString = new SecureString(); + var initFactory = new TestPowerBICmdletNoClientInitFactory(false); var testTenantName = "test.microsoftonline.com"; var cmdlet = new ConnectPowerBIServiceAccount(initFactory) { Tenant = testTenantName, ServicePrincipal = true, - Credential = new PSCredential("appId", new SecureString()), + Credential = new PSCredential("appId", secureString), ParameterSet = "ServicePrincipal" }; @@ -181,5 +184,133 @@ public void ConnectPowerBIServiceAccountDiscoveryUrl_NullCustomEnvironment() //Assert Assert.Fail("Custom environment was not provided"); } + + [TestMethod] + public void ConnectPowerBIServiceAccount_Illegal_Token_Format_Throws() + { + // Arrange + var factory = new TestPowerBICmdletNoClientInitFactory(setProfile: false); + + var cmdlet = new ConnectPowerBIServiceAccount(factory) + { + Token = "thisjwtissowrong", + ParameterSet = ConnectPowerBIServiceAccount.BringYourOwnTokenParameterSet, + }; + + // Act & Assert + Assert.ThrowsException(cmdlet.InvokePowerBICmdlet); + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_Expired_Token_Throws() + { + // Arrange + var factory = new TestPowerBICmdletNoClientInitFactory(setProfile: false); + + var cmdlet = new ConnectPowerBIServiceAccount(factory) + { + // this is a dummy generated expired token + Token = "eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0X2NsYWltIjp0cnVlLC" + + "Jpc3MiOiJ1cm46ZXhhbXBsZTppc3N1ZXIiLCJhdWQiOiJ1cm46Z" + + "XhhbXBsZTphdWRpZW5jZSIsImV4cCI6MTc3MDcxNzUxNCwiaWF0I" + + "joxNzcwNzE3NDU0fQ.8aM6WbtJkp8mOpWKsmFngPFIMdzGIQje1ZhKpHH_UVE", + + ParameterSet = ConnectPowerBIServiceAccount.BringYourOwnTokenParameterSet, + }; + + // Act & Assert + Assert.ThrowsException(cmdlet.InvokePowerBICmdlet); + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_Valid_Token_Success() + { + // Arrange + // dummy generated token that will expire in 30 years (from 10022026) + var dummyToken = "eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0X2NsYWltIjp0cnVlLCJpc3MiOiJ1cm46ZXhhbXBsZT" + + "ppc3N1ZXIiLCJhdWQiOiJ1cm46ZXhhbXBsZTphdWRpZW5jZSIsImV4cCI6MjcxN" + + "zQ0Njc4NSwiaWF0IjoxNzcwNzE4Nzg1fQ.nOXgwieIpeFB9Svxxt6Z4_RkWVWSiVJcxbBzlPTaQJQ"; + + var factory = new TestPowerBICmdletNoClientInitFactory(setProfile: false); + + var cmdlet = new ConnectPowerBIServiceAccount(factory) + { + Token = dummyToken, + Environment = PowerBIEnvironmentType.Daily, + ParameterSet = ConnectPowerBIServiceAccount.BringYourOwnTokenParameterSet, + }; + + // Act + cmdlet.InvokePowerBICmdlet(); + + // Assert + var profile = factory.GetProfileFromStorage(); + Assert.IsNotNull(profile); + Assert.AreEqual(dummyToken, profile.AccessToken); + Assert.AreEqual(PowerBIEnvironmentType.Daily, profile.Environment.Name); + Assert.AreEqual(PowerBIProfileType.BringYourOwnToken, profile.LoginType); + factory.AssertExpectedUnitTestResults([profile]); + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_BringYourOwnTokenParameterSet_Token_with_CertificateThumbPrint_Throws() + { + using (var ps = System.Management.Automation.PowerShell.Create()) + { + // Arrange + ps.AddCommand(ProfileTestUtilities.ConnectPowerBIServiceAccountCmdletInfo); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.Token), "dummytoken"); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.CertificateThumbprint), "dummycreds"); + + // Act & Assert + Assert.ThrowsException(ps.Invoke); + } + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_BringYourOwnTokenParameterSet_Token_with_ApplicationId_Throws() + { + using (var ps = System.Management.Automation.PowerShell.Create()) + { + // Arrange + ps.AddCommand(ProfileTestUtilities.ConnectPowerBIServiceAccountCmdletInfo); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.Token), "dummytoken"); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.ApplicationId), "applicationId"); + + // Act & Assert + Assert.ThrowsException(ps.Invoke); + } + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_BringYourOwnTokenParameterSet_Token_with_Credential_Throws() + { + using (var secureString = new SecureString()) + using (var ps = System.Management.Automation.PowerShell.Create()) + { + // Arrange + ps.AddCommand(ProfileTestUtilities.ConnectPowerBIServiceAccountCmdletInfo); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.Token), "dummytoken"); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.Credential), new PSCredential("password", secureString)); + + // Act & Assert + Assert.ThrowsException(ps.Invoke); + } + } + + [TestMethod] + public void ConnectPowerBIServiceAccount_BringYourOwnTokenParameterSet_Token_with_ServicePrincipal_Throws() + { + using (var ps = System.Management.Automation.PowerShell.Create()) + { + // Arrange + ps.AddCommand(ProfileTestUtilities.ConnectPowerBIServiceAccountCmdletInfo); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.Token), "dummytoken"); + ps.AddParameter(nameof(ConnectPowerBIServiceAccount.ServicePrincipal), new SwitchParameter()); + + // Act & Assert + Assert.ThrowsException(ps.Invoke); + } + } } } \ No newline at end of file diff --git a/src/Modules/Profile/Commands.Profile/Commands.Profile.csproj b/src/Modules/Profile/Commands.Profile/Commands.Profile.csproj index e628d9a3..6bc60d9c 100644 --- a/src/Modules/Profile/Commands.Profile/Commands.Profile.csproj +++ b/src/Modules/Profile/Commands.Profile/Commands.Profile.csproj @@ -5,7 +5,7 @@ Microsoft.PowerBI.Commands.Profile Microsoft.PowerBI.Commands.Profile - + true @@ -13,13 +13,13 @@ Microsoft Power BI PowerShell - Profile credential management cmdlets for Microsoft Power BI PowerBI;Profile;Authentication;Environment - + - + PreserveNewest @@ -37,6 +37,7 @@ All + diff --git a/src/Modules/Profile/Commands.Profile/ConnectPowerBIServiceAccount.cs b/src/Modules/Profile/Commands.Profile/ConnectPowerBIServiceAccount.cs index d33b6904..a2376d54 100644 --- a/src/Modules/Profile/Commands.Profile/ConnectPowerBIServiceAccount.cs +++ b/src/Modules/Profile/Commands.Profile/ConnectPowerBIServiceAccount.cs @@ -10,6 +10,8 @@ using System.Net.Http; using System.Runtime.Serialization.Json; using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using Microsoft.PowerBI.Commands.Common; using Microsoft.PowerBI.Common.Abstractions; using Microsoft.PowerBI.Common.Abstractions.Interfaces; @@ -31,6 +33,7 @@ public class ConnectPowerBIServiceAccount : PowerBICmdlet public const string ServicePrincipalParameterSet = "ServicePrincipal"; public const string ServicePrincipalCertificateParameterSet = "ServicePrincipalCertificate"; public const string UserAndCredentialPasswordParameterSet = "UserAndCredential"; + public const string BringYourOwnTokenParameterSet = "BringYourOwnToken"; #endregion #region Parameters @@ -55,14 +58,18 @@ public class ConnectPowerBIServiceAccount : PowerBICmdlet public SwitchParameter ServicePrincipal { get; set; } [Alias("TenantId")] + [Parameter(ParameterSetName = UserParameterSet, Mandatory = false)] [Parameter(ParameterSetName = ServicePrincipalParameterSet, Mandatory = false)] - [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false)] + [Parameter(ParameterSetName = BringYourOwnTokenParameterSet, Mandatory = false)] [Parameter(ParameterSetName = UserAndCredentialPasswordParameterSet, Mandatory = false)] - [Parameter(ParameterSetName = UserParameterSet, Mandatory = false)] + [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false)] public string Tenant { get; set; } [Parameter(Mandatory = false)] public string DiscoveryUrl { get; set; } + + [Parameter(ParameterSetName = BringYourOwnTokenParameterSet, Mandatory = true)] + public string Token { get; set; } #endregion /// @@ -128,9 +135,9 @@ public override void ExecuteCmdlet() environment = settings.Environments[this.Environment]; } - if(!string.IsNullOrEmpty(this.Tenant)) + if (!string.IsNullOrEmpty(this.Tenant)) { - var tempEnvironment = (PowerBIEnvironment) environment; + var tempEnvironment = (PowerBIEnvironment)environment; tempEnvironment.AzureADAuthority = tempEnvironment.AzureADAuthority.ToLowerInvariant().Replace("/common", $"/{this.Tenant}"); this.Logger.WriteVerbose($"Updated Azure AD authority with -Tenant specified, new value: {tempEnvironment.AzureADAuthority}"); environment = tempEnvironment; @@ -168,6 +175,10 @@ public override void ExecuteCmdlet() token = this.Authenticator.Authenticate(this.Credential.UserName, this.Credential.Password, environment, this.Logger, this.Settings).Result; profile = new PowerBIProfile(environment, this.Credential.UserName, this.Credential.Password, token); break; + case BringYourOwnTokenParameterSet: + ValidateJwtToken(this.Token); + profile = new PowerBIProfile(environment, this.Token); + break; default: throw new NotImplementedException($"Parameter set {this.ParameterSet} was not implemented"); } @@ -176,6 +187,35 @@ public override void ExecuteCmdlet() this.Logger.WriteObject(profile); } + private void ValidateJwtToken(string token) + { + var handler = new JsonWebTokenHandler(); + + var validationParams = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + SignatureValidator = (rawToken, parameters) => new JsonWebToken(rawToken), + }; + + var result = handler.ValidateTokenAsync(token, validationParams) + .GetAwaiter() + .GetResult(); + + if (!result.IsValid) + { + if (result.Exception != null) + { + throw result.Exception; + } + else + { + throw new SecurityTokenValidationException("Token validation failed"); + } + } + } + private async Task GetServiceConfig(string discoveryUrl) { using (var client = new HttpClient()) @@ -188,7 +228,7 @@ private async Task GetServiceConfig(string discoveryUrl) } return this.CustomServiceEnvironments; - } + } protected override bool CmdletManagesProfile { get => true; set => base.CmdletManagesProfile = value; } } diff --git a/src/Modules/Profile/Commands.Profile/GetPowerBIAccessToken.cs b/src/Modules/Profile/Commands.Profile/GetPowerBIAccessToken.cs index 59585aad..fcb47774 100644 --- a/src/Modules/Profile/Commands.Profile/GetPowerBIAccessToken.cs +++ b/src/Modules/Profile/Commands.Profile/GetPowerBIAccessToken.cs @@ -41,7 +41,6 @@ public override void ExecuteCmdlet() { "Authorization", token.AuthorizationHeader } }); } - } } -} +} \ No newline at end of file diff --git a/src/Modules/Profile/Commands.Profile/help/Connect-PowerBIServiceAccount.md b/src/Modules/Profile/Commands.Profile/help/Connect-PowerBIServiceAccount.md index 5225e805..1be4c547 100644 --- a/src/Modules/Profile/Commands.Profile/help/Connect-PowerBIServiceAccount.md +++ b/src/Modules/Profile/Commands.Profile/help/Connect-PowerBIServiceAccount.md @@ -38,6 +38,12 @@ Connect-PowerBIServiceAccount [-Environment ] [-CustomEn [-DiscoveryUrl ] [] ``` +### BringYourOwnToken +``` +Connect-PowerBIServiceAccount [-Token ] [-Environment ] [-CustomEnvironment ] +[-Tenant ] [-DiscoveryUrl ] [] +``` + ## DESCRIPTION Log in to Power BI service with either a user or service principal account (application key or certificate). For user accounts, an Azure Active Directory (AAD) First-Party application is leveraged for authentication. @@ -71,9 +77,17 @@ Logs in using a service principal against the Public cloud, a prompt will displa PS C:\> Connect-PowerBIServiceAccount -ServicePrincipal -CertificateThumbprint 38DA4BED389A014E69A6E6D8AE56761E85F0DFA4 -ApplicationId b5fde143-722c-4e8d-8113-5b33a9291468 ``` -Logs in using a service principal with an installed certificate to the Public cloud. +Logs in using a service principal with an installed certificate to the Public cloud. The certificate must be installed in either CurrentUser or LocalMachine certificate store (LocalMachine requires administrator access) with a private key installed. + +Use the provided _Token_ (a raw OAuth 2.0 access token/JWT value, without the `Bearer ` prefix) for authentication when calling the Power BI REST API. Treat this token as a secret and do not paste it into logs, scripts, or command-line history. + +### Example 5 +```powershell +PS C:\> Connect-PowerBIServiceAccount -Token eyJhbGciOiJIUzI1NiJ9.fake_payload_and_signature_redacted_for_example +``` + ## PARAMETERS ### -ApplicationId @@ -138,7 +152,7 @@ Accept wildcard characters: False ``` ### -DiscoveryUrl -The discovery url to get the backend services info from. Custom environment must also be supplied. +The discovery url to get the backend services info from. Custom environment must also be supplied. ```yaml Type: String @@ -198,6 +212,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -Token +JWT access token to use for authentication; pass only the token value, which will be sent as the `Authorization: Bearer ` header for API calls. + +```yaml +Type: String +Parameter Sets: BringYourOwnToken +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216).