Power BI API with SPN Application rights - azure-active-directory

I have SPN that has Power BI - Tenant.ReadWrite.All Application permissions granted in AAD, but when making a call to list all groups from tenant, I get 401 Unauthorized.
string clientId = "{clientId}";
string clientSecret = "{clientSecred}";
string authority = "https://login.microsoftonline.com/{tenantId}";
string resource = "https://analysis.windows.net/powerbi/api";
string ApiUrl = "https://api.powerbi.com";
string[] scopes = new string[] { $"{resource}/.default" };
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri(authority))
.Build();
AuthenticationResult result = null;
result = app.AcquireTokenForClient(scopes).ExecuteAsync().Result;
var tokenCredentials = new TokenCredentials(result.AccessToken, "Bearer");
using (PowerBIClient _powerBIClient = new PowerBIClient(new Uri(ApiUrl), tokenCredentials))
{
var workspaceNames = _powerBIClient.Groups.GetGroupsAsAdmin(100, filter: "type eq 'Workspace'").Value.Select(x => x.Name);
};
Any clue what can be wrong? Bearer token returned seems to be all good.

Related

Validating an Azure AD generated JWT signature and algorithm in .NET Core 3.1

I am new to Azure AD. We are using v1.0 token. I have an Azure JWT token validation routine mostly based on ValidateSignature and AzureTokenValidation
The below is my ClaimsTransformer:
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
<do something>
var token = httpContextAccessor.HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
if (Validate(token))
{
<proceed to add claims>
}
and my validation routine:
public bool Validate(string token)
{
string stsEndpoint = "https://login.microsoftonline.com/common/.well-known/openid-configuration";
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;
TokenValidationParameters parameters = new TokenValidationParameters
{
ValidIssuer = "https://sts.windows.net/<mytenant-id>",
ValidAudience = <my client-id>,
IssuerSigningKeys = config.SigningKeys,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
};
JwtSecurityTokenHandler tokendHandler = new JwtSecurityTokenHandler();
SecurityToken jwt;
bool verified;
try
{
var handler = tokendHandler.ValidateToken(token, validationParameters, out jwt);
var signature = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)jwt).RawSignature;
string algorithm = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)jwt).Header["alg"].ToString();
if (signature.Length == 0 || algorithm.ToLower().Equals("none"))
{
tokenVerified = false;
}
tokenVerified = true;
}
catch
{
tokenVerified = false;
}
return tokenVerified;
}
Please tell me if I am doing the right thing or can I just use (in my Validate(string token)
try
{
var result = tokendHandler.ValidateToken(token, validationParameters, out _);
verified = true;
}
catch {
verified = false;
}
and is, the checking for algorithm (to not accept "none" in alg) and signature presence is required or this way to check is right? There is no "secret keys"
JwtBearer middleware, like the OpenID Connect middleware in web apps, validates the token based on the value of TokenValidationParameters. The token is decrypted as needed, the claims are extracted, and the signature is verified. The middleware then validates the token by checking for this data:
Audience: The token is targeted for the web API.
Sub: It was issued for an app that's allowed to call the web API.
Issuer: It was issued by a trusted security token service (STS).
Expiry: Its lifetime is in range.
Signature: It wasn't tampered with.
For more details refer this document
Example:
String token = "Token";
string myTenant = "Tenant Name";
var myAudience = "Audience";
var myIssuer = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/v2.0", myTenant);
var mySecret = "Secrete";
var mySecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(mySecret));
var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", myTenant);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = myAudience,
ValidIssuer = myIssuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = false,
IssuerSigningKey = mySecurityKey
};
var validatedToken = (SecurityToken)new JwtSecurityToken();
// Throws an Exception as the token is invalid (expired, invalid-formatted, etc.)
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
Console.WriteLine(validatedToken);
Console.ReadLine();
OUTPUT

call graph as part of authentication to add claims .net 4.5

i think the correct place is in SecurityTokenValidated but account is always null. i dont know how to set up the graphclient here?
SecurityTokenValidated = async (x) =>
{
IConfidentialClientApplication clientApp2 = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result2 = null;
var account = await clientApp2.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId());
string[] scopes = { "User.Read" };
// try to get an already cached token
result2 = await clientApp2.AcquireTokenSilent(scopes, account).ExecuteAsync().ConfigureAwait(false);
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
//var token = await tokenAcquisition
// .GetAccessTokenForUserAsync(GraphConstants.Scopes, user: context.Principal);
var token = result2.AccessToken;
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
})
);
var user = await graphClient.Me.Request()
.Select(u => new
{
u.DisplayName,
u.Mail,
u.UserPrincipalName
})
.GetAsync();
var identity = x.AuthenticationTicket.Identity;
identity.AddClaim(new Claim(ClaimTypes.Role, "test"));
}
Please refer to this sample: https://learn.microsoft.com/en-us/samples/azure-samples/active-directory-dotnet-admin-restricted-scopes-v2/active-directory-dotnet-admin-restricted-scopes-v2/
You could follow this sample to get access token with GetGraphAccessToken() and make sure the signed-in user is a user account in your Azure AD tenant. Last thing is using Chrome in incognito mode this helps ensure that the session cookie does not get in the way by automatically logging you in and bypassing authentication.
This sample will not work with a Microsoft account (formerly Windows
Live account). Therefore, if you signed in to the Azure portal with a
Microsoft account and have never created a user account in your
directory before, you need to do that now. You need to have at least
one account which is a directory administrator to test the features
which require an administrator to consent.
var graphserviceClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
(requestMessage) =>
{
// Get a token for the Microsoft Graph
var access_token = await GetGraphAccessToken();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token);
return Task.FromResult(0);
}));
}
private async Task<string> GetGraphAccessToken()
{
IConfidentialClientApplication cc = MsalAppBuilder.BuildConfidentialClientApplication();
var userAccount = await cc.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId());
AuthenticationResult result = await cc.AcquireTokenSilent(new string[] { "user.read" }, userAccount).ExecuteAsync();
return result.AccessToken;
}

Unable to send email via Microsoft Graph API with Delegated Permission

I created a C# console application to send email using Microsoft Graph API. On adding Mail.Send Application Permission, it works fine. But, because of company requirements, I was asked to use Mail.Send Delegated Permission instead and with that permission I don't see it working and I see this error:
Are there any steps I should consider doing after adding Mail.Send Delegated Permission in order to get this working?
Here is my code:
static void Main(string[] args)
{
// Azure AD APP
string clientId = "<client Key Here>";
string tenantID = "<tenant key here>";
string clientSecret = "<client secret here>";
Task<GraphServiceClient> callTask = Task.Run(() => SendEmail(clientId, tenantID, clientSecret));
// Wait for it to finish
callTask.Wait();
// Get the result
var astr = callTask;
}
public static async Task<GraphServiceClient> SendEmail(string clientId, string tenantID, string clientSecret)
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var message = new Message
{
Subject = subject,
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = content
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress { Address = recipientAddress }
}
}
};
var saveToSentItems = true;
await _graphClient.Users[<userprincipalname>]
.SendMail(message, saveToSentItems)
.Request()
.PostAsync();
return graphClient;
}
UPDATE:
Based on below answer, I updated code as follows:
var publicClientApplication = PublicClientApplicationBuilder
.Create("<client-id>")
.WithTenantId("<tenant-id>")
.Build();
var authProvider = new UsernamePasswordProvider(publicClientApplication);
var secureString = new NetworkCredential("", "<password>").SecurePassword;
User me = await graphClient.Me.Request()
.WithUsernamePassword("<username>", secureString)
.GetAsync();
I enabled "Allow public client flows" to fix an exception.
And now I see another exception: Insufficient privileges to complete the operation.
What am I missing?
UPDATE: Currently I see this exception with no changes in the code:
The code you provided shows you use client credential flow to do the authentication. When you use Mail.Send Application permission, use client credential flow is ok. But if you use Mail.Send Delegated permission, we can not use client credential. You should use username/password flow to do authentication.
=================================Update===================================
Below is my code:
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Security;
namespace ConsoleApp34
{
class Program
{
static async System.Threading.Tasks.Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var publicClientApplication = PublicClientApplicationBuilder
.Create("client id")
.WithTenantId("tenant id")
.Build();
string[] scopes = new string[] { "mail.send" };
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(publicClientApplication, scopes);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "to email address"
}
}
}
};
var securePassword = new SecureString();
foreach (char c in "your password")
securePassword.AppendChar(c);
var saveToSentItems = true;
await graphClient.Me
.SendMail(message, saveToSentItems)
.Request().WithUsernamePassword("your email", securePassword)
.PostAsync();
}
}
}
The reason for your error message Insufficient privileges to complete the operation is you use the code:
User me = await graphClient.Me.Request()
.WithUsernamePassword("<username>", secureString)
.GetAsync();
This code is used to get the user(me)'s information but not send email, you haven't added the permission to the app. So it will show Insufficient privileges to complete the operation. Please remove this code and use the code block in my code instead:
await graphClient.Me.SendMail(message, saveToSentItems)
.Request().WithUsernamePassword("your email", securePassword)
.PostAsync();
==============================Update2====================================

Possible to exchange expired Microsoft Graph API access token for a new one?

I am authenticating to the Graph API in my Startup.cs:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false // Setting this to true prevents logging in, and is only necessary on a multi-tenant app.
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = async (context) =>
{
// This block executes once an auth code has been sent and received.
Evar idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var signedInUser = new ClaimsPrincipal(context.AuthenticationTicket.Identity);
var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, signedInUser);
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(scopes, context.Code).ExecuteAsync();
var userDetails = await GraphUtility.GetUserDetailAsync(result.AccessToken);
After retrieving this access token, I store it into a class variable. The reason why I do this is so that I can retrieve it for use in one of my services (called by an API controller) that interfaces with the Graph API.
public GraphAPIServices(IDbContextFactory dbContextFactory) : base(dbContextFactory)
{
_accessToken = GraphUtility.GetGraphAPIAccessToken();
_graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
}));
}
The problem that I am running into is that after some time, this access token eventually expires. I obviously can't run Startup.cs again so there is no opportunity to retrieve a new access token.
What I would like to know is if it's possible to exchange this expired access token for a new one without the need to request that the user logs in again with their credentials?

Change user password for using Azure Graph API

I am not able to change the password of the logged in Azure AD B2C user.
I have Azure B2C tenant which is used for dev and QA.Also, i have two applications something-Local and something-QA used for DEV and QA respectively in Azure B2C as shown below and I have verified the settings of both the apps they are same
Below are the configurations of the applications
Here is my code which is used for B2C connection
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
// meta data
MetadataAddress = "https://login.microsoftonline.com/" + "mytenant" + "/v2.0/.well-known/openid-configuration?p=" + policy,
AuthenticationType = policy,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = AzureAdConfig.ClientId,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = OnSecurityTokenValidated,
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
}
};
}
in the above code the ClientID used for QA and Dev are different.
Below is the code used to change the user password using graph API.
public async Task<HttpResponseMessage> ChangePassword(string currentPassword, string newPassword)
{
string userId = ClaimValues.ObjectIdentifier();
var adUser = _activeDirectoryClient.Users
.Where(u => u.ObjectId.Equals(userId))
.ExecuteAsync().Result.CurrentPage.FirstOrDefault();
string upn = adUser.UserPrincipalName;
var client = new HttpClient();
string uriString = "https://login.microsoftonline.com/"+ AzureAdConfig.Tenant + "/oauth2/token";
Uri requestUri = new Uri(uriString);
string requestString = "resource=https%3a%2f%2fgraph.windows.net&client_id=" + AzureAdConfig.AppId + "&grant_type=password&username=" + upn + "&password=" + currentPassword + "&client_secret=" + AzureAdConfig.AppKey;
var tokenResult = await client.PostAsync(requestUri, new StringContent(requestString, Encoding.UTF8, "application/x-www-form-urlencoded"));
if (tokenResult.IsSuccessStatusCode)
{
var stringResult = await tokenResult.Content.ReadAsStringAsync();
GraphApiTokenResult objectResult = JsonConvert.DeserializeObject<GraphApiTokenResult>(stringResult);
client = new HttpClient();
string requestUrl = AzureAdConfig.GraphResourceId + AzureAdConfig.Tenant + "/me/changePassword?" + AzureAdConfig.GraphVersion;
Uri graphUri = new Uri(requestUrl);
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", objectResult.access_token);
requestString = JsonConvert.SerializeObject(new
{
currentPassword = currentPassword,
newPassword = newPassword
});
var response = await client.PostAsync(graphUri, new StringContent(requestString, Encoding.UTF8, "application/json"));
return response;
}
else
{
return tokenResult;
}
}
Also, i wanted to understand what is the difference between Application Registrations in Azure Active directory service of azure and the Application in Azure AD B2C of azure?
Thanks in advance
To change user password by using Azure AD Graph API, first you should be a global administrator in your tenant, and then you could use PATCH https://graph.windows.net/myorganization/users/{user_id}?api-version and then update.
{
"passwordProfile": {
"password": "value",
"forceChangePasswordNextLogin": false
}
}
Also, i wanted to understand what is the difference between
Application Registrations in Azure Active directory service of azure
and the Application in Azure AD B2C of azure?
You can know about this from the difference between Azure AD tenant and Azure AD B2C tenant from here.
Hope it can help you.

Resources