I am trying to create a bot which would be deployed into MS Teams (and Skype for Business). I see when a user interacts with the bot they are provided with a channelData.tenant.id and the Bot Framework docs say that this is the "The tenant ID for the user." I was wondering if I can use this (or another piece of information coming from the inbound message) to authenticate the user against my Azure AD? Also, would this require me to authenticate the user via an authentication flow like is done with the AuthBot?(https://github.com/MicrosoftDX/AuthBot)
Any help would be great!
You have the tenant.id provided in channelData yes, so you could use it to make some customs requests like with Graph API.
For MS Teams, you can also get more information by calling GetConversationMembersAsync and call AsTeamsChannelAccount method on the members that you got (this method is included in Microsoft.Bot.Connector.Teams NuGet package)
Sample:
// Fetch the members in the current conversation
var connector = new ConnectorClient(new Uri(context.Activity.ServiceUrl));
var members = await connector.Conversations.GetConversationMembersAsync(context.Activity.Conversation.Id);
// Concatenate information about all the members into a string
var sb = new StringBuilder();
foreach (var member in members.Select(m => m.AsTeamsChannelAccount()))
{
sb.AppendLine($"GivenName = '{member.Name}', Email = '{member.Email}', User Principal Name '{member.UserPrincipalName}', AAD ObjectId '{member.ObjectId}', TeamsMemberId = '{member.Id}'");
}
// Post the member info back into the conversation
await context.PostAsync($"People in this conversation: {sb.ToString()}");
With this call you will have additional interesting values: Email and ObjectId (which is the user's Azure AD object ID).
As a conclusion, you still have to log your user if you need to do some authenticated logic, but in MS Teams case you have more information and ways to do it.
Related
I am trying to Grant Admin Consent of a API in an Azure AD application through Graph API. I created the App, created its Client Secret, then created a Service Principal to which I want to add AppRoleAssignment.
The API call to do so requires three attributes in the body (Documentation)
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var appRoleAssignment = new AppRoleAssignment
{
PrincipalId = {Input the Service Principal Id},
ResourceId = {? Where to get this value?},
AppRoleId = {Input the App role of the API I added to my Azure AD App}
};
await graphClient.ServicePrincipals["{servicePrincipal-id}"].AppRoleAssignments
.Request()
.AddAsync(appRoleAssignment);
My question is where to get the ResourceId from? Knowing that this is different from one tenant to the other.
Please note that if I grant the admin consent manually, then run this API call
var appRoleAssignments = GraphAppClient.ServicePrincipals[servicePrincipalId].AppRoleAssignments.Request().GetAsync().Result;
Then revoke the consent, get the ResourceId from what the API returned, and then use it in the original call, the admin consent works fine.
The documentation for the appRoleAssignment resource type says the following about the resourceId property:
The unique identifier (id) for the resource service principal for which the assignment is made.
A good approach to find a service principal in a tenant is to search the servicePrincipals collection, filtering on the appId or servicePrincipalName properties.For example, to search for the Microsoft Graph service principal by its identifier URI:
GET https://graph.microsoft.com/v1.0/servicePrincipals
?$filter=servicePrincipalNames/any(n:n eq 'https://graph.microsoft.com')
Or to find it by its appId:
GET https://graph.microsoft.com/v1.0/servicePrincipals
?$filter=appId eq '00000003-0000-0000-c000-000000000000'
With the Microsoft Graph SDK for .NET, this would look something like this:
var r = await graphServiceClient.ServicePrincipals
.Request()
.Filter("appId eq '00000003-0000-0000-c000-000000000000'")
.GetAsync();
Using Microsoft Graph PowerShell, things look very similar:
$r = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
Using the Azure portal, each app under "Enterprise apps" corresponds to a service principal, and the "Properties" page shows its object ID.
(Once you have the service principal, you can use its appRoles collection to see the list of app roles it publishes, often useful to get the app role's id, which you'll need for the appRoleId property.)
I ended up figuring it out. The resourceId of every set of permission you are trying to add is the objectId of the app in Entreprise Applications. It sounds very confusing, but if you are trying to add Graph API permissions, you need to go to enterprise applications, find GraphAPI, and then find its ObjectId. That is your resourceId.
I am new to the Graph API and would like to call my outlook calendars with the event schedules from a daemon application.
When I login to Microsoft account using the email I use to login to Azure I can see my calendar fine and I can also call the Web API using the Graph Explorer.
E.g. the Graph Explorer call:
https://graph.microsoft.com/v1.0/me/calendars
returns my calendar events fine when I am logged in with my Microsoft account.
Now, I would like to be able to access the same API using a service application i.e. without the user login prompt. So I went to the Azure portal, created and registered a new application, gave it Calendar.Read API permission with the administrator's consent and downloaded a quickstart daemon app which makes
await apiCaller.CallWebApiAndProcessResultASync($"{config.ApiUrl}v1.0/users", result.AccessToken, Display);
call which works i.e. it returns a user so that I can see that the
"userPrincipalName": "XYZ#<formattedemail>.onmicrosoft.com"
which is not what the Graph Explorer call returns. The Graph explorer call:
https://graph.microsoft.com/v1.0/users
and returns "userPrincipalName": "myactualemail"
So basically when I make the Graph Explorer call:
https://graph.microsoft.com/v1.0/me/calendars
it returns the calendars' result which is correct.
However, an equivalent daemon API call
await apiCaller.CallWebApiAndProcessResultASync($"{config.ApiUrl}v1.0/users/f5a1a942-f9e4-460b-9c6c-16f45045548f/calendars", result.AccessToken, Display);
returns:
Failed to call the web API: NotFound
Content: {"error":{"code":"ResourceNotFound","message":"Resource could not be discovered.","innerError":{"date":"2021-12-26T16:46:35","request-id":"67ef50e4-bec6-48ae-9e45-7765436d1345","client-request-id":"67ef50e4-bec6-48ae-9e45-7765436d1345"}}}
I suspect that the issue is in the userPrincipalName mismatch between the Graph Explorer and the daemon application, but I am failing to find a solution to this.
Also note that a normal ASP.NET Core sample which requires manual user login works ok. The issue is only with the daemon application.
There is no "me" in your case, so you need to use https://graph.microsoft.com/v1.0/users/user#domain.demo/calendars url.
When you used Graph Explorer to test the api, you've signed in the website, so /me/calendars contained in the request can know who is me and then return correct data to you.
Come back to your daemon app, we usually use client credential flow to gain the access token/credential to call the api in the daemon so that we don't need to let user sign in and then call the api, this flow makes the app itself can call microsoft graph api. But using this flow will lead to the issue that you can't use me any more because you never signed in yourself, so we should use /users/userPrincipalName/calendars instead.
Then come to the programming module, microsoft provides graph SDK for calling api, this is what you can also see in the api document. You can refer to this document to learn more details about how to use client credential flow with graph SDK. You can also copy my code below.
using Azure.Identity;
using Microsoft.Graph;
public IActionResult Privacy()
{
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "your_tenant_name.onmicrosoft.com";
var clientId = "azure_ad_app_client_id";
var clientSecret = "client_secret";
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var res = graphClient.Users["your_user_id_which_looks_like_xxxx-xxx-xxx-xxxx-xxxxxx"].Calendars.Request().GetAsync().Result;
return View();
}
By the way, if you're not familiar with the flows, you may take a look at my this answer.
I was able to kind of resolve this issue after chatting with the Azure tech guy. It turned out that my Azure account was considered a personal account. And the reason for this apparently was because I was using a personal #yahoo.com email to setup up the Azure account first place. Because of this they would apparently not allow me to purchase o365 and license it. So I had to create a new account with the amazon default domain for S3 - awsapps.com, which I took from my AWS S3 subscription. Then I had to run through a whole process of creating a new email in Azure from my existing S3 custom domain.
After the email was created I was able to purchase o365 basic license (trial version for now) and then login to Azure using a new email. o365 purchase gave me access to outlook and then recreating a new daemon application from the quickstart with the new credentials just worked.
I don't know if it makes sense what I had done as it sounds awfully convoluted. But it seems to work in the end.
I am looking for a complete Step-by-step to accomplish the following objective. I have been looking around and trying many, many ways, but not one of them works.
Objective: using C# .net core code (in Azure Functions) connect to a shared mailbox of Office 365 to retrieve emails [with date filter] to do some processing, then move that email from Inbox into another folder (Processed). I want to use MailKit (or something similar
that is free and has MIT license) to retrieve, load and parse the emails.
What I have done, but of course, I can be way off…
In Azure Active Directory, I have created an App Registration
In API Permissions, I have added and granted (I am an admin) a lot of permissions (everything I guess may be related to this, from
Email, to IMAP, SMTP, POP, Application, User and a bunch of other permissions.
In Certificates & Secrets, I created a client secret and recorded the secret (Value)
From Overview, I recorded the Client ID, Tenant ID, etc.
Code (I tried several variation of this…)
string[] scopes = {"https://graph.microsoft.com/.default" }
/*
for scopes, I have also tried:
“https://graph.microsoft.com/IMAP.AccessAsUser.All”
“https://outlook.office.com/IMAP.AccessAsUser.All”
*/
var authority = authority + tenant;
/*
for authority, I have also tried:
“https://login.microsoftonline.com/“
“https://login.microsoftonline.com/common”
“https://login.microsoftonline.com/oauth2”, etc…
*/
var client = ConfidentialClientApplicationBuilder
.Create(clientID)
.WithClientSecret(secret)
.WithAuthority(new Uri(authority))
.Build();
/* Fails every time! */
CancellationToken cancellationToken = default;
var authResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync(cancellationToken);
/* MailKit to retrieve emails… */
/*
any step-by-step code using MailKit to
accomplish the Objective would be much appreciated.
*/
Firstly, you should not use this method to get the access token.
var client = ConfidentialClientApplicationBuilder
.Create(clientID)
.WithClientSecret(secret)
.WithAuthority(new Uri(authority))
.Build();
This method is using client credential flow which is not supported to use IMAP.AccessAsUser.All delegated permission.
This method mentioned by jstedfast is using Interactive provider. The interactive flow is used by mobile applications (Xamarin and UWP) and desktops applications to call Microsoft Graph.
So if configuring "http://localhost" as a Public client (mobile & desktop) redirect URI for your application doesn't work, I don't think you could implement it in the C# .net core Azure Function app. Azure Function app doesn't support login interactively within it. You can only use it in a console app.
But there are 2 workarounds which allow you to get the user access token for Microsoft Graph.
Implement Username/password provider to generate the client and access token. But it is using ROPC flow which is not recommended by Microsoft. See details on Warning tip.
Configure additional Login Params in App service to get the access token to call Microsoft Graph. Please refer to CONFIGURING AN APP SERVICE TO GET AN ACCESS TOKEN FOR AAD GRAPH API. The key point is changing the additionaloginparams to the following [“response_type=code id_token”, “resource=https://graph.microsoft.com”]. Related official document here.
I am currently implementing an Identity Server solution and I needed some help with the IProfileService and how it works exactly.
It exposes a method called "GetProfileDataAsync". I understand that this is called when IS4 returns a token. So that means the person gets to the login screen, inputs his details, and then before IS4 returns an Identity token and an Access token, this method will get called to add additional claims.
I am currently trying to figure out the best way to implement roles and permissions based authorization. Currently I need to have access to both the permissions and roles that the user has assigned as that is what our existing code does and we are just switching our authentication model to IS4 but keeping the User Management to be as it currently is.
Questions then...
How best do I implement it? I currently have an ApplicationUser class which implements IIdentity. So should I add a list of roles in there and then a list of permissions, and then populate it when I go get it from the DB when the user does a LogIn?
E.G. In this method
ApplicationUser user = await _userRepo.FindByUsername(model.Username);
The alternative is to add each role and each permission as a claim in my UserProfileService, specifically in the method below
public virtual async Task GetProfileDataAsync(ProfileDataRequestContext context)
I read the following
Often IdentityServer requires identity information about users when creating tokens or when handling requests to the userinfo or introspection endpoints. By default, IdentityServer only has the claims in the authentication cookie to draw upon for this identity data. It is impractical to put all of the possible claims needed for users into the cookie, so IdentityServer defines an extensibility point for allowing claims to be dynamically loaded as needed for a user. This extensibility point is the IProfileService and it is common for a developer to implement this interface to access a custom database or API that contains the identity data for users.
With the above situation, as I have implement the IProfileService, does that mean that all claims that are loaded will be automatically returned and put into the Identity/Access token? Does that mean that for every request that is made to the API, my application will be sending in a token (in the cookie) which could get quite big with these claims that include roles and permissions? What is the alternative as the above statement from the IS4 website mentions it is impractical
How best do I implement it? I currently have an ApplicationUser class which implements IIdentity. So should I add a list of roles in there and then a list of permissions, and then populate it when I go get it from the DB when the user does a LogIn?
There is two kind of things here, Roles and Permissions. Roles are data and you can add them to the token and pass to clients and APIs. you can save the Roles in DB any how which fits your design.
To have the roles in the token you need to fetch them in ProfileService and add to token. Sth like this:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.IssuedClaims.AddRange(context.Subject.Claims);
var user = await _userManager.GetUserAsync(context.Subject);
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
}
After doing this your token should contains the roles. make sure to verify the token on https://jwt.ms/
But Permissions are more of real time calculation things.We need to decide about permissions on API based on the user info, user's role or any other data available.
For example a user may have the role as delete (means user can delete things). If this user call order API and tries to delete some one else's order it has to be declined. Means delete permission MUST be calculated based on user's ID + user's role + Order's owner ID.
With the above situation, as I have implement the IProfileService, does that mean that all claims that are loaded will be automatically returned and put into the Identity/Access token? Does that mean that for every request that is made to the API, my application will be sending in a token (in the cookie) which could get quite big with these claims that include roles and permissions? What is the alternative as the above statement from the IS4 website mentions it is impractical
Yes the profile service is called whenever IDS4 needs to return claims about a user to a client applications. If you request an identity and access token - it will get called twice (since you might be putting different claims into each token type).
What is the alternative as the above statement from the IS4 website mentions it is impractical
You should just fetch the data that you need - not extra. As I mentioned above permissions should be calculated on the fly and not be in the token.
Also you can use cache in the ProfileService. But if you use cache you are the one responsible to manage in your code.
I'm using Custom Policy to sign up users in Azure AD B2C.
In the last step, before the JWT is issue, the technical profile does the following:
<!-- Store the user in the AD -->
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" ContinueOnError="false" />
<!-- Sends the user information, including ObjectId to website to store locally-->
<ValidationTechnicalProfile ReferenceId="REST-SendUserInformation" ContinueOnError="false"/>
So, this works fine. However, in my code, if something goes wrong in the step 2 (send the user's information to store locally), I call the Graph API to delete the user that was just created.
However, I noticed that the Graph API works when I "debug" (which means, taking some time to click next.. next...), but when it's running in the server, the user is not deleted from the AD.
It doesn't throw an exception.
My code to delete the user is the following:
public AzureGraphService(IConfiguration configuration)
{
var azureOptions = new AzureAdOptions();
configuration.Bind("AzureAdB2C", azureOptions);
// Client credential provider is used by services and desktop applications to acquire Microsoft Graph access token without a user.
_confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(azureOptions.ClientId)
.WithTenantId(azureOptions.Domain)
.WithClientSecret(azureOptions.ClientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(_confidentialClientApplication);
// Set up the Microsoft Graph service client with client credentials
_graphClient = new GraphServiceClient(authProvider);
}
public async Task DeleteUserFromAD(string azuresubid)
{
try
{
// Delete user by object ID
await _graphClient.Users[azuresubid]
.Request()
.DeleteAsync();
}
catch (Exception ex)
{
// TODO - Log exception?
throw ex;
}
}
After looking the Audit Logs in Azure AD B2C, I found this
"ObjectNotFoundException".
However, I'm certain that the object is correct and the objectID that I use to delete is also correct because it works if I "debug" (go slowly).
My question is:
Is there a delay between the Object (User) is created in the AD B2C and the time that I can actually see and delete the object from there?
Thank you
There is a delay, the policy execution targets the same DCs, your graph api call from your REST API is likely hitting a different DC and cannot find the account at that time. Use retry logic, try 3 times with a 5 second delay. Usually replication completes in 7sec.