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