OpenId Connect Authentication Properties - azure-ad-b2c

In my application I am redirecting users to an Azure B2C screen so they can make a change to their account.
I pass the user to B2C using the following code, which includes a reference to properties.Items, which I am populating with a value:
var scheme = OpenIdConnectDefaults.AuthenticationScheme;
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items["data1"] = "[possiblySensitiveData]";
return Challenge(properties, scheme);
When the user returns from the Azure B2C screen to my application, I can then retrieve the value from data1.
Is it possible that someone listening in could also retrieve the value stored in ``data1```? Or are any values passed in this way encrypted by default?

The OpenIdConnectHandler encrypts the data: https://github.com/dotnet/aspnetcore/blob/78ab4bd673c1040cf59400be76daf395062bc6a7/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L450.
message.State = Options.StateDataFormat.Protect(properties);
This data protector is setup here: https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs#L46-L48.
var dataProtector = options.DataProtectionProvider.CreateProtector(
typeof(OpenIdConnectHandler).FullName!, name, "v1");
options.StateDataFormat = new PropertiesDataFormat(dataProtector);
It uses the standard Data Protection system in ASP.NET Core.

Related

ASP.NET Identity using Azure Active Directory... when/how are AspNetUsers db records created

I have a simple MVC .Core 6 web application that will be used on the corporate extranet (via an azure app service).
I've setup Microsoft Identity for login/authentication. This works really well. User requests a page and they're sent off to the corporate azure active directory login page (include 2fa) and returned to the application authenticated with user claims.
BUT... my Identity database tables remain empty (AspNetUsers et al). I was half expecting a record to be created representing the user that just signed in.
I scaffolded the ExternalLogin.cshtml page expecting it to be displayed after a user logs in (and there I could manually create the user if userManager.GetUserAsync(User) == null). But the page is never shown
I think I want AspNetUsers table entries because my simple app does a bit of audit trail stuff (CreatedByUserId, LastUpdatedByUserId) and I would like these to be foreign keys to the AspNetUsers table.
Thoughts? Are my expectations out of whack?
I was way off base on this.
Microsoft.Identity.Web and Microsoft.Identity.Web.UI don't need/create AspNetUsers table (et al).
If you want to build your own users table when a user logs in to the application you can do the following:
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, opts =>
{
opts.Events = new OpenIdConnectEvents()
{
OnTicketReceived = async (context) =>
{
//
// Pull out the user details
//
var objectidentifier = context.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
var nameidentifier = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var name = context.Principal.FindFirstValue("name");
var email = context.Principal.FindFirstValue("preferred_username");
//
// Demo code to create a record in a users db table
//
using var scope = context.HttpContext.RequestServices.CreateScope();
var ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var isUserExists = await ctx.MyApplicationUser.AnyAsync(u => u.ObjectIdentifier == objectidentifier);
if (!isUserExists)
{
var applicationUser = new MyApplicationUser()
{
ObjectIdentifier = objectidentifier,
NameIdentifier = nameidentifier,
Email = email,
Name = name,
};
ctx.MyApplicationUser.Add(applicationUser);
await ctx.SaveChangesAsync();
};
}
};
});
Note that some care is needed to pull out the correct claims.
Note this should probably update an existing user if their details changed (names do change).
This is slightly modified from what I found at: https://dotnetthoughts.net/azure-active-directory-b2c-in-aspnet-core-mvc-part1/

How to query MS Graph API in User Context?

I'm trying to change a user's password using MS Graph API. I was checking earlier questions like this and this where the answer were always similar: register an AAD application, because changing the password requires Delegated
UserAuthenticationMethod.ReadWrite.All permissions, and you cannot set that in a B2C application as a B2C app supports only offline_access and openid for Delegated.
So the answers were always suggesting creating an AAD app, and using this app I could query the Graph API on behalf of the user. The question is, how to achieve this? If I check the documentation from Microsoft: Get access on behalf of a user, it is saying that first you need to get authorization, only then you can proceed to get your access token.
But as part of the authorization process, there is a user consent screen. If I'm calling my ASP.NET Core Web API endpoint to change my password on behalf of my user, how will it work on the server? The client won't be able to consent, if I'm doing these calls on the server, right?
Also, I'm using Microsoft.Graph and Microsoft.Graph.Auth Nuget packages and it's not clear how to perform these calls on behalf of the user. I was trying to do this:
var client = new GraphServiceClient(new SimpleAuthProvider(authToken));
await client.Users[myUserId]
.ChangePassword(currentPassword, newPassword)
.Request()
.PostAsync();
Where SimpleAuthProvider is just a dummy IAuthProvider implementation.
Any ideas how to make this work?
OK, got it:
static void ChangePasswordOfAUser()
{
var myAzureId = "65e328e8-5017-4966-93f0-b651d5261e2c"; // id of B2C user
var currentPassword = "my_old_pwd";
var newPassword = "newPassword!";
using (var client = new HttpClient())
{
var passwordTokenRequest = new PasswordTokenRequest
{
Address = $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
ClientId = clientId, // client ID of AAD app - not the B2C app!
ClientSecret = clientSecret,
UserName = $"{myAzureId}#contoso.onmicrosoft.com",
Password = currentPassword,
Scope = "https://graph.microsoft.com/.default" // you need to have delegate access
};
var response = client.RequestPasswordTokenAsync(passwordTokenRequest).Result;
var userAccessToken = response.AccessToken;
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {userAccessToken}");
var json = System.Text.Json.JsonSerializer.Serialize(new
{
currentPassword = currentPassword,
newPassword = newPassword
});
var changePasswordResponse = client.PostAsync(
$"https://graph.microsoft.com/v1.0/users/{myAzureId}/changePassword",
new StringContent(json, Encoding.UTF8, "application/json"))
.Result;
changePasswordResponse.EnsureSuccessStatusCode();
}
}

Integrate Windows Azure Active Directory Bearer Authentication at run time

Currently, I am integrating Azure Active Directory into my .NET Web API using following code:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = WebConfigurationManager.AppSettings["AzureClientId"],
Tenant = WebConfigurationManager.AppSettings["AzureTenant"]
}
});
The audience and tenant were set in web.config file.
I can get the token correctly and users can log in using their azure AD account.
However, I moved the audience and tenant to database to allow users to change the settings and disable/enable azure login by UI instead of changing the setting in web.config.
The above code was changed to:
var azureSetting = db.GetAzureSetting();
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = azureSetting.AzureClientId,
Tenant = azureSetting.AzureTenant
}
});
When the app first starts, there is no config in the database because users do not enter the config yet. Then users go to azure config screen, enter the correct Client Id, Tenant, Client Secret. But users can not log in using azure AD user.
Can anyone explain this case for me?
Is there any way to save azure config in db instead of web.config?
Your code in the post works when web API start and we can’t change configuration runtime , if you want to enable users change the audience and tenant dynamically, you could handle token validation yourself . You could use JwtSecurityTokenHandler to validate the token after your api app get the access token , code below is for your reference :
public JwtSecurityToken Validate(string token)
{
string stsDiscoveryEndpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
OpenIdConnectConfiguration config = configManager.GetConfigurationAsync().Result;
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = false,
ValidAudience = "https://testbasic1.onmicrosoft.com/TodoListService", //your value from database
IssuerSigningTokens = config.SigningTokens,
ValidateLifetime = false
};
JwtSecurityTokenHandler tokendHandler = new JwtSecurityTokenHandler();
SecurityToken jwt;
var result = tokendHandler.ValidateToken(token, validationParameters, out jwt);
return jwt as JwtSecurityToken;
}
This handler helps you verify the signature of the token to ensure the token was issued by Azure Active Directory,and also verify the claims in the token based on the business logic,in your scenario,you need to confirm audience and tenant.
In you web api app , you could register custom TokenValidationHandler in Global.asax :
GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
You could click here for code sample , you could modify your code to check whether tokens comes from tenant-id which stores in database .

Set display name / username for email local accounts in Azure AD B2C?

I'm new to Azure AD B2C and have set up a site that correctly authenticates using local accounts (email only). When the validation request comes back, I see the email address under the 'emails' claim, but the 'name' claim comes back as 'unknown'.
Looking in Azure portal, the account is created but the name is unset and is 'unknown' for all users that register. This isn't what I was expecting. I would prefer that the 'name' be set to the email address by default so that it is easier to find the account in the portal, since we aren't collecting a 'Display Name' at all for this account type (user already enters given and surname).
Do I have something configured incorrectly, and is there a way to default the username to the email address for local, email only accounts?
Azure AD B2C does not "auto-populate" any fields.
When you setup your sign-up policy or unified sign-up/sign-in policy you get to pick the Sign-up attributes. These are the attributes that are show to the user for him/her to provide and are then stored in Azure AD B2C.
Anything that the user is not prompted for is left empty or in a few select cases (like name as you have observed) set to 'unknown'.
Azure AD B2C can not make assumptions as to what to pre-populate a given attribute with. While you might find it acceptable to use the email as the default for the name, others might not. Another example, the display name, for some, can be prepopulated with "{Given name} {Surname}", but for others, it's the other way around "{Surname, Givenname}".
What you are effectively asking for is an easy way to configure defaults for some attributes which is not that's available today. You can request this feature in the Azure AD B2C UserVoice forum.
At this time, you have two options:
Force your users to explicitly provide this value by select it as a sign-up attribute in your policy.
Add some code that updates these attributes with whatever logic you want (for example in the controller that processes new sign-ups or via a headless client running periodically).
Here's a quick & dirty snippet of .Net code that you can use for this (assuming you want to do this in the auth pipeline (Startup.Auth.cs):
private async Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
try
{
var userObjectId = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
// You'll need to register a separate app for this.
// This app will need APPLICATION (not Delegated) Directory.Read permissions
// Check out this link for more info:
// https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet
var authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(string.Format(graphAuthority, tenant));
var t = await authContext.AcquireTokenAsync(graphResource, new ClientCredential(graphClientId, graphClientSecret));
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.AccessToken);
var url = graphResource + tenant + "/users/" + userObjectId + "/?api-version=1.6";
var name = "myDisplayName";
var content = new StringContent("{ \"displayName\":\"" + name + "\" }", Encoding.UTF8, "application/json");
var result = await client.PostAsync(url, content);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
You'll reference this method when you setup your OpenIdConnectAuthenticationOptions like so:
new OpenIdConnectAuthenticationOptions
{
// (...)
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = OnSecurityTokenValidated,
},
// (...)
};
I wrote this extension:
public static class ClaimsPrincipal
{
public static string Username(this System.Security.Claims.ClaimsPrincipal user)=> user.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
}
Now you can use
User.Identity.Name for name if you have this in your OpenId config in the Startup.cs
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
and User.Username if you include the extension

Sending IM with Skype for Business Online from Console App

I am trying to set up a C# console app that can send notifications/reminders to users via Skype for Business online from a generic AD account. I was excited to see the other day that according to this page, UCWA is now supported in Skype for Business online: https://msdn.microsoft.com/en-us/library/office/mt650889.aspx.
I've been trying to follow this tutorial to get this set up: https://msdn.microsoft.com/en-us/library/office/mt590891(v=office.16).aspx. So far I haven't really had much luck... I have my application set up in Azure AD but I get stuck at the "Requesting an access token using implicit grant flow" step of that article (not 100% certain I'm taking the correct actions before that either)... so far I have this:
string clientId = "xxxxxxxx"
string resourceUri = "https://webdir.online.lync.com";
string authorityUri = "https://login.windows.net/common/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
UserCredential cred = new UserCredential("username", "password");
string token = authContext.AcquireToken(resourceUri, clientId, cred).AccessToken;
var poolReq = CreateRequest("https://webdir.online.lync.com/autodiscover/autodiscoverservice.svc/root", "GET",token);
var poolResp = GetResponse(poolReq);
dynamic tmp = JsonConvert.DeserializeObject(poolResp);
string resourcePool = tmp._links.user.href;
Console.WriteLine(resourcePool);
var accessTokenReq = CreateRequest("https://login.windows.net/common/oauth2/authorize"
+ "?response_type=id_token"
+ "&client_id=" + clientId
+ "&redirect_uri=https://login.live.com/oauth20_desktop.srf"
+ "&state=" + Guid.NewGuid().ToString()
+ "&resource=" + new Uri(resourcePool).Host.ToString()
, "GET",token);
var accessTokenResp = GetResponse(accessTokenReq);
my GetResponse and CreateRequest methods:
public static string GetResponse(HttpWebRequest request)
{
string response = string.Empty;
using (HttpWebResponse httpResponse = request.GetResponse() as System.Net.HttpWebResponse)
{
//Get StreamReader that holds the response stream
using (StreamReader reader = new System.IO.StreamReader(httpResponse.GetResponseStream()))
{
response = reader.ReadToEnd();
}
}
return response;
}
public static HttpWebRequest CreateRequest(string uri, string method, string accessToken)
{
HttpWebRequest request = System.Net.WebRequest.Create(uri) as System.Net.HttpWebRequest;
request.KeepAlive = true;
request.Method = method;
request.ContentLength = 0;
request.ContentType = "application/json";
request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken));
return request;
}
accessTokenResp is an office online logon page, not the access token I need to move forward... so I'm stuck. I've tried quite a few variations of the above code.
I've been scouring the net for more examples but can't really find any, especially since UCWA support for Office 365 is so new. Does anyone have an example of how to do what I am trying to do or can point me to one? Everything I've found so far hasn't really even been close to what I'm trying. I can't use the Skype for Business client SDK unfortunately either as it doesn't meet all of my requirements.
I came to a working solution using ADAL (v3), with the help of steps outlined at
Authentication using Azure AD
Here the steps, which involve requesting multiple authentication tokens to AAD using ADAL
Register your application, as Native Application, in Azure AD.
Perform autodiscovery to find user's UCWA root resource URI.
This can be done by performing a GET request on
GET https://webdir.online.lync.com/Autodiscover/AutodiscoverService.svc/root?originalDomain=yourdomain.onmicrosoft.com
Request an access token for the UCWA root resource returned in the autodiscovery response, using ADAL
For instance, your root resource will be at
https://webdir0e.online.lync.com/Autodiscover/AutodiscoverService.svc/root/oauth/user?originalDomain=yourdomain.onmicrosoft.com
you'll have to obtain a token from AAD for resource https://webdir0e.online.lync.com/
Perform a GET on the root resource with the bearer token obtained from ADAL
GET https://webdir0e.online.lync.com/Autodiscover/AutodiscoverService.svc/root/oauth/user?originalDomain=yourdomain.onmicrosoft.com
This will return, within the user resource, the URI for applications resource, where to create your UCWA application. This in my case is:
https://webpoolam30e08.infra.lync.com/ucwa/oauth/v1/applications
Residing then in another domain, thus different audience / resource, not included in the auth token previously obatained
Acquire a new token from AAD for the host resource where the home pool and applications resource are (https://webpoolam30e08.infra.lync.com in my case)
Create a new UCWA application by doing a POST on the applications URI, using the token obtained from ADAL
Voilá, your UCWA application is created. What I notice at the moment, is that just few resources are available, excluding me / presence. So users' presence can be retrieved, but self presence status can't be changed.
I've been able however to retrieve my personal note, and the following resources are available to me:
people
communication
meetings
Show me some code:
Function to perform the flow obtaining and switching auth tokens
public static async Task<UcwaApp> Create365UcwaApp(UcwaAppSettings appSettings, Func<string, Task<OAuthToken>> acquireTokenFunc)
{
var result = new UcwaApp();
result.Settings = appSettings;
var rootResource = await result.Discover365RootResourceAsync(appSettings.DomainName);
var userUri = new Uri(rootResource.Resource.GetLinkUri("user"), UriKind.Absolute);
//Acquire a token for the domain where user resource is
var token = await acquireTokenFunc(userUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped));
//Set Authorization Header with new token
result.AuthToken = token;
var usersResult = await result.GetUserResource(userUri.ToString());
//
result.ApplicationsUrl = usersResult.Resource.GetLinkUri("applications");
var appsHostUri = new Uri(result.ApplicationsUrl, UriKind.Absolute).GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped);
//Acquire a token for the domain where applications resource is
token = await acquireTokenFunc(appsHostUri);
//Set Authorization Header with new token
result.AuthToken = token;
//
var appResult = await result.CreateApplicationAsync(result.ApplicationsUrl, appSettings.ApplicationId, appSettings.UserAgent, appSettings.Culture);
return result;
}
Usage code ato retrieve OAuth tokens using ADAL
var ucSettings = new UcwaAppSettings
{
UserAgent = "Test Console",
Culture = "en-us",
DomainName = "yourdomain.onmicrosoft.com",
ApplicationId = "your app client id"
};
var acquireTokenFunc = new Func<string, Task<OAuthToken>>(async (resourceUri) =>
{
var authContext = new AuthenticationContext("https://login.windows.net/" + ucSettings.DomainName);
var ar = await authContext.AcquireTokenAsync(resourceUri,
ucSettings.ApplicationId,
new UserCredential("myusername", "mypassword"));
return new OAuthToken(ar.AccessTokenType, ar.AccessToken, ar.ExpiresOn.Ticks);
});
var app = await UcwaApp.Create365UcwaApp(ucSettings, acquireTokenFunc);
It should be of course possible to avoid hard-coding username and password using ADAL, but this was easier for PoC and especially in case of Console Application as you asked
I've just blogged about this using a start-to-finish example, hopefully it will help you. I only go as far as signing in, but you can use it with another post I've done on sending IMs using Skype Web SDK here (see day 13 and 14) and combine the two, it should work fine.
-tom
Similar to Massimo's solution, I've created a Skype for Business Online C# based console app that demonstrates how to sign and use UCWA to create/list/delete meetings and change user presence. I haven't gotten around to extending it to send IM's, but you're certainly welcome to clone my repository and extend it to your needs. Just drop in your Azure AD tenant name and native app ID into the code.
I think they just turned this on today - I was doing something unrelated with the Skype Web SDK samples and had to create a new Azure AD app, and noticed that there are two new preview features for receiving conversation updates and changing user information.
Now everything in the Github samples works for Skype For Business Online.

Resources