Use Azure AD Graph to update values on the `AdditionalValues` dictionary for a user - azure-active-directory

How do I use Azure AD Graph to update values on the AdditionalValues dictionary for a user? The test below returns 400 Bad Response.
Background:
The rest of my application uses MSGraph. However, since a federated user can not be updated using MSGraph I am searching for alternatives before I ditch every implementation and version of Graph and implement my own database.
This issue is similar to this one however in my case I am trying to update the AdditionalData property.
Documentation
[TestMethod]
public async Task UpdateUserUsingAzureADGraphAPI()
{
string userID = "a880b5ac-d3cc-4e7c-89a1-123b1bd3bdc5"; // A federated user
// Get the user make sure IsAdmin is false.
User user = (await graphService.FindUser(userID)).First();
Assert.IsNotNull(user);
if (user.AdditionalData == null)
{
user.AdditionalData = new Dictionary<string, object>();
}
else
{
user.AdditionalData.TryGetValue(UserAttributes.IsCorporateAdmin, out object o);
Assert.IsNotNull(o);
Assert.IsFalse(Convert.ToBoolean(o));
}
string tenant_id = "me.onmicrosoft.com";
string resource_path = "users/" + userID;
string api_version = "1.6";
string apiUrl = $"https://graph.windows.net/{tenant_id}/{resource_path}?{api_version}";
// Set the field on the extended attribute
user.AdditionalData.TryAdd(UserAttributes.IsCorporateAdmin, true);
// Serialize the dictionary and put it in the content of the request
string content = JsonConvert.SerializeObject(user.AdditionalData);
string additionalData = "{\"AdditionalData\"" + ":" + $"[{content}]" + "}";
//additionalData: {"AdditionalData":[{"extension_myID_IsCorporateAdmin":true}]}
HttpClient httpClient = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage
{
Method = HttpMethod.Patch,
RequestUri = new Uri(apiUrl),
Content = new StringContent(additionalData, Encoding.UTF8, "application/json")
};
var response = await httpClient.SendAsync(request); // 400 Bad Request
}

Make sure that the Request URL looks like: https://graph.windows.net/{tenant}/users/{user_id}?api-version=1.6. You need to change the api_version to "api-version=1.6".
You cannot directly add extensions in AdditionalData and it will return the error(400).
Follow the steps to register an extension then write an extension value to user.
Register an extension:
POST https://graph.windows.net/{tenant}/applications/<applicationObjectId>/extensionProperties?api-version=1.6
{
"name": "<extensionPropertyName like 'extension_myID_IsCorporateAdmin>'",
"dataType": "<String or Binary>",
"targetObjects": [
"User"
]
}
Write an extension value:
PATCH https://graph.windows.net/{tenant}/users/{user-id}?api-version=1.6
{
"<extensionPropertyName>": <value>
}

Related

Add tenant claim to access token using IdentityServer 4 based on acr value

In my scenario a user can be linked to different tenants. A user should login in the context of a tenant. That means i would like the access token to contain a tenant claim type to restrict access to data of that tenant.
When the client application tries to login i specify an acr value to indicate for which tenant to login.
OnRedirectToIdentityProvider = redirectContext => {
if (redirectContext.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication) {
redirectContext.ProtocolMessage.AcrValues = "tenant:" + tenantId; // the acr value tenant:{value} is treated special by id4 and is made available in IIdentityServerInteractionService
}
return Task.CompletedTask;
}
The value is received by my identity provider solution and is as well available in the IIdentityServerInteractionService.
The question is now, where can i add a claim to the access token for the requested tenant?
IProfileService
In a IProfileService implementation the only point where acr values would be available is in the IsActiveAsync method when context.Caller == AuthorizeEndpoint in the HttpContext via IHttpContextAccessor.
String acr_values = _context.HttpContext.Request.Query["acr_values"].ToString();
But in IsActiveAsync i can not issue claims.
In the GetProfileDataAsync calls the acr values are not available in the ProfileDataRequestContext nor in the HttpContext. Here i wanted to access acr values when
context.Caller = IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken. If i would have access i could issue the tenant claim.
Further i analyzed CustomTokenRequestValidator, IClaimsService and ITokenService without success. It seems like the root problem is, that the token endpoint does not receive/process acr values. (event though here acr is mentioned)
I have a hard time figure this one out. Any help appreciated. Is it maybe completely wrong what i am trying? After figuring this one out i will have as well to understand how this affects access token refresh.
Since you want the user to login for each tenant (bypassing sso) makes this solution possible.
When logging in, you can add a claim to the local user (IdentityServer) where you store the tenant name:
public async Task<IActionResult> Login(LoginViewModel model, string button)
{
// take returnUrl from the query
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.ClientId != null)
{
// acr value Tenant
if (context.Tenant == null)
await HttpContext.SignInAsync(user.Id, user.UserName);
else
await HttpContext.SignInAsync(user.Id, user.UserName, new Claim("tenant", context.Tenant));
When the ProfileService is called you can use the claim and pass it to the access token:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Only add the claim to the access token
if (context.Caller == "ClaimsProviderAccessToken")
{
var tenant = context.Subject.FindFirstValue("tenant");
if (tenant != null)
claims.Add(new Claim("tenant", tenant));
}
The claim is now available in the client.
Problem is, that with single sign-on the local user is assigned to the last used tenant. So you need to make sure the user has to login again, ignoring and overwriting the cookie on IdentityServer.
This is the responsibility from the client, so you can set prompt=login to force a login. But originating from the client you may want to make this the responsibility of the server. In that case you may need to override the interaction response generator.
However, it would make sense to do something like this when you want to add tenant specific claims. But it seems you are only interested in making a distinction between tenants.
In that case I wouldn't use above implementation but move from perspective. I think there's an easier solution where you can keep the ability of SSO.
What if the tenant identifies itself at the resource? IdentityServer is a token provider, so why not create a custom token that contains the information of the tenant. Use extension grants to create an access token that combines tenant and user and restricts access to that combination only.
To provide some code for others who want to use the extension grant validator as one suggested option by the accepted answer.
Take care, the code is quick and dirty and must be properly reviewed.
Here is a similar stackoverflow answer with extension grant validator.
IExtensionGrantValidator
using IdentityServer4.Models;
using IdentityServer4.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace IdentityService.Logic {
public class TenantExtensionGrantValidator : IExtensionGrantValidator {
public string GrantType => "Tenant";
private readonly ITokenValidator _validator;
private readonly MyUserManager _userManager;
public TenantExtensionGrantValidator(ITokenValidator validator, MyUserManager userManager) {
_validator = validator;
_userManager = userManager;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
String userToken = context.Request.Raw.Get("AccessToken");
String tenantIdRequested = context.Request.Raw.Get("TenantIdRequested");
if (String.IsNullOrEmpty(userToken)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken).ConfigureAwait(false);
if (result.IsError) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
if (Guid.TryParse(tenantIdRequested, out Guid tenantId)) {
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
var claims = result.Claims.ToList();
claims.RemoveAll(x => x.Type == "tenantid");
IEnumerable<Guid> tenantIdsAvailable = await _userManager.GetTenantIds(Guid.Parse(sub)).ConfigureAwait(false);
if (tenantIdsAvailable.Contains(tenantId)) {
claims.Add(new Claim("tenantid", tenantId.ToString()));
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.Result = new GrantValidationResult(principal);
return;
}
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
}
}
}
Client config
new Client {
ClientId = "tenant.client",
ClientSecrets = { new Secret("xxx".Sha256()) },
AllowedGrantTypes = new [] { "Tenant" },
RequireConsent = false,
RequirePkce = true,
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true,
AllowedScopes = new List<String> {
IdentityServerConstants.StandardScopes.OpenId,
},
},
Token exchange in client
I made a razor page which receives as url parameter the requested tenant id, because my test app is a blazor server side app and i had problems to do a sign in with the new token (via _userStore.StoreTokenAsync). Note that i am using IdentityModel.AspNetCore to manage token refresh. Thats why i am using the IUserTokenStore. Otherwise you would have to do httpcontext.signinasync as Here.
public class TenantSpecificAccessTokenModel : PageModel {
private readonly IUserTokenStore _userTokenStore;
public TenantSpecificAccessTokenModel(IUserTokenStore userTokenStore) {
_userTokenStore = userTokenStore;
}
public async Task OnGetAsync() {
Guid tenantId = Guid.Parse(HttpContext.Request.Query["tenantid"]);
await DoSignInForTenant(tenantId);
}
public async Task DoSignInForTenant(Guid tenantId) {
HttpClient client = new HttpClient();
Dictionary<String, String> parameters = new Dictionary<string, string>();
parameters.Add("AccessToken", await HttpContext.GetUserAccessTokenAsync());
parameters.Add("TenantIdRequested", tenantId.ToString());
TokenRequest tokenRequest = new TokenRequest() {
Address = IdentityProviderConfiguration.Authority + "connect/token",
ClientId = "tenant.client",
ClientSecret = "xxx",
GrantType = "Tenant",
Parameters = parameters
};
TokenResponse tokenResponse = await client.RequestTokenAsync(tokenRequest).ConfigureAwait(false);
if (!tokenResponse.IsError) {
await _userTokenStore.StoreTokenAsync(HttpContext.User, tokenResponse.AccessToken, tokenResponse.ExpiresIn, tokenResponse.RefreshToken);
Response.Redirect(Url.Content("~/").ToString());
}
}
}

How do I Authenticate against Active Directory to hit a secured Azure Function from Console App?

I was able to successfully accomplish the following:
Enabled Authentication/Authorization for my Azure Function.
Created an App Registration in Azure for my function to be called securely through AAD auth.
I can successfully authenticate, get a token and hit my Azure Function from Postman.
My question is how can I programmatically do the same, say, from a console application I created?
Will I get a prompt to enter my Microsoft credentials or can I some how configure the credentials to be passed to the console app for authentication?
Here I provide a sample for your reference. The code get access token first and then use the access token to request your function url in console app. When get the access token, I provide two ways(password grant and client_credential grant) in code, you can choose any one of them.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace ConsoleApp16
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
//Get a access token(password grant)
HttpClient client = new HttpClient();
var values = new Dictionary<string, string>
{
{ "client_id", "<your app client id>" },
{ "scope", "<scope>" },
{ "username", "<username>" },
{ "password", "<password>" },
{ "grant_type", "password" },
{ "client_secret", "<your app client secret>" }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("https://login.microsoftonline.com/<your tenant id>/oauth2/v2.0/token", content);
String responseString = await response.Content.ReadAsStringAsync();
dynamic json = JsonConvert.DeserializeObject<Response>(responseString);
String accessToken = json.access_token;
//You can also get the access token by the code below(client_credential grant)
/*
HttpClient client = new HttpClient();
var values = new Dictionary<string, string>
{
{ "client_id", "<your app client id>" },
{ "scope", "<scope>" },
{ "client_secret", "<your app client secret>" },
{ "grant_type", "client_credentials" },
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("https://login.microsoftonline.com/<your tenant id>/oauth2/v2.0/token", content);
var responseString = await response.Content.ReadAsStringAsync();
dynamic json = JsonConvert.DeserializeObject<Response>(responseString);
String accessToken = json.access_token;
*/
//Use the access token to request your function url
HttpClient client1 = new HttpClient();
client1.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
var response1 = await client1.GetAsync("https://myfunapp.azurewebsites.net/api/myHTTPtrigger?name=azure");
String responseString1 = await response1.Content.ReadAsStringAsync();
Console.WriteLine(responseString1);
}
}
public class Response
{
public string access_token { get; set; }
}
}
For the source of some parameters above, please go to your the app which registered in AD first.
You can find the client_id and tenantId in the screenshot below:
You need to new a client secret in the screenshot below, it is the client_secret parameter in the code above.
The scope parameter in my code comes from here:

Microsoft graph API: getting 403 while trying to read user groups

I am trying to get user's group information who log-Ins into the application.
Using below code, when I am hitting https://graph.microsoft.com/v1.0/users/{user}, then I am able to see that user is exist (200), but when trying to hit https://graph.microsoft.com/v1.0/users/{user}/memberOf, then I am getting 403.
private static async Task Test()
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "TOKEN HERE");
var user = "testuser#onmicrosoft.com";
var userExist = await DoesUserExistsAsync(client, user);
Console.WriteLine($"Does user exists? {userExist}");
if (userExist)
{
var groups = await GetUserGroupsAsync(client, user);
foreach (var g in groups)
{
Console.WriteLine($"Group: {g}");
}
}
}
}
private static async Task<bool> DoesUserExistsAsync(HttpClient client, string user)
{
var payload = await client.GetStringAsync($"https://graph.microsoft.com/v1.0/users/{user}");
return true;
}
private static async Task<string[]> GetUserGroupsAsync(HttpClient client, string user)
{
var payload = await client.GetStringAsync($"https://graph.microsoft.com/v1.0/users/{user}/memberOf");
var obj = JsonConvert.DeserializeObject<JObject>(payload);
var groupDescription = from g in obj["value"]
select g["displayName"].Value<string>();
return groupDescription.ToArray();
}
Is this something related to permission issue, my token has below scope now,
Note - Over here I am not trying to access other user/group information, only who log-ins. Thanks!
Calling /v1.0/users/[a user]/memberOf requires your access token to have either Directory.Read.All, Directory.ReadWrite.All or Directory.AccessAsUser.All and this is
documented at https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_memberof.
A great way to test this API call before implementing it in code is to use the Microsoft Graph explorer where you can change which permissions your token has by using the "modify permissions" dialog.

Asp.net core token based claims authentication with OpenIdConnect and angularjs: Bearer was forbidden

I'm using Asp.net core rc2 with OpenIdConnectServer. I'm using angular 1.x with augular-oauth2. After a few days, my error has digressed to
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54275/api/Account/Username
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: Successfully validated the token.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was successfully authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was forbidden.
My ConfigureServices consists of
services.AddAuthorization(options =>
{
options.AddPolicy("UsersOnly", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("role");
});
});
My configure has
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Audience = "http://localhost:54275/",
Authority = "http://localhost:54275/",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "client1",
//ValidAudiences = new List<string> { "", "empty", "null"}
}
});
});
app.UseOpenIdConnectServer(options =>
{
options.AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme;
options.Provider = new SimpleAuthorizationServerProvider();
options.AccessTokenHandler = new JwtSecurityTokenHandler();
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.TokenEndpointPath = new PathString("/oauth2/token");
options.LogoutEndpointPath = new PathString("/oauth2/logout");
options.RevocationEndpointPath = new PathString("/oauth2/revoke");
options.UseJwtTokens();
//options.AccessTokenLifetime = TimeSpan.FromHours(1);
});
My authorize attribute is defined on the Controller as
[Authorize(Policy = "UsersOnly", ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/Account")]
I store the token as a cookie and attach it to requests using an http interceptor in angular.
I generate the token with
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// validate user credentials (demo mode)
// should be stored securely (salted, hashed, iterated)
using (var con = new SqlConnection(ConnectionManager.GetDefaultConnectionString()))
{
if (!Hashing.ValidatePassword(context.Password, await con.ExecuteScalarAsync<string>("SELECT PassHash FROM dbo.Users WHERE Username = #UserName", new { context.UserName })))
{
context.Reject(
error: "bad_userpass",
description: "UserName/Password combination was invalid."
);
return;
}
// create identity
var id = new ClaimsIdentity(context.Options.AuthenticationScheme);
id.AddClaim(new Claim("sub", context.UserName));
id.AddClaim(new Claim("role", "user"));
// create metadata to pass on to refresh token provider
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", context.ClientId}
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), props,
context.Options.AuthenticationScheme);
ticket.SetAudiences("client1");
//ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.Email, OpenIdConnectConstants.Scopes.Profile, "api-resource-controller");
context.Validate(ticket);
}
}
I've spent the last three days on this problem and I realize that at this point I'm probably missing something obvious due to lack of sleep. Any help would be appreciated.
The error you're seeing is likely caused by 2 factors:
You're not attaching an explicit destination to your custom role claim so it will never be serialized in the access token. You can find more information about this security feature on this other SO post.
policy.RequireClaim("role"); might not work OTB, as IdentityModel uses an internal mapping that converts well-known JWT claims to their ClaimTypes equivalent: here, role will be likely replaced by http://schemas.microsoft.com/ws/2008/06/identity/claims/role (ClaimTypes.Role). I'd recommend using policy.RequireRole("user") instead.
It's also worth noting that manually storing the client_id is not necessary as it's already done for you by the OpenID Connect server middleware.
You can retrieve it using ticket.GetPresenters(), that returns the list of authorized presenters (here, the client identifier). Note that it also automatically ensures a refresh token issued to a client A can't be used by a client B, so you don't have to do this check in your own code.

Authentication with custom API controller with Azure Mobile App and Xamarin

I have create a Mobile App service with Azure. I have created a new custom controller as seen below.
[MobileAppController]
public class NewsController : ApiController
{
public ApiServices Services { get; set; }
// GET api/News
public async Task<IEnumerable<NewsItem>> Get()
{//returns some data}
}
Within Azure I have enabled authentication and set the options to Active Directory as seen below.
I'm trying to consume the API within a Xamarin iOS application.
I create a access token via Active Directory as seen below and this works and generates the token correctly.
public static class ServicePrincipal
{
static string authority = "https://login.microsoftonline.com/xxx";
static string clientId = "xxx";
static string clientSecret = "xx";
static string resource = "xx";
public static async Task<AuthenticationResult> GetS2SAccessTokenForProdMSA()
{
return await GetS2SAccessToken();
}
static async Task<AuthenticationResult> GetS2SAccessToken()
{
try
{
AdalInitializer.Initialize();
var clientCredential = new ClientCredential(clientId, clientSecret);
var context = new AuthenticationContext(authority, false);
var authenticationResult = await context.AcquireTokenAsync(
resource,
clientCredential);
return authenticationResult;
}
catch (Exception ex)
{
throw;
}
}
}
However when trying to consume the API i always get an unauthorized exception.
I have tried authenticating by passing the token to the custom API like this. This throws an unauthorized exception
var client = new MobileServiceClient("THE URL");
var authenticationResult = await ServicePrincipal.GetS2SAccessTokenForProdMSA();
var authHeader = new Dictionary<string, string> { { "Bearer", authenticationResult.AccessToken } };
var orderResult = await client.InvokeApiAsync("News", HttpMethod.Get, authHeader);
I also tried the following, which doesn't work either.
CurrentPlatform.Init();
var client = new MobileServiceClient("THE URL");
var authenticationResult = await ServicePrincipal.GetS2SAccessTokenForProdMSA();
JObject payload = new JObject();
payload["access_token"] = authenticationResult.AccessToken;
await client.LoginAsync(MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload);
Can you see any issues here, how do i pass though the authorization token?
I suggest enabling application logging in the Azure portal and then looking to see what the authentication error is.

Resources