Retrieving a persistent token for Azure user access - c#

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

Related

Cannot get past Azure Authentication for basic c# application

I am just starting in with Azure and my first attempt is using the Graph client API for a simple data display. In simple terms, I want to get the Teams status of an employee and display it on a form in some graphical way.
I am trying to be as basic as can be so when I tried to download the sample I did not want the UWP project, just basic winform (console would work at the moment). I did borrow from the project and got something to compile but I get the error:
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
This is the full code and I am obviously missing something...what? This is an App that should be able to access the Graph API for a get user read and a getPresence call to show current status with the nee to have a use log in. I can see that Graph Explorer has a token and looking at postman set up there is some way to do this without a interaction, but none of the documentation is clear. I'll continue to pok at this and maybe see if I can get postman to work which might help, but behind the scene's access is not clear to me.
public partial class Form1 : Form
{
//Set the scope for API call to user.read
private string[] scopes = new string[] { "user.read" };
private const string ClientId = "my client id";
private const string Tenant = "my tenant id";
private const string Authority = "https://login.microsoftonline.com/" + Tenant;
// The MSAL Public client app
private static IPublicClientApplication PublicClientApp;
private static string MSGraphURL = "https://graph.microsoft.com/v1.0/";
private static AuthenticationResult authResult;
public Form1()
{
InitializeComponent();
PublicClientApp = PublicClientApplicationBuilder.Create(ClientId).WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient").Build();
callMe();
}
private async void callMe()
{
// Sign-in user using MSAL and obtain an access token for MS Graph
GraphServiceClient graphClient = await SignInAndInitializeGraphServiceClient(scopes);
// Call the /me endpoint of Graph
User graphUser = await graphClient.Me.Request().GetAsync();
Console.WriteLine(graphUser.Id);
var graphu2 = await graphClient.Users["my email address"].Request().GetAsync();
}
private async Task<GraphServiceClient> SignInAndInitializeGraphServiceClient(string[] scopes)
{
GraphServiceClient graphClient = new GraphServiceClient(MSGraphURL,
new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", await getToken(scopes));
}));
return await Task.FromResult(graphClient);
}
public async Task<string> getToken(string[] scopes)
{
PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority(Authority)
.WithLogging((level, message, containsPii) =>
{
Console.WriteLine($"MSAL: {level} {message} ");
}, LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true)
.Build();
IEnumerable<IAccount> accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false);
IAccount firstAccount = accounts.FirstOrDefault();
try
{
authResult = await PublicClientApp.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
Console.WriteLine($"MsalUiRequiredException: {ex.Message}");
authResult = await PublicClientApp.AcquireTokenInteractive(scopes)
.ExecuteAsync()
.ConfigureAwait(true);
}
return authResult.AccessToken;
}
Apologies but I'm going to ignore your code and break it back to something that's a lot more simple.
using Azure.Identity;
using Microsoft.Graph;
namespace StackoverflowAnswer
{
internal class Program
{
static void Main(string[] args)
{
MainAsync().Wait();
}
static async Task MainAsync()
{
var tenantId = "YOUR_TENANT_ID";
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
try
{
string[] scopes = { "https://graph.microsoft.com/.default" };
ClientSecretCredential clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
GraphServiceClient graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var users = await graphClient.Users.Request().GetAsync();
foreach (var user in users)
Console.WriteLine(user.UserPrincipalName);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
A lot of the above code was taken from the following documentation as once you've authenticated, the rest of the SDK is much the same. It can be tricky in points though depending on the specific nature of what you want to do ...
https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/tokencredentials.md
This also helps ...
https://learn.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#client-credentials-provider
Also make sure that you've assigned the desired API permissions to the app in the Azure Portal ...
... and also make sure you've set a client secret for your app. If you have a client ID then you've clearly already gotten that far ...
https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
Update
Now, in relation to working with the Presence API, this is a little more tricky.
Although it appears to, the Presence API doesn't support application permissions. There is an application permission for it but put simply, it doesn't work. This user voice link provides insight on that.
https://techcommunity.microsoft.com/t5/microsoft-365-developer-platform/graph-api-presence-should-support-application-permissions/idi-p/2276109
So what you need to do is apply the delegated permissions to your registered application.
Because of that, you need to use a UsernamePasswordCredential rather than a ClientSecretCredential in your code and replace it when instantiating the GraphServiceClient.
UsernamePasswordCredential usernamePasswordCredential = new UsernamePasswordCredential("<USERNAME>", "<PASSWORD>", tenantId, clientId);
Further to that, you'll need to make sure that the user in question has granted access to use that permission. If it was a user facing app, then they'd log in and be presented with the question to approve the permissions that you have set but because it's not, you need to go to the Enterprise Applications section in Azure AD, find your app, go to Permissions and press the Grant admin consent button for your tenant.
Someone may have a better approach than the above but it's the only way I could find to do it. It will mean if someone knows the client ID and how to authenticate, they can then execute the same API's as you.
Anyway, that will then allow you to get the presence of all users in your organisation.

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;
}

Access users from Azure active Directory having persmison type as delegated

I am new to the Azure. I had a requirement retrieve the the users from Azure active directory. I am having Delegated Type, and user.Read and users.Readall.
I am able to get the bearer code but I am getting permission denied when it goes to retrieve the users.
I need to write a console application in c#, below is the code I tried .
var app = ConfidentialClientApplicationBuilder.Create(clientID)
.WithClientSecret(Secret)
.WithRedirectUri(URL)
.WithAuthority("https://login.microsoftonline.com/tenantID/oauth2/token")
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
var token = app.AcquireTokenForClient(scopes).ExecuteAsync().Result;
- GraphServiceClient graphClient1 = new
GraphServiceClient("https://graph.microsoft.com/v1.0", new
DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.AccessToken);
}));
var users = graphClient1.Users.Request().Top(99).GetAsync();
var organization = graphClient1.Organization.Request().GetAsync().Result;
I am getting below
system.AggregateException: 'One or more errors occurred. (Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.
for the LIne **var organization = graphClient1.Organization.Request().GetAsync().Result;**
Need help on this
Thanks in advance.
Since you are using delegated permissions it requires users to login to use these permisssions.So, https://graph.microsoft.com/.default is not the correct scope and it should be with delegated permission, For example https://graph.microsoft.com/User.Read.All.
Also, you need to use await before the graph request,
var users = await graphClient.Users.Request().Top(99).GetAsync();
Since you are new to Ms Graph please go through a simple console app code sample with helps you to implement easily.

B2B access in multi tenant application

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

Categories

Resources