I have followed the following tutorial
Azure AD B2C: Call an ASP.NET Web API from an ASP.NET Web App
I am able to run the app.
When the authorisation code is received I put a break point on line var code = notification.Code;and I am able to see all the claims received.
But when I try to print the claims nothing is displayed and the app exits. It shows a null object reference on the following line
string a = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Country).Value;
Any idea how I can print all the claims. Any help will be appreciated. Thank you
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Extract the code from the response notification
var code = notification.Code;
string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
string a = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Country).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
Related
We have Xamarin forms app integrated with azure authentication using MSAL. When we log out we are removing the accounts from the PCA and the code was executed without having any issue. But on the subsequent login, it is getting authenticated without even entering the credentials. It is logging in with previously entered credentials. Looks like the cache is not getting cleared properly.
private async void AuthenticateUser()
{
App.Scopes = new string[] { "<client_id>" + "/.default" };
var redirectUri = "msal<clientId>" + "://auth";
if (Device.RuntimePlatform == Device.iOS)
{
App.PCA = PublicClientApplicationBuilder.Create("<client_id>")
.WithIosKeychainSecurityGroup("<package_name>")
.WithRedirectUri(redirectUri)
.Build();
}
else
{
App.PCA = PublicClientApplicationBuilder
.Create(CommonHelper.ClientId)
.WithRedirectUri(redirectUri)
.Build();
}
var accounts = await App.PCA.GetAccountsAsync();
var uid = new UserIdentifier("<user_name>", UserIdentifierType.OptionalDisplayableId);
AuthenticationResult authResult;
try
{
while (accounts.Any())
{
await App.PCA.RemoveAsync(accounts.First());
accounts = (await App.PCA.GetAccountsAsync()).ToList();
}
var firstAccount = accounts.FirstOrDefault();
authResult = await App.PCA.AcquireTokenSilent(App.Scopes, firstAccount)
.ExecuteAsync();
ProceedToLogin(authResult.AccessToken);
}
catch (MsalUiRequiredException mex)
{
try
{
authResult = await App.PCA.AcquireTokenInteractive(App.Scopes)
.WithParentActivityOrWindow(App.ParentWindow)
.WithLoginHint("<user_name>")
.WithUseEmbeddedWebView(true)
.ExecuteAsync();
ProceedToLogin(authResult.AccessToken);
}
catch (Exception ex)
{
Log(ex);
}
}
}
Please find the code below which will execute during the logout.
public void Logout(string authority)
{
if (App.PCA == null)
{
App.Scopes = new string[] { "<client_id>" + "/.default" };
var redirectUri = "msal<azure_client_id>://auth";
App.PCA = PublicClientApplicationBuilder.Create("<client_id>")
.WithIosKeychainSecurityGroup("<package_name>")
.WithRedirectUri(redirectUri)
.Build();
}
var accounts = App.PCA.GetAccountsAsync().Result;
if (accounts != null)
{
while (accounts.Any())
{
App.PCA.RemoveAsync(accounts.First());
accounts = App.PCA.GetAccountsAsync().Result;
}
}
}
Also, we tried with the below code to clear the cookies. It was working fine in lower versions but again the issue is happening from iOS 14.6 and above.
var cookieStorage = NSHttpCookieStorage.SharedStorage;
foreach (var cookie in cookieStorage.Cookies)
{
cookieStorage.DeleteCookie(cookie);
}
Try adding the following line
.WithPrompt(Prompt.ForceLogin);
Example
PublicClientApplicationBuilder.Create("<client_id>")
.WithIosKeychainSecurityGroup("<package_name>")
.WithRedirectUri(redirectUri)
.WithPrompt(Prompt.ForceLogin);
.Build();
As far as I understand this from my own implementation of MSAL in .NET this is working as expected, I'm not sure how well this carries over to Xamarin. When you log a user out of they will no longer be authenticated against your application, but will keep authentication to Microsoft. When you send an unauthenticated user to the Microsoft endpoint to log back in to your application (as in your screenshot) Microsoft correctly identifies that they are still logged in to their Microsoft account, however that account is not logged in to your application. At this point Microsoft offers the list you see giving the option to use the authenticated account or choose a different one to use sign in to your application.
Their are two levels in play when authenticating against MS, authentication against MS and authentication against your application. Your application can only clear authentication against itself, not Microsoft which lets user stay logged into to other MS services (Outlook etc).
This question applies to a core3/react project with an external identity provider, created as follows.
dotnet new react --auth Individual --use-local-db --output conf
and modified to support an external identity provider. The package is added
dotnet add package Microsoft.AspNetCore.Authentication.MicrosoftAccount
and startup is modified
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
options.CallbackPath = "/signin-microsoft";
})
After following the instructions provided by Microsoft I tested my work by registering as a user. No errors were thrown but the promised confirmation email never arrived.
Following the troubleshooting advice at the end of the instructions I set a breakpoint at the start of the SendEmailAsync method of my implementation of IEmailSender and repeated the exercise. The breakpoint is not hit.
If I manually confirm the account by updating the database,
I am able to log in.
The Forgot Password link takes me to a password recovery page and using this hits my breakpoint and successfully sends a password reset email with a link that works.
Clearly my implementation of IEmailSender works and is correctly registered. It's not exactly the same as the sample code because I have my own Exchange server and didn't use SendGrid, but it sent an email successfully for password reset and I can repeat this any number of times without a hitch.
Against the slim possibility that it is somehow the cause of the problem, here's my implementation
public class SmtpEmailSender : IEmailSender
{
public SmtpEmailSender(IOptions<SmtpOptions> options)
{
this.smtpOptions = options.Value;
}
private SmtpOptions smtpOptions { get; }
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
var smtp = new SmtpClient();
if (!smtpOptions.ValidateCertificate)
{
smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;
}
smtp.Connect(smtpOptions.Host, smtpOptions.Port, SecureSocketOptions.Auto);
if (smtpOptions.Authenticate)
{
smtp.Authenticate(smtpOptions.Username, smtpOptions.Password);
}
var message = new MimeMessage()
{
Subject = subject,
Body = new BodyBuilder() { HtmlBody = htmlMessage }.ToMessageBody()
};
message.From.Add(new MailboxAddress(smtpOptions.Sender));
message.To.Add(new MailboxAddress(email));
return smtp.SendAsync(FormatOptions.Default, message).ContinueWith(antecedent =>
{
smtp.Disconnect(true);
smtp.Dispose();
});
}
}
Registration in startup.cs looks like this.
services.AddTransient<IEmailSender, SmtpEmailSender>();
services.Configure<SmtpOptions>(Configuration.GetSection("SmtpOptions"));
SmptOptions is just settings hauled out of appsettings.json and injected into the ctor. Obviously that aspect works or password reset emails wouldn't work.
There can't be anything wrong with the registration because the app stops producing a message about needing to read and follow the account confirmation instructions I linked.
To see whether the problem was caused by some inadvertent side-effect of my code I created an instrumented stub of IEmailSender
public class DummyEmailSender : IEmailSender
{
private readonly ILogger logger;
public DummyEmailSender(ILogger<DummyEmailSender> logger)
{
this.logger = logger;
}
public Task SendEmailAsync(string email, string subject, string htmlMessage)
{
logger.LogInformation($"SEND EMAIL\r\nemail={email} \r\nsubject={subject}\r\nhtmlMessage={htmlMessage}\r\n{new StackTrace().ToString().Substring(0,500)}");
return Task.CompletedTask;
}
}
I also updated service registration to match.
This is the simplest possible instrumented stub, and the observed behaviour is the same, it's invoked when the Forgot Password form is submitted and is not invoked when the Confirm Registration form is submitted.
Has anyone ever got the horrible thing to work? How?
Immediately before the failure, this URL https://wone.pdconsec.net/Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&handler=Callback looks like this
Inspecting the page we find the Register button posts a form to /Identity/Account/ExternalLogin?returnUrl=%2Fauthentication%2Flogin&handler=Confirmation
The code for this is available from the dotnet repository.
After cloning the repo https://github.com/dotnet/aspnetcore.git I read the build instructions and succeeded in building dotnet 5 preview. Then I ran clean before switching to the tagged branch release/3.1 to build debugging packages for core3.1 but this fails because the tagged branch brings into play a version of msbuild that's just slightly too old and the remedy suggested by the error message doesn't seem to work. Since my grip on PowerShell is weak (the build script is PowerShell) I am reduced to code inspection. The pertinent code looks like this.
public override async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
if (ModelState.IsValid)
{
var user = CreateUser();
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
}
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
ProviderDisplayName = info.ProviderDisplayName;
ReturnUrl = returnUrl;
return Page();
}
It looks like it ought to work. What do we know?
No unhandled errors are thrown, it makes it through to RegisterConfirmation which puts up a message about the email that never comes.
CreateUser is invoked and succeeds. We know this because the user is created in the database. So it definitely gets past there, which implies that ModelState isn't null and .IsValid is true.
IEmailSender.SendEmailAsync is not actually invoked, despite the code above.
If result.Succeeded is true there should be a log message saying something like "User created an account using Microsoft Account provider"
It redirects to https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname#outlook.com
I'm seeing log messages for most things. Trying to register a second time after the first pass creates the user but fails to send the email, a warning about a DuplicateUserName appears on the console and in the event log. Setting the confirmation directly in the database we are able to log in and then interactively delete the account, and logs appear for these activities.
But no logs appear for confirmation. What's really doing my head in is the fact that it then redirects to https://localhost:5001/Identity/Account/RegisterConfirmation?Email=accountname#outlook.com
That's crazy. In order to get to there, userManager.AddLoginAsync() must return true and the very next line in that case is a write to the logger about creating the user account.
This makes no sense.
You should send confirmation email yourself, it doesn't send automatically.
After registering your user:
string token = await userManager.GenerateEmailConfirmationTokenAsync(user);
string urltoken = Base64UrlEncoder.Encode(token);
string link = string.Format(emailOptions.ConfirmationUrl, user.Id, urltoken);
string body = $"<a href='{link}'>confirm</a>";
await emailSender.SendEmailAsync(user.Email, "confirmation", body);
I created a whole new project and worked the exercise. It works perfectly.
What's the difference? The failing version was added to an existing project that has been jerked back and forth between 3.0 and 3.1 several times in the course of troubleshooting CICD issues. Clearly it's damaged in some unobvious way and this is a non-issue.
The only reason I haven't deleted the whole question is others may fall down this hole.
I created a Web API (Server) project in ASP.Net Core. Testing it with Postman is successful. Consuming the Web API from an ASP.Net Core Blazor (Client) project is also successful. I was able to display the JSon result in the Razor pages without problems. However, when I created a client project in Xamarin.Forms, I wasn't able to successfully get the JSon results but it gives me an "Error occurred while sending request" and an "InnerException "The text associated with this error code could not be found. The certificate authority is invalid or incorrect". The code I used to consume the API is below:
public async Task<List<Contact>> GetAllDataAsync()
{
var Contacts = new List<Contact>();
var uri = new Uri(string.Format(Constants.ContactsUrl, string.Empty));
try
{
var response = await _client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
Contacts = JsonConvert.DeserializeObject<List<Contact>>(content);
}
}
catch (Exception ex)
{
Debug.WriteLine(#"\tERROR {0}", ex.Message);
}
return Contacts;
}
What else did I miss?
I am developing a multi-tenant application registered on my Azure AD that consumes Office 365 apis, Graph API etc.
I followed this Microsoft sample to build my work which uses ADAL .NET library and OpenIdConnect: Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.19.0.0
In ADAL.NET, we use an AuthenticationContext instance with a custom inherited class for the TokenCache (see code the sample code here).
For each request to the authorized resources, depending on the API, we invoke one of these methods (see code below) to get the auth_token that will be put in the request Bearer parameter. Is it the correct way to do it?
We never make use of the method AcquireTokenByRefreshTokenAsync, does it mean that our application never uses the refresh_token? Does it mean that our user will have to relog after one hour? Should we implement a kind of refreshing procedure with AcquireTokenByRefreshTokenAsync in the catch statement? Can it be made without prompting anything to the end-user?
REMARK: I posted a question regarding OpenIdConnect authentication ticket lifetime. To me these two questions are unrelated but they may be.
string signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
public async Task<string> AcquireOutlook365TokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://outlook.office365.com/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//handle token acquisition failure
if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
{
authContext.TokenCache.Clear();
}
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
public async Task<string> AcquireAzureGraphTokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://graph.windows.net/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//Same as other method
}
}
ADAL uses the stored refresh tokens automatically and transparently, you aren't required to perform any explicit action. AcquireTOkenByRefreshToken is in the ADAL surface for legacy reasons, and has been removed from version 3.x. More background at http://www.cloudidentity.com/blog/2015/08/13/adal-3-didnt-return-refresh-tokens-for-5-months-and-nobody-noticed/
So far i have this.
public static async Task<OutlookServicesClient> CreateOutlookClientAsync(string capability)
{
try
{
string authority = CommonAuthority;
// Create an AuthenticationContext using this authority.
_authenticationContext = new AuthenticationContext(authority);
//See the Discovery Service Sample (https://github.com/OfficeDev/Office365-Discovery-Service-Sample)
//for an approach that improves performance by storing the discovery service information in a cache.
DiscoveryClient discoveryClient = new DiscoveryClient(
async () => await GetTokenHelperAsync(_authenticationContext, DiscoveryResourceId));
// Get the specified capability ("Contacts").
CapabilityDiscoveryResult result =
await discoveryClient.DiscoverCapabilityAsync(capability);
var client = new OutlookServicesClient(
result.ServiceEndpointUri,
async () =>
await GetTokenHelperAsync(_authenticationContext, result.ServiceResourceId));
return client;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
if (_authenticationContext != null && _authenticationContext.TokenCache != null)
_authenticationContext.TokenCache.Clear();
return null;
}
}
}
private static async Task<string> GetTokenHelperAsync(AuthenticationContext context, string resourceId)
{
string accessToken = null;
AuthenticationResult result = null;
string myId = WebConfigurationManager.AppSettings["ida:ClientID"];
string myKey = WebConfigurationManager.AppSettings["ida:Password"];
ClientCredential client = new ClientCredential(myId,myKey);
result = await context.AcquireTokenAsync(resourceId, client);
//result =context.AcquireToken(resourceId, ClientID,_returnUri);
accessToken = result.AccessToken;
return accessToken;
}
When i get to result one of two things happen if i user AcquireTokenAsync i get an error stating Application with identifier XXXX was not found in directory api.office.com otherwise if i run AcquireToken i get the login modal to pop but an error occurs indicating the request must contain client_secret .
I have no idea how to resolve this issue i suspect it may have something to do with the actual app configuration i have tried both creating my own app in Azure AD and using VS Connected Service, Has Anyone Else ran into a similar issues?
Based on the errors you're seeing, there seems to be an issue with how your app is registered. The first error usually happens when the app is not marked as multi-tenant, and you login to the app with a tenant other than the one where the app is registered.
The second error is odd. Client secret is what you're reading out of the ida:Password element and passing in the ClientCredential object.
I just put a .NET tutorial up yesterday that walks through setting this stuff up. Take a look and see if that helps get you unblocked.