Can not create a team from scratch via microsoft graph api - azure-active-directory

I follow this document and tried to create a team in the code
https://learn.microsoft.com/en-us/graph/api/team-post?view=graph-rest-1.0&tabs=csharp%2Chttp.
here is my code snippets:
var scopes = new string[] { "https://graph.microsoft.com/.default" };
// Configure the MSAL client as a confidential client
var confidentialClient = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClient
.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
// Make a Microsoft Graph API call
var team = new Team
{
DisplayName = "My Sample Team",
Description = "My Sample Team’s Description",
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"},
{"members#odata.bind", "[{\"#odata.type\":\"#microsoft.graph.aadUserConversationMember\",\"roles\":[\"owner\"],\"userId\":\"57d4fc1c-f0a3-1111-b41e-22229f05911c\"}]"}
}
};
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClient
.AcquireTokenForClient(scopes)
.ExecuteAsync();
// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
// Make a Microsoft Graph API call
var team = new Team
{
DisplayName = "My Sample Team",
Description = "My Sample Team’s Description",
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"},
{"members#odata.bind", "[{\"#odata.type\":\"#microsoft.graph.aadUserConversationMember\",\"roles\":[\"owner\"],\"userId\":\"57d4fc1c-f0a3-4105-b41e-1ba89f05911c\"}]"}
}
};
but get this error:
"message": "Bind requests not supported for containment navigation property.",\r\n
I'm using the latest Microsoft.Graph library and version is V3.1.8
does anyone have some ideas on this issue or the odata format error?

It seems that the members#odata.bind is still in change. It doesn't work currently.
You need to use members property.
POST https://graph.microsoft.com/v1.0/teams
{
"template#odata.bind":"https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
"displayName":"My Sample Team555",
"description":"My Sample Team’s Description555",
"members":[
{
"#odata.type":"#microsoft.graph.aadUserConversationMember",
"roles":[
"owner"
],
"userId":"9xxxxxc9-f062-48e2-8ced-22xxxxx6dfce"
}
]
}
The corresponding C# code should be:
var team = new Team
{
DisplayName = "My Sample Team557",
Description = "My Sample Team’s Description557",
Members = (ITeamMembersCollectionPage)new List<ConversationMember>()
{
new AadUserConversationMember
{
Roles = new List<String>()
{
"owner"
},
UserId = "9xxxxxc9-f062-48e2-8ced-22xxxxx6dfce"
}
},
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"}
}
};
Unfortunately, when I run the code, it shows:
System.InvalidCastException: 'Unable to cast object of type 'System.Collections.Generic.List`1[Microsoft.Graph.ConversationMember]' to type 'Microsoft.Graph.ITeamMembersCollectionPage'.'
I cannot make it work. The workaround is to use httpClient to send the request in your code.
See a similar question here.
UPDATE:
I have figured it out.
You can try the following code:
var team = new Team
{
DisplayName = "My Sample Team558",
Description = "My Sample Team’s Description558",
Members = new TeamMembersCollectionPage() {
new AadUserConversationMember
{
Roles = new List<String>()
{
"owner"
},
UserId = "9xxxxxc9-f062-48e2-8ced-22xxxxx6dfce"
}
},
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"}
}
};
If you prefer httpClient method, refer to this:
string str = "{\"template#odata.bind\":\"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\",\"displayName\":\"My Sample Team999\",\"description\":\"My Sample Team’s Description555\",\"members\":[{\"#odata.type\":\"#microsoft.graph.aadUserConversationMember\",\"roles\":[\"owner\"],\"userId\":\"9xxxxxc9-f062-48e2-8ced-22xxxxx6dfce\"}]}";
var content = new StringContent(str, Encoding.UTF8, "application/json");
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = client.PostAsync("https://graph.microsoft.com/v1.0/teams", content).Result;
UPDATE 2:
If you need to call it in Postman, use this format:
{
"template#odata.bind":"https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
"displayName":"My Sample Team555",
"description":"My Sample Team’s Description555",
"members":[
{
"#odata.type":"#microsoft.graph.aadUserConversationMember",
"roles":[
"owner"
],
"userId":"9xxxxxc9-f062-48e2-8ced-22xxxxx6dfce"
}
]
}

Related

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?

Getting a Refresh Token from IdentitySever4

I have a Blazor web app that connects to a different Identity Server 4 server. I can get the login to work correctly and pass the access token back the Blazor. However, when the token expires I don't know how to go out and get a new access token? Should I be getting a refresh token and then an access token? I am confused on how this all works.
Blazor Code
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = AzureADDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(AzureADDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:44382";
options.RequireHttpsMetadata = true;
options.ClientId = "client";
options.ClientSecret = "secret";
options.ResponseType = "code id_token token";
options.SaveTokens = true;
options.Scope.Add("IdentityServerApi");
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
});
IdentityServer4 Setup
...
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Hybrid,
AllowAccessTokensViaBrowser = true,
RequireClientSecret = true,
RequireConsent = false,
RedirectUris = { "https://localhost:44370/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44370/signout-callback-oidc" },
AllowedScopes = { "openid", "profile", "email", "roles", "offline_access",
IdentityServerConstants.LocalApi.ScopeName
},
AllowedCorsOrigins = { "https://localhost:44370" },
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true,
AccessTokenLifetime = 1,//testing
UpdateAccessTokenClaimsOnRefresh = true
},
...
UPDATE:
I have updated my code to offline_access for the client and server (thanks for the update below). My next question is how do I inject the request for the refresh token in Blazor once I get rejected because the access token is expired?
I have the Blazor app making calls back to the API (which validates the access token).
public class APIClient : IAPIClient
{
private readonly HttpClient _httpClient;
//add the bearer token to the APIClient when the client is used
public APIClient(IHttpContextAccessor httpAccessor, HttpClient client, IConfiguration configuration)
{
var accessToken = httpAccessor.HttpContext.GetTokenAsync("access_token").Result;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestVersion = new Version(2, 0);
client.BaseAddress = new Uri(configuration["Api_Location"]);
_httpClient = client;
_logger = logger;
}
What do I need to add to my API calls to validate?
Yes, you should obtain a refresh token as well to keep getting new access tokens. To get a refresh token from IdentityServer you need to add the 'offline_access' scope in the 'AllowedScopes' property of your client. You also need to set the 'AllowOfflineAccess' property on your client to true.
After that you need to include 'offline_access' to the scopes sent by the client and you should receive a refresh token in the response.
To use the refresh token, send a request to the token endpoint with everything you sent for the code exchange except replace the 'code' param with 'refresh_token' and change the value for 'grant_type' from 'code' to 'refresh_token'. The IdentityServer4 response to this request should contain an id_token, an access_token, and a new refresh_token.
I think I have found an answer (given the push from Randy). I did something familiar to this post, where I created a generic method in my APIClient.
public async Task<T> SendAsync<T>(HttpRequestMessage requestMessage)
{
var response = await _httpClient.SendAsync(requestMessage);
//test for 403 and actual bearer token in initial request
if (response.StatusCode == HttpStatusCode.Unauthorized &&
requestMessage.Headers.Where(c => c.Key == "Authorization")
.Select(c => c.Value)
.Any(c => c.Any(p => p.StartsWith("Bearer"))))
{
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", _httpAccessor.HttpContext.GetTokenAsync("refresh_token").Result),
new KeyValuePair<string, string>("client_id", "someclient"),
new KeyValuePair<string, string>("client_secret", "*****")
};
//retry do to token request
using (var refreshResponse = await _httpClient.SendAsync(
new HttpRequestMessage(HttpMethod.Post, new Uri(_authLocation + "connect/token"))
{
Content = new FormUrlEncodedContent(pairs)})
)
{
var rawResponse = await refreshResponse.Content.ReadAsStringAsync();
var x = Newtonsoft.Json.JsonConvert.DeserializeObject<Data.Models.Token>(rawResponse);
var info = await _httpAccessor.HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", x.Refresh_Token);
info.Properties.UpdateTokenValue("access_token", x.Access_Token);
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", x.Access_Token);
//retry actual request with new tokens
response = await _httpClient.SendAsync(new HttpRequestMessage(requestMessage.Method, requestMessage.RequestUri));
}
}
if (typeof(T).Equals(typeof(HttpResponseMessage)))
return (T)Convert.ChangeType(response, typeof(T));
else
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync());
}
I don't like that I have to call AuthenticateAsync. Yet, that seems to be the way I have found to get access to the UpdateTokenValue method to delete and then re-add the new access token.

SAML2.0 Access token using 'itfoxtec-identity-saml2'

I am trying to use your Nuget package for dotnet core and I get little bit success also I can login to SAML identity providers like Onelogin,Okta and I got loggin user information also But I am confuse while generating access token(Bearer token to call APIs of SAML identity providers). How will I get that token?
I can see securitytoken object in saml2AuthnResponse but don’t know how to that token and in that object security key and singin key is null.
I am totally new to this so may be I misunderstand something.
Please help me.
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
You can get access to the SAML 2.0 token as a XML string by setting Saml2Configuration.SaveBootstrapContext = true in appsettings.json:
...
"Saml2": {
"SaveBootstrapContext": true,
"IdPMetadata": "https://localhost:44305/metadata",
"Issuer": "itfoxtec-testwebappcore",
...
}
Alternatively you can set the configuration in code:
config.SaveBootstrapContext = true;
Then you can read the SAML 2.0 token as a XML string in the saml2AuthnResponse.ClaimsIdentity.BootstrapContext:
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));
var samlTokenXml = saml2AuthnResponse.ClaimsIdentity.BootstrapContext as string;
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}

Resources