I'm trying to upgrade one of the projects I'm working on to use the Microsoft.Identity.Web nuget package. So far really working well but I'm having trouble figuring out how to add additional claims which I was previously doing by the following:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Events.OnAuthorizationCodeReceived = async ctx =>
{
var serviceProvider = services.BuildServiceProvider();
var distributedCache = serviceProvider.GetRequiredService<IDistributedCache>();
var identifier = ctx.Principal.FindFirst(ObjectIdentifierType)?.Value;
var cca = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(new ConfidentialClientApplicationOptions()
{
ClientId = "ClientId",
RedirectUri = "RedirectUri",
ClientSecret = "ClientSecret"
})
.WithAuthority(ctx.Options.Authority)
.Build();
var tokenCache = new SessionTokenCache(identifier, distributedCache);
tokenCache.Initialize(cca.UserTokenCache);
var token = await cca.AcquireTokenByAuthorizationCode(scopes, ctx.TokenEndpointRequest.Code).ExecuteAsync();
ctx.HandleCodeRedemption(token.AccessToken, token.IdToken);
// get the claims
var claimService = serviceProvider.GetRequiredService<ClaimService>();
var response = await apiClient.GetUserAdditionalClaimsAsync(token.AccessToken);
// add the claims
};
Now when I try to use the ITokenAcquisition.GetAccessTokenForUserAsync() method instead of using the ConfidentialClientApplicationBuilder
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftWebApp(options =>
{
Configuration.Bind("AzureAD", options);
options.Events.OnAuthorizationCodeReceived = async ctx =>
{
var serviceProvider = services.BuildServiceProvider();
var tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>();
var token = await tokenAcquisition.GetAccessTokenForUserAsync("scopes");
// get the claims
var claimService = serviceProvider.GetRequiredService<ClaimService>();
var response = await apiClient.GetUserAdditionalClaimsAsync(token);
// add the claims
};
})
.AddMicrosoftWebAppCallsWebApi(Configuration, new[] { "scopes" })
.AddDistributedTokenCaches();
Any help would be so much appreciated on how best to handle this.
Thanks!
I get the following error:
You use the client credentials flow when using ConfidentialClientApplicationBuilder. I don't know why you use the ITokenAcquisition.GetAccessTokenForUserAsync() to instead.
You can use the below code sample for client credential flow :
// Even if this is a console application here, a daemon application is a confidential client application
IConfidentialClientApplication app;
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithTenantId("{tenantID}")
.WithClientSecret(config.ClientSecret)
.Build();
// With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the
// application permissions need to be set statically (in the portal or by PowerShell), and then granted by
// a tenant administrator
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
}
catch(MsalServiceException ex)
{
// Case when ex.Message contains:
// AADSTS70011 Invalid scope. The scope has to be of the form "https://resourceUrl/.default"
// Mitigation: change the scope to be as expected
}
Related
I'm trying to call an Azure Machine Learning Pipeline Endpoint I've set up using C# & the Machine Learning REST api.
I am certain that I have the Service Principal configured correctly, as I can successfully authenticate & hit the endpoint using the azureml-core python sdk:
sp = ServicePrincipalAuthentication(
tenant_id=tenant_id,
service_principal_id=service_principal_id,
service_principal_password=service_principal_password)
ws =Workspace.get(
name=workspace_name,
resource_group=resource_group,
subscription_id=subscription_id,
auth=sp)
endpoint = PipelineEndpoint.get(ws, name='MyEndpoint')
endpoint.submit('Test_Experiment')
I'm using the following example in C# to attempt to run my endpoint: https://learn.microsoft.com/en-us/azure/machine-learning/how-to-deploy-pipelines#run-a-published-pipeline-using-c
I'm attempting to fill auth_key with the following code:
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var cred = new ClientSecretCredential(tenantId, clientId, clientSecret);
var auth_key = cred.GetToken(new Azure.Core.TokenRequestContext(new string[] {".default" }));
I receive a 401 (unauthorized).
What am I am doing wrong?
UPDATE *
I changed the 'scopes' param in the TokenRequestContext to look like:
var auth_key = cred.GetToken(new Azure.Core.TokenRequestContext(new string[] { "http://DataTriggerApp/.default" }));
http://DataTriggerApp is one of the servicePrincipalNames that shows up when i query my Service Principal from the azure CLI.
Now, when I attempt to use the returned token to call the Machine Learning Pipeline Endpoint, I receive a 403 instead of a 401. Maybe some progress?
Ok, through a lot of trial-and-error I was able to come up with two ways of acquiring a token that allows me to hit my Azure Machine Learning Pipeline Endpoint through the REST api. One uses Microsoft.Identity.Client & one uses Azure.Identity.
using Microsoft.Identity.Client;
...
public static async Task<string> GetAccessToken()
{
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.Build();
var result = await app.AcquireTokenForClient(new string[] { "https://ml.azure.com/.default" }).ExecuteAsync();
return result.AccessToken;
}
Or:
using Azure.Identity;
...
public static async Task<string> GetAccessToken()
{
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var cred = new ClientSecretCredential(tenantId, clientId, clientSecret);
var token = await cred.GetTokenAsync(new Azure.Core.TokenRequestContext(new string[] { "https://ml.azure.com/.default" }));
return token.Token;
}
I am trying to create SAS policy for azure service bus namespace using shared access policy connectionstring in .NET Core 2.1.
I can create it using Microsoft.Azure.Management.ServiceBus NuGet package as follows
private static async Task<string> GetToken()
{
try
{
// Check to see if the token has expired before requesting one.
// We will go ahead and request a new one if we are within 2 minutes of the token expiring.
if (tokenExpiresAtUtc < DateTime.UtcNow.AddMinutes(-2))
{
Console.WriteLine("Renewing token...");
var tenantId = appOptions.TenantId;
var clientId = appOptions.ClientId;
var clientSecret = appOptions.ClientSecret;
var context = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
var result = await context.AcquireTokenAsync(
"https://management.core.windows.net/",
new ClientCredential(clientId, clientSecret)
);
// If the token isn't a valid string, throw an error.
if (string.IsNullOrEmpty(result.AccessToken))
{
throw new Exception("Token result is empty!");
}
tokenExpiresAtUtc = result.ExpiresOn.UtcDateTime;
tokenValue = result.AccessToken;
Console.WriteLine("Token renewed successfully.");
}
return tokenValue;
}
catch (Exception e)
{
Console.WriteLine("Could not get a new token...");
Console.WriteLine(e.Message);
throw e;
}
}
private static async Task CreateSASPolicy()
{
try
{
if (string.IsNullOrEmpty(namespaceName))
{
throw new Exception("Namespace name is empty!");
}
var token = await GetToken();
var creds = new TokenCredentials(token);
var sbClient = new ServiceBusManagementClient(creds)
{
SubscriptionId = appOptions.SubscriptionId,
};
List<AccessRights?> list = new List<AccessRights?> { AccessRights.Send };
var AuthRule = new SBAuthorizationRule { Rights = list };
var authorizationRuleName = "SendRule"; //policy name
Console.WriteLine("Creating SAS policy...");
var result = sbClient.Namespaces.CreateOrUpdateAuthorizationRuleAsync(resourceGroupName, namespaceName, authorizationRuleName, AuthRule).Result;
Console.WriteLine("Created SAS policy successfully.");
}
catch (Exception e)
{
Console.WriteLine("Could not create a SAS policy...");
Console.WriteLine(e.Message);
throw e;
}
}
But for above code I need to give at least "Azure Service Bus Data Owner" to the app which we are using to create token.
I have also tried using http client as follows
using (HttpClient httpclient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }))
{
httpclient.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(
System.Text.ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}", "", accessToken))));
string baseAddress = #"https://management.core.windows.net/<subscriptionId>/services/ServiceBus/namespaces/<namespace>/AuthorizationRules/";
var sendRule = new SharedAccessAuthorizationRule("contosoSendAll",
new[] { AccessRights.Send });
var result = await httpclient.GetAsync(baseAddress).ConfigureAwait(false);
if (result.IsSuccessStatusCode)
{
var response = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
It returns 403(Forbidden).
Question :-
Is there any way to create SAS policy without giving "Azure Service Bus Data Owner" permission to app? How?
Can create SAS policy using shared access policy connectionstring? How?
Can create SAS policy by using current login user credentials? How?
Can create SAS policy using shared access policy connectionstring? How?
If you use shared access policy connectionstring to create SAS policy, we just can create policy for topic or subscription. For the namespace, we need to implement it with Azure Resource provider API. It the way you are currently using. For more details, please refer to here.
Regarding how to create policy fro the topic or queue, we can use the package Azure.Messaging.ServiceBus.Administration.
For example
ServiceBusAdministrationClient client = new ServiceBusAdministrationClient(connectionString);
QueueProperties queue =await client.GetQueueAsync("myqueue");
queue.AuthorizationRules.Add( new SharedAccessAuthorizationRule(
"manage",
new[] { AccessRights.Manage, AccessRights.Send, AccessRights.Listen })
);
queue= await client.UpdateQueueAsync(queue);
foreach (var rule in queue.AuthorizationRules) {
Console.WriteLine(rule.KeyName);
}
Is there any way to create SAS policy without giving "Azure Service Bus Data Owner" permission to app? How?
When we use Azure Resource Provider API to create the resource, we should have the right Azure RABC permissions. About creating policy, we need to have permissions Microsoft.ServiceBus/namespaces/authorizationRules/action and Microsoft.ServiceBus/namespaces/authorizationRules/write. If you do not want to use Azure Service Bus Data Owner, you can create a custom role with these permissions. For more details, please refer to here and here.
I am trying to create Online Meeting using microsoft graph api without login into AzureActiveDirectory with asp.net web application.For this my app has below permissions which are required as per documentation https://learn.microsoft.com/en-us/graph/api/application-post-onlinemeetings?view=graph-rest-1.0&tabs=csharp with client credential auth flow https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow without immediate interaction with a user.I am able to retrive access token successfully as per client-creds-grant-flow.
I tried Micosoft.Graph and Micosoft.Graph.Beta still getting 404 error.
Create online meeting code
var graphClient = GetAuthenticatedClientCredential();
var onlineMeeting = new OnlineMeeting
{
StartDateTime = DateTimeOffset.Parse("2020-10-01T10:30:34.2444915+00:00"),
EndDateTime = DateTimeOffset.Parse("2020-10-01T11:00:34.2464912+00:00"),
Subject = "Create Online Meeting-Without user login to Office 365"
};
return await graphClient.Me.OnlineMeetings
.Request()
.AddAsync(onlineMeeting);
Access Token code
public static async Task<string> GetUserAccessTokenAsyncByCc()
{
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(appId)
.WithTenantId(appTenantId)
.WithClientSecret(appSecret)
.Build();
string[] scopes1 = new string[] { "https://graph.microsoft.com/.default" };
//string[] scopes1 = new string[] { "https://graph.microsoft.com/OnlineMeetings.ReadWrite.All" };
// string[] scopes1 = new string[] { "https://graph.microsoft.com/beta/OnlineMeetings.Read.All" };
//string[] scopes1 = new string[] { "https://graph.microsoft.com/beta/.default" };
var result = await cca.AcquireTokenForClient(scopes1).ExecuteAsync();
return result.AccessToken;
}
and Auth Provider code
public static GraphServiceClient GetAuthenticatedClientCredential()
{
DelegateAuthenticationProvider provider = new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await GetUserAccessTokenAsyncByCc();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
});
GraphServiceClient graphClient = new GraphServiceClient(provider);
return graphClient;
}
app permission image
below are the necessary app permission
You can only use delegated permissions to create an onlineMeeting, so you must log in as a user, and you cannot use the client credential flow. You need to use the auth code flow to obtain the token.
When using the ASP.Net core 3.0 angular SPA individual account template, and the AspNet.Security.OAuth.Spotify nuget package. When A user logs in, I want to be able to get their spotify access token so I can preform actions on the user's behalf. However, when I call await HttpContext.GetTokenAsync("spotify", "access_token"); the results returns null.
I've debugged a bit and saw the spotify tokens on second+ login in the options.Events.OnCreatingTicket event, but I guess the token is just not passed around past that? I'm not really sure anymore.
Startup.cs
ConfigureServices
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify("spotify", options =>
{
options.ClientId = Configuration["SpotifySettings:ClientId"];
options.ClientSecret = Configuration["SpotifySettings:ClientSecret"];
options.CallbackPath = "/callback";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SaveTokens = true;
String[] items = {
"playlist-read-private", "playlist-modify-public", "playlist-modify-private", "playlist-read-collaborative", "user-library-modify", "user-library-read", "user-read-email"
};
foreach (var item in items)
{
options.Scope.Add(item);
}
options.Events.OnRemoteFailure = (context) =>
{
// Handle failed login attempts here
return Task.CompletedTask;
};
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
Code to try to retrieve token
if (!User.Identity.IsAuthenticated)
{
return StatusCode(403);
}
var client = httpClientFactory.CreateClient("spotify");
String spotifyToken = await HttpContext.GetTokenAsync("spotify", "access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", spotifyToken);
var result = await client.GetAsync("v1/me/playlists");
I expected to get a spotify access_token that I can use to call the spotify api but spotifyToken just returns null.
On your callback handling method, in case of IdentityServer, on the ExternalController you read out the external identity of the temporary cookie - the result (in case of a success) holds both the Access & Refresh Token as Properties:
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result.Succeeded != true)
throw new Exception("External authentication error");
var tokens = result.Properties.GetTokens();
if (tokens?.Any() == true)
{
_logger.LogDebug("External authentication success resulted in provided access tokens: \r\n {0}.", string.Join(",", tokens.Select(d => $"{d.Name}:{d.Value}")));
}
I am trying to secure my web api using azure active directory.
I have the following code to setup the web api:
public void BuildConfiguration(IAppBuilder appBuilder)
{
var HttpConfiguration = new System.Web.Http.HttpConfiguration();
HttpConfiguration.Filters.Add(new AuthorizeAttribute());
HttpConfiguration.MapHttpAttributeRoutes();
HttpConfiguration.Routes.MapHttpRoute("DefaultApi",
$"api/{{controller}}/{{action}}/{{id}}", new { id = RouteParameter.Optional });
var authenticationProvider = new OAuthBearerAuthenticationProvider()
{
OnValidateIdentity = AuthenticationOnValidateIdentity
};
var options = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = "https://xxx.onmicrosoft.com/yyy"
},
Tenant = "xxx.onmicrosoft.com",
Provider = authenticationProvider
};
appBuilder.UseWindowsAzureActiveDirectoryBearerAuthentication(options);
appBuilder.UseCors(CorsOptions.AllowAll);
appBuilder.UseWebApi(HttpConfiguration);
HttpConfiguration.EnsureInitialized();
}
Now, the code to perform the validation looks like this:
private async Task AuthenticationOnValidateIdentity(OAuthValidateIdentityContext context)
{
context.Ticket.Identity.AddClaim(new Claim("apikey", xx)); // <- what to put here instead of xx?
}
The client requests a token using this piece of code:
AuthenticationResult result = null;
try
{
result = await authContext.AcquireTokenAsync("https://xx.onmicrosoft.com/yyy", clientId, redirectUri, new PlatformParameters(PromptBehavior.Always), UserIdentifier.AnyUser);
}
catch (AdalException ex)
{
...
}
How can I pass additional info like an apikey (string) to the AAD validator so I can access the apikey in 'AuthenticationOnValidateIdentity'?
I tried the extraQueryParameters parameter like his:
result = await authContext.AcquireTokenAsync("https://xxx.onmicrosoft.com/yyy",
clientId,
redirectUri,
new PlatformParameters(PromptBehavior.Always),
UserIdentifier.AnyUser, "apikey=test");
but since I cannot get these values anywhere in AuthenticationOnValidateIdentity I wonder if that is the way to go.
So, the question remains: How can I pass aditional information to Azure Active Directory authentication flow?