B2B access in multi tenant application - c#

I have succesfully setup a multi tenant application.
For now, I am able to authenticate the user and use tokens to access other resources. (Microsoft Graph & Microsoft AD Graph)
Now I want to get B2B working.
Current flow:
- User signs in
- AuthorizationCodeReceived gets the acquires the token (via $commonAuthority endpoint)
- When requesting a token for the Ad Graph, I am using the $tenantAuthority
This works perfectly when $tenantAuthority is the same tenant authority as the one where the account was created in.
However, if I login with another user (from another tenant, given trust to the actual tenant) and use $tenantAuthority = trusted authority, then I always the following error:
Failed the refresh token:
AADSTS65001: The user or administrator has not consented to use the application with ID
If I change $tenantAuthority to the 'source' tenant authority where the user was created in, everything works fine.
Any help would be greatly appreciated.
Update: Code sample
App has two tenants (tenantA en tenantB) and I will use a user from tenantB with tenantA given a trust to this user.
AuthorizationCodeReceived = async context =>
{
TenantContext.TenantId = "someguid";
var tenantId =
TenantContext.TenantId;
// get token cache via func, because the userid is only known at runtime
var getTokenCache = container.Resolve<Func<string, TokenCache>>();
var userId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var tokenCache = getTokenCache(userId);
var authenticationContext = new AuthenticationContext($"{configuration.Authority}",
tokenCache);
await authenticationContext.AcquireTokenByAuthorizationCodeAsync(
context.Code,
new Uri(context.Request.Uri.GetLeftPart(UriPartial.Authority)),
new ClientCredential(configuration.ClientId, configuration.ClientSecret),
configuration.GraphResourceId);
}
This code works perfectly. Login in with a user from both tenants works perfectly.
But when I need the Graph Service Client or ActiveDirectoryClient, I need to obtain access tokens to been able to address an api for a certain tenant. I retrieve the access tokens like this:
public IGraphServiceClient CreateGraphServiceClient()
{
var client = new GraphServiceClient(
new DelegateAuthenticationProvider(
async requestMessage =>
{
Logger.Debug("Retrieving authentication token to use in Microsoft Graph.");
string token;
var currentUserHomeTenantId = TenantContext.TenantId;
var currentUserObjectId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var authenticationContext =
new AuthenticationContext($"{_configuration.TenantAuthorityPrefix}{currentUserHomeTenantId}",
_tokenCacheFactoryMethod(currentUserObjectId));
var clientCredential = new ClientCredential(_configuration.ClientId, _configuration.ClientSecret);
try
{
token = await GetTokenSilently(authenticationContext, _configuration.GraphResourceId, currentUserObjectId);
}
catch (AdalSilentTokenAcquisitionException e)
{
Logger.Error("Failed to retrieve authentication token silently, trying to refresh the token.", e);
var result = await authenticationContext.AcquireTokenAsync(_configuration.GraphResourceId, clientCredential);
token = result.AccessToken;
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeaderKeys.Bearer, token);
}));
return client;
}
public IActiveDirectoryClient CreateAdClient()
{
var currentUserHomeTenantId = TenantContext.TenantId;
var currentUserObjectId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.ObjectIdentifier).Value;
var graphServiceUrl = $"{_configuration.AdGraphResourceId}/{currentUserHomeTenantId}";
var tokenCache = _tokenCacheFactoryMethod(currentUserObjectId);
var client = new ActiveDirectoryClient(new Uri(graphServiceUrl),
() => GetTokenSilently(
new AuthenticationContext(
$"{_configuration.TenantAuthorityPrefix}{ClaimsPrincipal.Current.FindFirst(ClaimTypes.TenantId).Value}", tokenCache
),
_configuration.AdGraphResourceId, currentUserObjectId
));
return client;
}
When I do a request with one of the two client SDK's, I got the following error:
Failed the refresh token: AADSTS65001: The user or administrator has not consented to use the application with ID.

Changing the catch method when retrieving the Token did the trick:
if(e.ErrorCode == "failed_to_acquire_token_silently")
{
HttpContext.Current.Response.Redirect(authenticationContext.GetAuthorizationRequestUrlAsync(resourceId, _configuration.ClientId, new Uri(currentUrl),
new UserIdentifier(currentUserId, UserIdentifierType.UniqueId), string.Empty);
}

I don't see that you mention that so: in a B2B collaboration you've to invite user from other tenant first. The steps are like that:
invite and authorize a set of external users by uploading a comma-separated values - CSV file
Invitation will be send to external users.
The invited user will either sign in to an existing work account with Microsoft (managed in Azure AD), or get a new work account in Azure AD.
After signed in, user will be redirected to the app that was shared with them
That works perfectly in my case.
Regarding some problems which I've detect:
Trailing "/" at the end of the active directory resource - try to remove it as this may cause problems. Bellow you will find some code to get authentication headers:
string aadTenant = WebServiceClientConfiguration.Settings.ActiveDirectoryTenant;
string clientAppId = WebServiceClientConfiguration.Settings.ClientAppId;
string clientKey = WebServiceClientConfiguration.Settings.ClientKey;
string aadResource = WebServiceClientConfiguration.Settings.ActiveDirectoryResource;
AuthenticationContext authenticationContext = new AuthenticationContext(aadTenant);
ClientCredential clientCredential = new ClientCredential(clientAppId, clientKey);
UserPasswordCredential upc = new UserPasswordCredential(WebServiceClientConfiguration.Settings.UserName, WebServiceClientConfiguration.Settings.Password);
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(aadResource, clientAppId, upc);
return authenticationResult.CreateAuthorizationHeader();
Applications provisioned in Azure AD are not enabled to use the OAuth2 implicit grant by default. You need to explicitly opt in - more details can be found here: Azure AD OAuth2 implicit grant

Related

Code: BadRequest Message: /me request is only valid with delegated authentication flow

I am trying to upload file on onedrive by using microsoft graph onedrive api.
I am using the method for authentication
Client credentials provider
https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#client-credentials-provider
Like:
// /.default scope, and preconfigure your permissions on the
// app registration in Azure. An administrator must grant consent
// to those permissions beforehand.
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "my-tenantid";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
HttpPostedFileBase file = Request.Files;[0];
int fileSize = file.ContentLength;
string fileName = file.FileName;
string mimeType = file.ContentType;
Stream fileContent = file.InputStream;
var res = await graphClient.Me.Drive.Root.ItemWithPath(fileName).Content
.Request()
.PutAsync<DriveItem>(fileContent);
After executing this code then it gives an error in response.
Message: /me request is only valid with delegated authentication flow.
Inner error:
AdditionalData:
date: 2021-12-29T05:30:08
request-id: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
client-request-id: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
ClientRequestId: b51e50ea-4a62-4dc7-b8d2-b26d75268cdc
Client credential flow will generate the token on behalf the app itself, so in this scenario, users don't need to sign in first to generate the token stand for the user and then call the api. And because of the design,when you used Me in the graph SDK, your code/app don't know who is Me so it can't work. You should know the user_id first and use /users/{id | userPrincipalName} instead of /Me, in the SDK, that is graphClient.Users["your_user_id"] instead of graphClient.Me
In your scenario, there're 2 solutions, one way is using delegated authentication flow like what you said in your title, another way is get the user id before calling the graph api so that you can use Users["id"] but not Me
===================== Update=========================
I haven't finished the code yet but I found the correct solution now.
Firstly, we can upload file to one drive by this api, you may check the screenshot if this is one drive or sharepoint:
https://graph.microsoft.com/v1.0/users/user_id/drive/items/root:/testupload2.txt:/content
If it is, then the next is easy, using the code below to get an access token and send http request to calling the api:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "your_azuread_clientid";
var clientSecret = "corresponding_client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var tokenRequestContext = new TokenRequestContext(scopes);
var token = clientSecretCredential.GetTokenAsync(tokenRequestContext).Result.Token;
I know it's complex because the api is not the same as this one which has SDK sample, but I think it also deserves to try if they are similar.

Creating an Azure DevOPS Personal Access Token (PAT) using C#

I am trying to create a PAT using the new capabilities in the TokensHttpClient. However I keep getting authorisation exception. I am using my Microsoft account which is an organization administrator.
VssCredentials creds = new VssClientCredentials();
creds.Storage = new VssClientCredentialStorage();
// Connect to Azure DevOps Services
VssConnection connection = new VssConnection(_uri, creds);
connection.ConnectAsync().SyncResult();
var t = connection.GetClient<TokenAdminHttpClient>();
//next line works as expected
var tokens = t.ListPersonalAccessTokensAsync(connection.AuthorizedIdentity.SubjectDescriptor).Result;
var tokenAdmin = connection.GetClient<TokensHttpClient>();
PatTokenCreateRequest createRequest = new PatTokenCreateRequest();
createRequest.DisplayName = "Niks_Api_Token";
createRequest.Scope = "vso.work_full";
createRequest.ValidTo = DateTime.Now.AddYears(1);
//this is where authorization exception occurs
var result = tokenAdmin.CreatePatAsync(createRequest).Result;
To manage personal access tokens with APIs, you must authenticate with an Azure AD token. Azure AD tokens are a safer authentication mechanism than using PATs. Given this API’s ability to create and revoke PATs, we want to ensure that such powerful functionality is given to allowed users only.
Please check the Prerequisites here.
Make sure your org has been connect to AAD, see here.
Please register an application in Azure AD, make sure the client secret has been created. You can refer to this doc. And add the permission of Azure DevOps.
The sample code to get Azure AD access token.
public static async Task<string> GetAccessTokenAsyncByClientCredential()
{
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(<appId/clientId>)
.WithTenantId(<tenantId>)
.WithClientSecret(<clientSecret>)
.Build();
string[] scopes = new string[] { "499b84ac-1321-427f-aa17-267ca6975798/.default" };
var result = await cca.AcquireTokenForClient(scopes).ExecuteAsync();
return result.AccessToken;
}

Retrieving a persistent token for Azure user access

I’m working on a project where I need access to a users mailbox (similar to how the MS Flow mailbox connector works), this is fine for when the user is on the site as I can access their mailbox from the graph and the correct permissions request. The problem I have is I need a web job to continually monitor that users mail folder after they’ve given permission. I know that I can use an Application request rather than a delegate request but I doubt my company will sign this off. Is there a way to persistently hold an azure token to access the user information after a user has left the site.. e.g. in a webjob?
Edit
Maybe I've misjudged this, the user authenticates in a web application against an Azure Application for the requested scope
let mailApp : PublicClientApplication = new PublicClientApplication(msalAppConfig);
let mailUser = mailApp.getAllAccounts()[0];
let accessTokenRequest = {
scopes : [ "User.Read", "MailboxSettings.Read", "Mail.ReadWrite", "offline_access" ],
account : mailUser,
}
mailApp.acquireTokenPopup(accessTokenRequest).then(accessTokenResponse => {
.....
}
This returns the correct response as authenticated.
I then want to use this users authentication in a Console App / Web Job, which I try to do with
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithClientSecret(Secret)
.WithAuthority(Authority, true)
.WithTenantId(Tenant)
.Build();
System.Threading.Tasks.Task.Run(async () =>
{
IAccount test = await app.GetAccountAsync(AccountId);
}).Wait();
But the GetAccountAsync allways comes back as null?
#juunas was correct that the tokens are refreshed as needed and to use the AcquireTokenOnBehalfOf function. He should be credited with the answer if possible?
With my code, the idToken returned can be used anywhere else to access the resources. Since my backend WebJob is continuous, I can use the the stored token to access the resource and refresh the token on regular intervals before it expires.
Angalar App:
let mailApp : PublicClientApplication = new PublicClientApplication(msalAppConfig);
let mailUser = mailApp.getAllAccounts()[0];
let accessTokenRequest = {
scopes : [ "User.Read", "MailboxSettings.Read", "Mail.ReadWrite", "offline_access" ],
account : mailUser,
}
mailApp.acquireTokenPopup(accessTokenRequest).then(accessTokenResponse => {
let token : string = accessTokenResponse.idToken;
}
On the backend, either in an API, webJob or Console:
var app = ConfidentialClientApplicationBuilder.Create(ClientId)
.WithClientSecret(Secret)
.WithAuthority(Authority, true)
.WithTenantId(Tenant)
.Build();
var authProvider = new DelegateAuthenticationProvider(async (request) => {
// Use Microsoft.Identity.Client to retrieve token
List<string> scopes = new List<string>() { "Mail.ReadWrite", "MailboxSettings.Read", "offline_access", "User.Read" };
var assertion = new UserAssertion(YourPreviouslyStoredToken);
var result = await app.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
var graphClient = new GraphServiceClient(authProvider);
var users = graphClient.Me.MailFolders.Request().GetAsync().GetAwaiter().GetResult();
In the end I had to abandon using the ConfidentialClientApplicationBuilder, I still use PublicClientApplicationBuilder on the front end to get the users consent but then I handle everything else with the oauth2/v2.0/token rest services which returns and accepts refresh tokens.
That way I can ask the user for mailbox consent using PublicClientApplicationBuilder
Access the user mailbox at any time using oauth2/v2.0/token

Unable to authorize Azure LogAnalytics Workspace

I am trying to connect to my workspace in the Azure Portal. I am getting the error as
Operation returned an invalid status code 'Unauthorized'.
The creds object has fetched the Authentication Token and I have added resource permissions to my app as mentioned in this link
using System;
using Microsoft.Azure.OperationalInsights;
using Microsoft.Rest.Azure.Authentication;
namespace LogAnalytics
{
class Program
{
static void Main(string[] args)
{
var workspaceId = "**myworkspaceId**";
var clientId = "**myClientId**";
var clientSecret = "**myClientSecret**";
//<your AAD domain>
var domain = "**myDomain**";
var authEndpoint = "https://login.microsoftonline.com";
var tokenAudience = "https://api.loganalytics.io/";
var adSettings = new ActiveDirectoryServiceSettings
{
AuthenticationEndpoint = new Uri(authEndpoint),
TokenAudience = new Uri(tokenAudience),
ValidateAuthority = true
};
var creds = ApplicationTokenProvider.LoginSilentAsync(domain,clientId, clientSecret,
strong textadSettings).GetAwaiter().GetResult();
var client = new OperationalInsightsDataClient(creds);
client.WorkspaceId = workspaceId;
//Error happens below
var results = client.Query("union * | take 5");
Console.WriteLine(results);
Console.ReadLine();
}
}
}
Operation returned an invalid status code 'Unauthorized'.
According to the error message and the code you provided, you need to add permission in your registered application in Azure AD.
Note: If you want to add permission to application you need to be admin, and then you could use the ClientId and ClientSecret to get Authentication Token and read log analytics.
However, if you are not admin, you could delegate permission to user and access to Azure AD with username and password.
To get authentication token with user, you could can use the function UserTokenProvider.LoginSilentAsync(nativeClientAppClientid, domainName, userName, password).GetAwaiter().GetResult() to get our credentials.

How to get On-Behalf-Of-User token in .NET using ADAL

I have created an Azure AAD app of type webapp which has client secret and redirect url. Now, I want to get an access token on behalf of user using the AAD app. From looking at the documentation, I got the following code so far.
static void Main(string[] args)
{
var clientId = "<REDACTED>";
var clientSecret = "<REDACTED>";
var resourceAppIdURI = "https://api.office.com/discovery/";
var authority = "https://login.microsoftonline.com/common";
AuthenticationContext ac = new AuthenticationContext(authority, new FileCache());
ClientCredential cc = new ClientCredential(clientId, clientSecret);
// Get token as application
var task = ac.AcquireTokenAsync(resourceAppIdURI, cc);
task.Wait();
var appToken = task.Result.AccessToken;
// Get tokenn on behalf of user
UserCredential uc = new UserCredential("usrname#mytenant.com");
task = ac.AcquireTokenAsync(resourceAppIdURI, clientId, uc);
var userToken = task.Result.AccessToken;
Console.ReadLine();
}
But this is the error I get when I try to get user token is as follows.
Message "AADSTS70002: The request body must contain the following
parameter: 'client_secret or client_assertion'.\r\nTrace ID:
0e977f67-d5cb-4cf5-8fea-bac04b6d0400\r\nCorrelation ID:
824a96bf-8007-4879-970c-2680644b8669\r\nTimestamp: 2017-07-21
05:02:41Z" string
Why am I getting this error and how to fix it?
Do I need to login with the user first and then use UserAssertion instead ?
There are tonnes of overloaded methods for AcquireTokenAsync method, but not sure what I should use.
I also looked at this github url to see how they are doing it
https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof/blob/8afb3e6a648d8e7246685bf6747d009006c761b8/TodoListService/Controllers/TodoListController.cs
This is the relevant code to get token as logged in user
ClientCredential clientCred = new ClientCredential(clientId, appKey);
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
string userAccessToken = bootstrapContext.Token;
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(authority, new DbTokenCache(userId));
Here they already have a logged in user and creating a UserAssertion from that loggedin user's token. In my console app, the user hasn't logged in yet.
So I need a way to do this in my console app. How can I show the AAD login page to the user as a pop-up and then once the user enters creds use that info to create an UserAssertion object?
thanks
Your scenario is a native application that calls a web API on behalf of a user . The native application could obtains an access token for the user by using the OAuth 2.0 authorization code grant , then access token is then sent in the request to the web API, which authorizes the user and returns the desired resource :
Please read more about the description of protocol flow here .Also see the code samples for Native Application to Web API scenarios.
In addition , you could click here for code sample about how to call the Azure AD Graph API from a native client(.net console application ) ,it uses the Active Directory Authentication Library (ADAL) for authentication .

Categories

Resources