Adding a user with custom claim - azure-ad-b2c

I'm having an issue adding a user to Azure B2C via the Microsoft Graph Api with a custom claim. I have added a claim called "sample", and when I add a user via the registration that claim will be populated with the value that I enter.
However, I need to add users via code not with self-registration. I have the following code that will add a user
// create the connection
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var client = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
DisplayName = "John Smith",
UserPrincipalName = $"john.smith#{tenantId}",
MailNickname = "john.smith",
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = false,
Password = "P#ssword1"
},
};
var addedUser = await client.Users.Request().AddAsync(user);
This is the same, but using HTTP direct. Which again will add the users
POST /v1.0/users HTTP/1.1
Host: graph.microsoft.com
SdkVersion: postman-graph/v1.0
Content-Type: application/json
Authorization: Bearer [redacted]
User-Agent: PostmanRuntime/7.20.1
Accept: */*
Cache-Control: no-cache
Postman-Token: [redacted]
Host: graph.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 325
Connection: keep-alive
cache-control: no-cache
{
"accountEnabled": true,
"displayName": "Joe Blogs",
"mailNickname": "joe.blogs",
"userPrincipalName": "joe.blogs#[redacted].onmicrosoft.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": true,
"password": "P#ssword1"
},
"passwordPolicies": "DisablePasswordExpiration"
}
As I say the above code will add a user, but I can't figure out how to add the custom claim at the same time. The first set of code is using the Microsoft.Graph nuget package. The second is taken from a direct http call in Postman.
I seem to be going round in circles when reading the documentation, can can't seem to see how to do it in the new B2C v2.
Anyone, any idea?
Cheers

Ok worked out how to do this. Need to add the user like above, then update it with the custom claim. They key is that claim has the name in the format of
extension_xxxxx_sample
Where the xxxxx has the application id of the b2c-extensions-app application, which is a in build application.
So once the user has been added then the following code will add the custom claim
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var client = new GraphServiceClient(authProvider);
var dictionary = new Dictionary<string, object>();
dictionary.Add("extension_xxxxx_sample", "abcd");
await client.Users[$"john.smith#{tenantId}"]
.Request()
.UpdateAsync(new User()
{
AdditionalData = dictionary
});
The key is that the b2c-extensions-app may have the application id of 7e4ff0cd-825a-47ba-a08c-b13a4244b4ce. However, you would add the claim with the following
extension_7e4ff0cd825a47baa08cb13a4244b4ce_sample
not with
extension_7e4ff0cd-825a-47ba-a08c-b13a4244b4ce_sample

Related

Azure AD B2C API Access through Postman and Web app

As a follow-up on my question about how to setup a ROPC Flow. I want to access my API through the ROPC flow (currently using default user flows) and also through my web app which uses a custom policy on sign-in. This results in two different access tokens. On the left is access token received using the AcquireTokenSilent call and on the right is the access token received through postman with ROPC.
The custom policy token (on the left) gives an "Authorization has been denied for this request." error, while the token on the right is fine. I am assuming that the custom policy token does not work because it does not contain the tfp claim (and if it did, it would be a different one).
How can I set it up so that I can still use the ROPC flow while also using the custom policy? I would like to keep the current userjourney in the custom policy the same. Although if it is possible to somehow add ROPC as an option to it, then it would be fine.
Based on the description above, you are using two policy types - a user flow and a custom policy. And, you are attempting to get SSO between the two.
This is not a supported scenario. This is because the token uses different keys that signs the token.
If custom policies are required for your scenario, I suggest converting the user flow ROPC to a custom policy using this document https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-ropc-policy?tabs=app-reg-ga&pivots=b2c-custom-policy
So I finally found a way to do this in .NET Framework, if you want a solution for .NET Core you would sadly have to look somewhere else.
In your startup add the following.
/*
* Configure the authorization OWIN middleware
*/
private void ConfigureAuthenticationAzure(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(CreateOptions(ClientId, SignUpSignInPolicy, azureDiscoveryEndpoint));
app.UseOAuthBearerAuthentication(CreateOptions(ClientId, ApiPolicy, azureDiscoveryEndpointAPI));
}
private OAuthBearerAuthenticationOptions CreateOptions(string audience, string policy, string discoveryEndpoint)
{
var metadataEndpoint = String.Format(discoveryEndpoint, Tenant, policy);
// This is the default check, in OnValidateIdentity, we check for more.
TokenValidationParameters tvps = new TokenValidationParameters
{
// This is where you specify that your API only accepts tokens from its own clients
ValidAudience = ClientId,
ValidateAudience = true,
AuthenticationType = policy,
NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier",
ValidateIssuer = true,
};
return new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint)),
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = async context =>
{
try
{
var authorizationHeader = context.Request.Headers.Get("Authorization");
var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();
var ticket = context.Ticket;
//var identity = ticket.Identity;
var jwtSecurityToken = new JwtSecurityToken(userJwtToken);
var expiration = jwtSecurityToken.ValidTo.ToLocalTime();
if (expiration < DateTime.Now)
{
log.Warn("The JWT token has expired.");
context.Rejected();
return;
}
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(discoveryEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdconfig = configManager.GetConfigurationAsync().Result;
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openIdconfig.SigningKeys,
ValidateIssuer = true,
ValidIssuer = $"{AzureIssuer.ToLower()}/v2.0/",
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
//ClockSkew = TimeSpan.Zero
};
var handler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = handler.ValidateToken(userJwtToken, validationParameters, out securityToken);
var policyName = principal.FindFirst("tfp")?.Value;
// Add the name claim type for this authentication type
if (policyName.ToLower() == DefaultPolicy.ToLower()) // Sign In Only policy...
{
// Run specific code here for the policy that just sent a token back to the application...
context.Validated(ticket);
return;
}
else if (policyName.ToLower() == SignUpSignInPolicy.ToLower())
{
context.Validated(ticket);
return;
}
context.Rejected();
return;
}
catch(Exception ex)
{
context.Rejected();
return;
}
}
}
};
}

Failed to get AccessToken via authorization code using MSAL 1.1.0-preview in asp.net core

I followed official steps as below to try the scenario "web app calling a Web API in Azure Ad B2C", the only difference is I am using Asp.Net core. I am using AuthorizationCode to get the access token, but it always returns with id token and NULL access token.
Create an Azure AD B2C tenant.
Register a web api.
Register a web app.
Set up policies.
Grant the web app permissions to use the web api.
My code:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
AutomaticChallenge = true,
ClientId = aadB2cSettings.ClientId,
MetadataAddress = $"{aadB2cSettings.Instance}{aadB2cSettings.Tenant}/v2.0/.well-known/openid-configuration?p={aadB2cSettings.B2cSignUpOrSignInPolicy}",
PostLogoutRedirectUri = aadB2cSettings.RedirectUrl,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
var authCode = context.TokenEndpointRequest.Code;
var b2cAuthority = $"{aadB2cSettings.Instance}tfp/{aadB2cSettings.Tenant}/{aadB2cSettings.B2cSignUpOrSignInPolicy}/v2.0/.well-known/openid-configuration";
var cca = new ConfidentialClientApplication(
aadB2cSettings.ClientId,
b2cAuthority,
aadB2cSettings.RedirectUrl,
new ClientCredential(aadB2cSettings.ClientSecret),
new TokenCache(),
null);
try
{
var authResult = await cca.AcquireTokenByAuthorizationCodeAsync(authCode, new[] { "https://hulab2c.onmicrosoft.com/b2cdemo/all" });
context.HandleCodeRedemption(authResult.AccessToken, authResult.IdToken);
}
catch (Exception ex)
{
throw ex;
}
}
},
Used fiddler to capture the request, it is:
POST
https://login.microsoftonline.com/hulab2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_signuporsignin
HTTP/1.1
Request Body:
client_id=1ff91f47-08ee-4973-83f4-379ad7e0679c&client_info=1&client_secret=......&scope=https%3A%2F%2Fhulab2c.onmicrosoft.com%2Fb2cdemo%2Fall+offline_access+openid+profile&grant_type=authorization_code&code=......&redirect_uri=https%3A%2F%2Flocalhost%3A44383%2F
Return:
{"id_token":"......","token_type":"Bearer","not_before":1494494423,"client_info":"......","scope":""}
So only id token, no access token. But we should get access token here, right?
Finally found out my failure reason: the request to get AuthorizationCode doesn't contain the target scope. Reflect in code, for OpenIdConnectOption in aspnetcore, the Scope parameter is readonly and its default value is "opened profile".
Scope is readonly in OpenIdConnectOption
So the default authorization code request sent is:
GET
https://login.microsoftonline.com/hulab2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_signuporsignin&client_id=7f865ca0-271e-4f27-be21-6f0072fe3ad7&redirect_uri=https%3A%2F%2Flocalhost%3A44355%2Fsignin-oidc&response_type=code%20id_token&scope=openid%20profile&response_mode=form_post&nonce=......
HTTP/1.1
Thus, using this authorization code in response to get token, even we set right scope in the token request, we still can't get the access code but only id token, because the provide authorization code is only for "openid profile".
To fix this, we need to add target web api scope into the authorization code as well. Here is the how-to-fix code:
Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Scope += $" offline_access {myapiscope}";
return Task.FromResult(0);
},
......
}
In AspNet, we don't need to do this because its scope is not readonly as aspnetcore and can be set directly:
new OpenIdConnectAuthenticationOptions
{
......
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
}
https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/issues/4 Microsoft have reproduced the issue and working on fix

Access token validation failure when creating Microsoft Graph webhook using the "Web API on-behalf-of flow"

What I am trying to do is to use the "Web API on-behalf-of flow" scenario Microsoft described in this article to create a web hook.
So I started with the Microsoft github example and made sure that I can successfully get the users profile via the Graph API.
Then I modified the code where it gets the users profile to create the web hook, so the code looks like this:
// Authentication and get the access token on behalf of a WPF desktop app.
// This part is unmodified from the sample project except for readability.
const string authority = "https://login.microsoftonline.com/mycompany.com";
const string resource = "https://graph.windows.net";
const string clientId = "my_client_id";
const string clientSecret = "my_client_secret";
const string assertionType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
var user = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var authenticationContext = new AuthenticationContext(authority,new DbTokenCache(user));
var assertion = ((BootstrapContext) ClaimsPrincipal.Current.Identities.First().BootstrapContext).Token;
var userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null
? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value
: ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
var result = await authenticationContext.AcquireTokenAsync(resource,new ClientCredential(clientId,clientSecret),new UserAssertion(assertion,assertionType,userName));
var accessToken = result.AccessToken;
// After getting the access token on behalf of the desktop WPF app,
// subscribes to get notifications when the user receives an email.
// This is the part that I put in.
var subscription = new Subscription
{
Resource = "me/mailFolders('Inbox')/messages",
ChangeType = "created",
NotificationUrl = "https://mycompany.com/subscription/listen",
ClientState = Guid.NewGuid().ToString(),
ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 4230, 0)
};
const string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/";
var request = new HttpRequestMessage(HttpMethod.Post, subscriptionsEndpoint);
var contentString = JsonConvert.SerializeObject(subscription, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
request.Content = new StringContent(contentString, System.Text.Encoding.UTF8, "application/json");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await new HttpClient().SendAsync(request);
if (response.IsSuccessStatusCode)
{
// Parse the JSON response.
var stringResult = await response.Content.ReadAsStringAsync();
subscription = JsonConvert.DeserializeObject<Subscription>(stringResult);
}
The error I get from the response is:
{
"error":
{
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure.",
"innerError":
{
"request-id": "f64537e7-6663-49e1-8256-6e054b5a3fc2",
"date": "2017-03-27T02:36:04"
}
}
}
The webhook creation code was taken straight from the ASP.NET webhook github sample project, which, I have also made sure that I can run successfully.
The same access token code works with the original user profile reading code:
// Call the Graph API and retrieve the user's profile.
const string requestUrl = "https://graph.windows.net/mycompany.com/me?api-version=2013-11-08";
request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await new HttpClient().SendAsync(request);
So I want to find out:
Is creating a webhook via the graph API using the on-behalf-of flow even supported? Not sure if this SO question is what I'm looking for here.
If it is supported, what am I missing here?
If it is not supported, is there an alternative to achieve it? E.g. is there anything from the existing Office 365 API that I can use?
"message": "Access token validation failure.",
The error means you got incorrect access token for the resource . According to your code ,you get the access token for resource :https://graph.windows.net( Azure AD Graph API) , But then you used that access token to access Microsoft Graph API(https://graph.microsoft.com) ,so access token validation failed .

Unauthorized when try to get token using ARM API

I am trying to get token from using Azure Resource Manager API but getting 401-Unauthorized in response.I have my code as below :
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(
System.Text.ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}", client_Id, client_secret))));
var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[]{
new KeyValuePair<string, string>("grant_type", "client_credentials")
});
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var response = client.PostAsync("https://login.windows.net/subscriptionId/oauth2/token", content);
According to your code, you could refer to the following code to retrieve your token:
var client = new HttpClient();
var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[]{
new KeyValuePair<string, string>("resource", "https://management.core.windows.net/"),
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", "{ClientID}"),
new KeyValuePair<string, string>("client_secret", "{ClientSecret}")
});
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.PostAsync("https://login.windows.net/{TennantID}/oauth2/token", content);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Result:
For more details, you could refer to this blog about using the Azure ARM REST API – Get Access Token.
I dont fully understand your code but i know how to construct ARM API calls so i can help you out with the core facts.
What jumps out at me is the fact that your POST URL looks wrong:
you should be using https://login.microsoftonline.com/ - check out the following blogpost Simplifying our Azure AD Authentication Flows
There needs to be the tenantID in your POST uri, not the subscriptionID. Access to subscriptions is managed through assigning RBAC Roles to the serivceprincipal created for the AzureAD App
Here is an example call. I use Postman to check if my constructed calls use the correct values and parameters:
Request
POST /[YOURTENANTID]/oauth2/token?api-version=1.0 HTTP/1.1
Host: login.microsoftonline.com
Cache-Control: no-cache
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Expect: 100-continue
Postman-Token: [token]
grant_type=client_credentials&client_id=[YOURCLIENTID]&client_secret=[YOUR-URLENCODED-Secret]&resource=https://management.azure.com/
response:
{
"token_type": "Bearer",
"expires_in": "3599",
"ext_expires_in": "0",
"expires_on": "1485695000",
"not_before": "1485691100",
"resource": "https://management.azure.com/",
"access_token": "[TOKEN]"
}

Custom OAuth2 server returns 401 - Unathorized

I'm trying to do custom OAuth2 authorization server that will support Resource Owner Password Credentials flow. The authorization server is an WebAPI application hosted in IIS7.5.
I have configured startup class where I register custom OAuthServerProvider (AtcAuthorizationServerProvider).
[assembly: OwinStartup(typeof(ATC.WebApi.AuthorizationServer.Startup))]
namespace ATC.WebApi.AuthorizationServer
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new AtcAuthorizationServerProvider(),
RefreshTokenProvider = new AtcRefreshTokenProvider(),
AuthenticationMode = AuthenticationMode.Passive
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions(){});
}
}
}
In my custom provider class, I override ValidateClientAuthentication() function where I accept both client credentials receiving ways (in Body and in Authorization header).
public class AtcAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
// get client credentials from header or from body
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
//rest of code
Everything works fine when I send client_id and client_secret in body.
POST /ATC.WebApi.AuthorizationServer/token HTTP/1.1
Host: localhost
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
grant_type=password&password=123456&username=myUser&client_id=myClient&client_secret=123%40abc
I get access token successfully.
{
"access_token": "3Fk_Ps10i45uL0zeCzIpvEh2WHKE8iJVNtKJ2XGWcQWXsT9jllKf...",
"token_type": "bearer",
"expires_in": 1799,
"refresh_token": "4c1097d17dd14df5ac1c5842e089a88e",
"as:client_id": "myClient"
}
However, if I use DotNetOpenAuth.OAuth2.WebServerClient which passes client_id and client_secret in Authorization header I will recieve 401.1 - Unauthorized HTTP response. I have found out that the ValidateClientAuthentication() is not fired.
Request than looks like this:
POST /ATC.WebApi.AuthorizationServer/token HTTP/1.1
Host: localhost
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic C16b34lUjEyM0BhYmM=
Cache-Control: no-cache
grant_type=password&password=123456&username=myUser
The question is how to persuade probably the OWIN middle-ware firing my custom Provider in this case?
Well, I finally found out where is the trouble. There was Basic authentication allowed in my IIS, so IIS got the request and tried to Authenticate User which failed and IIS returned 401 Unauthorized immediately. So my OWIN middleware even did not receive the request to processing.

Resources