the
Lately I joined a project that is using Azure AD Open ID connect authentication code to authenticate with the ASP.Net Core web application.
When I am trying to run it locally I am facing issues with retrieving info with the GetAccountAsync method (Return null). From what I read, I think the code is missing a caching helper to cache the user/application tokens.
public async Task<string> GetUserAccessTokenAsync(string userId)
{
var account = await _app.GetAccountAsync(userId);
try
{
var result = await _app.AcquireTokenSilent(_scopes, account).ExecuteAsync();
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
throw new ServiceException(new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = "Caller needs to authenticate. Unable to retrieve the access token silently."
});
}
}
If anyone has any idea what I could do to fix this issue Ill be happy to know :)
Thank you!
Related
I am working on a web application using Blazor Server .Net 5. On my index page, I need to show the number of online users that logged into the website through Azure AD.
First, the user hits the web, and it gets redirected to Azure AD. Once the user is Authenticated in AD he/she will land on the index page. I want to show number of online users inside the app. I started using SignalR, but I am getting a very weird Error.
I am using SingalR client lib
First I created the
PeoplHub : Hub{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Then in my Index.razor I have created
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
I have also Implemented the IAsyncDisposal
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
in my startup I implemented
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
app.UseResponseCompression();
endpoints.MapHub<PeopleHub>("/peoplehub");
When I run the app, I get this error message
An unhandled exception occurred while processing the request.
JsonReaderException: '<' is an invalid start of a value. LineNumber: 2 | BytePositionInLine: 0.
System.Text.Json.ThrowHelper.ThrowJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan<byte> bytes)
InvalidDataException: Invalid negotiation response received.
Microsoft.AspNetCore.Http.Connections.NegotiateProtocol.ParseResponse(ReadOnlySpan<byte> content)
After researching on this issue. I found some useful information. We don't know the known issue, you can create a support ticket and ask for help.
It turns out that there is a known issue breaking SignalR Hubs with Blazor Server and Microsoft Identity.
And I also find official engineer said they don't plan to make improvements in this area given that we haven't seen many customers hitting it.
Related Issue:
blazor server signalr JsonReaderException
Workaround
ASP.NET Core Blazor Server additional security scenarios
Adding on to the answer by Jason Pan.
A quick way to validate the authorization is the problem.
Since I knew my code worked without Authorization in a dotnet 7 app
and this error was seen when I moved the code into my production code (dotnet 6)
where we use authorization with Azure AD
I ran a test with "AllowAnymous" on the hub.
[AllowAnonymous()] //TODO: authorize...
public class SignalrHub : Hub
{
and everything works as expected.
Next : follow the workaround as posted by Jason
There are a number of solutions to this:
use the build-in dialog provided by esri/IdentityManager (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html)
use a server-side proxy (https://github.com/Esri/resource-proxy)
use the identity manager initialize() method (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html#initialize)
But there what is missing is the ability to hook into the request for a token. I am working with ArcGISDynamicMapServiceLayer and there is no way to know if the server return a 498/499, and no way to update the url to update the token.
I started hacking around in the API to try to hook into various events with no real promise of success. What seems to be missing:
a way to detect when a token is needed
a way to update the token
Closes I came up with is listening for "dialog-create" but there is no way to disable the dialog apart from throwing an exception, which disables the layer.
I tried replacing the "_createLoginDialog" method and returning {open: true} as a trick to pause the layers until I had a token ready but since there is no way to update the layer endpoint I did not pursue this hack. It seems the only way this might work is to use the initialize() method on the identity manager.
Does anyone have knowledge of options beyond what I have outlined?
EDIT: The goal is to provide a single-sign-on experience to users of our product.
"User" is already signed in to our application
"User" wishes to access a secure ESRI ArcGIS Server MapServer or FeatureServer services from the ESRI JSAPI
"User" is prompted for user name and password
The desired flow is to acquire a token on the users behalf using a RESTful services in our product and return the appropriate token that will allow the "User" to access the secure services without being prompted.
I do not wish to use a proxy because I do not want all that traffic routed through the proxy.
I do not wish to use initialize() because it is complicated and not clear how that works apart for re-hydrating the credentials.
I do wish for an API that simply allows me to set the token on any layer services that report a 499 (missing token) or 498 (invalid token), but I cannot find any such API. The solution I am focusing on hinges on being able to update the url of an ArcGISImageServiceLayer instance with a new token.
This answer lacks in satisfaction but delivers on my requirements. I will start with the code (client-side typescript):
class TokenProxy {
private tokenAssuranceHash = {} as Dictionary<Promise<{ token: string, expiration: string }>>;
private service = new TokenService();
private timeoutHandle = 0;
watchLayer(esriLayer: ArcGISDynamicMapServiceLayer) {
setInterval(async () => {
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
}, 5000);
}
updateRefreshInterval(ticks: number) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = setTimeout(() => {
Object.keys(this.tokenAssuranceHash).forEach(url => {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
});
this.updateRefreshInterval(ticks);
}, ticks);
}
async tokenAssurance(url: string) {
if (!this.tokenAssuranceHash[url]) {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
}
try {
const response = await this.tokenAssuranceHash[url];
await this.recomputeRefreshInterval();
return response.token;
} catch (ex) {
console.error(ex, "could not acquire token");
return null;
}
}
async recomputeRefreshInterval() {
const keys = Object.keys(this.tokenAssuranceHash);
if (!keys.length) return;
const values = keys.map(k => this.tokenAssuranceHash[k]);
const tokens = await Promise.all(values);
const min = Math.min(...tokens.map(t => new Date(t.expiration).getTime()));
if (Number.isNaN(min)) return; // error occured, do not update the refresh interval
const nextRefreshInTicks = min - new Date().getTime();
this.updateRefreshInterval(0.90 * nextRefreshInTicks);
}
}
And highlight the hack that makes it work:
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
The "_url" is a hidden/private model that I should not be using to update the token but it works.
I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.
I'm trying to change an Azure AD user password.
The user is already authenticated in a SPA application using the implicit flow and the adal library.
While calling:
return await graphClient.Me.Request().UpdateAsync(new User
{
PasswordProfile = new PasswordProfile
{
Password = userPasswordModel.NewPassword,
ForceChangePasswordNextSignIn = false
},
});
I'm getting this exception:
{"Code: Request_ResourceNotFound\r\nMessage: Resource
'35239a3d-67e3-4560-920a-2e9ce027aeab' does not exist or one of its
queried reference-property objects are not present.\r\n\r\nInner
error\r\n"}
Debugging the access token I got with Microsoft Client SDK I see that this GUID is referring to the oid and sub properties. See below:
This is the code I use to acquire the token:
IConfidentialClientApplication clientApp =
ConfidentialClientApplicationBuilder.Create(Startup.clientId)
.WithAuthority(string.Format(AuthorityFormat, Startup.tenant))
.WithRedirectUri(Startup.redirectUri)
.WithClientSecret(Startup.clientSecret)
.Build();
var authResult = await clientApp.AcquireTokenForClient(new[] { MSGraphScope }).ExecuteAsync();
return authResult.AccessToken;
I'm using the implicit flow in a SPA application. While doing ClaimsPrincipal.Current I see that my user is authenticated and all claims are present.
I've read a lot of docs # GitHub and Microsoft Docs but it's still not clear in my mind how to implement this.
By the way, I'm using these Microsoft Graph packages:
<package id="Microsoft.Graph" version="1.15.0" targetFramework="net461" />
<package id="Microsoft.Graph.Auth" version="0.1.0-preview.2" targetFramework="net461" />
<package id="Microsoft.Graph.Core" version="1.15.0" targetFramework="net461" />
I guess I'm not approaching this correclty because instead of acquiring a token for the application I should acquire a token for the user. Should I use clientApp.AcquireTokenOnBehalfOf instead?
If so what's the recommended way of acquiring a token for the currently logged in user using Microsoft Graph API SDK?
Can you shed some light?
####### EDIT #######
I was able to make some progress using this:
var bearerToken = ClaimsPrincipal.Current.Identities.First().BootstrapContext as string;
JwtSecurityToken jwtToken = new JwtSecurityToken(bearerToken);
var userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
IEnumerable<string> requestedScopes = jwtToken.Audiences.Select(a => $"{a}/.default");
var authResult = clientApp.AcquireTokenOnBehalfOf(new[] { MSGraphScope }, userAssertion).ExecuteAsync().GetAwaiter().GetResult();
return authResult.AccessToken;
but now I'm getting the error:
insufficient privileges to complete the operation.
authorization_requestdenied
After a long debugging session (8 hours or so) I was finally able to get what I wanted after I saw this answer by #Michael Mainer.
This is the "right" code I put together:
public async Task<User> ChangeUserPassword(UserPasswordModel userPasswordModel)
{
try
{
var graphUser = ClaimsPrincipal.Current.ToGraphUserAccount();
var newUserInfo = new User()
{
PasswordProfile = new PasswordProfile
{
Password = userPasswordModel.NewPassword,
ForceChangePasswordNextSignIn = false
},
};
// Update the user...
return await graphClient.Users[graphUser.ObjectId].Request().UpdateAsync(newUserInfo);
}
catch(Exception e)
{
throw e;
}
}
Note 1: graphClient.Users[graphUser.ObjectId] is being used instead of graphClient.Me
Note 2: .ToGraphUserAccount() is from Microsoft.Graph.Auth.
I had a sample PATCH request in Postman that correctly set a new password for the user.
The Access Token used in Postman's Authorization request-header had the same format\properties from the one I was acquiring with Microsoft Graph API. I just compared them using jwt.io. So I must've been calling something wrongly...
I used clientApp.AcquireTokenForClient instead:
var authResult = await clientApp.AcquireTokenForClient(new[] { MSGraphScope }).ExecuteAsync();
return authResult.AccessToken;
where:
MSGraphScope = "https://graph.microsoft.com/.default"
I totally understand if someone finds that my question is very basic or might not make a lot of sense all the way.
I am new to this and I am trying to use the latest .NET Framework 5 with MVC 6 in order to build a Web Api that could be used from an Angular JS client-side. This will allow me to create a website for it, as well as a mobile application by wrapping it with Phonegap. So please bear with me a bit.
What I am trying to achieve for the moment is to have a Web API controller that receives a login request and returns a result to the client based on Cookie Authentication (later the client should store this cookie and use it for communications with the server)
I added the following in the project.json
In the Startup.cs, I added under ConfigureServices:
// Add entity framework support
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
});
// add ASP.NET Identity
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonLetterOrDigit = false;
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
In the Startup.cs, under Configure:
// Using the identity that technically should be calling the UseCookieAuthentication
app.UseIdentity();
Now, in the Controller method to login, I am able to find the user using its email address and the UserManager:
// Verify that the model is valid according to the validation rules in the model itself.
// If it isn't valid, return a 400 Bad Request with some JSON reviewing the errors
if (!ModelState.IsValid)
{
return HttpBadRequest(ModelState);
}
// Find the user in our database. If the user does not exist, then return a 400 Bad Request with a general error.
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError("", INVALID_LOGIN_MESSAGE);
return HttpBadRequest(ModelState);
}
// If the user has not confirmed his/her email address, then return a 400 Bad Request with a request to activate the account.
if (!user.EmailConfirmed)
{
ModelState.AddModelError("Email", "Account not activated");
return HttpBadRequest(ModelState);
}
// Authenticate the user with the Sign-In Manager
var result = await signInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
// If the authentication failed, add the same error that we add when we can't find the user
// (so you can't tell the difference between a bad username and a bad password) and return a 400 Bad Request
if (!result.Succeeded)
{
ModelState.AddModelError("", INVALID_LOGIN_MESSAGE);
return new BadRequestObjectResult(ModelState);
}
return Ok();
The problem is happening at the line:
// Authenticate the user with the Sign-In Manager
var result = await signInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
it is throwing the following error:
Error: No authentication handler is configured to handle the scheme:
Microsoft.AspNet.Identity.Application
I am currently blocked and I searched googled for almost every possible token I could think of and tried multiple solution still in no vain. Any help is highly appreciated.
Regards,
Ok I finally figured it out after writing this whole question and I wanted to share the answer to avoid the hussle for someone else if they commit the same mistake I did!
The problem was that in the Configure in Startup.cs, I called "app.UseIdentity()" after calling "app.UseMVC()". The order should have been inversed. I donno if this is common knowledge or I should have read about it somewhere.