I trying to implement token base authentication in Blazor webassembly web application with Prerendering enabled.
The steps I have done so far:
Created a sample Blazor Webassembly application
Followed the Official MS doc: Prerender and integrate ASP.NET Core Razor components
Checked if application works
Added token base authentication (custom) changes - ref.
It gives an error at AuthStateProvider
public class AuthStateProvider : AuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(token))
return _anonymous;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")));
return _anonymous;
}
}
It gives an error
InvalidOperationException: JavaScript interop calls cannot be issued
during server-side prerendering, because the page has not yet loaded
in the browser. Prerendered components must wrap any JavaScript
interop calls in conditional logic to ensure those interop calls are
not attempted during prerendering.
at this line
var token = await _localStorage.GetItemAsync<string>("authToken");
Now that is obvious; this line should be wrapped in a condition to check for prerendering like:
public class AuthStateProvider : AuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var isNotPreRendering = this._httpContextAccessor.HttpContext.Response.HasStarted;
if(isNotPreRendering)
{
var token = await _localStorage.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(token))
return _anonymous;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")));
}
return _anonymous;
}
}
But the value of isNotPreRendering always false. Is there any other way or work around to make it work?
I have fixed this issue in this way:
Once the app hits GetAuthenticationStateAsync() will find a try-catch, withing the catch I will return an AnonymousState.
AuthenticationState anonymousState = new(new ClaimsPrincipal(new ClaimsIdentity()));
try
{
var token = await _localStorageService.GetItemAsync<string>("authToken");
var memberId = await _localStorageService.GetItemAsync<int>("memberId");
if (string.IsNullOrEmpty(token) || memberId == 0)
return anonymousState;
_httpClient.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue("bearer", token);
var authState = new AuthenticationState(
new ClaimsPrincipal(
new ClaimsIdentity(JwtParser.ParseClaimFromJwt(token), "jwtAuthType")));
NotifyAuthenticationStateChanged(Task.FromResult(authState));
return authState;
}
catch
{
return await Task.FromResult(anonymousState);
}
In my MainLayout.cs I call the AuthenticationState again. This is not the cleanest solution but it does the work
Related
I am trying to generate access token using On-Behalf-Of flow which will be used to authenticate API calls. I am adding a claim called userIdToken which is value of id_token in startup file(sample below). Then retrieving the value from ClaimsPrincipal to to generate token using AcquireTokenOnBehalfOf method.
It works perfectly fine until userIdToken expires(1 hour). But it won't refresh the userIdToken and hence it throws exception when AcquireTokenOnBehalfOf method is called. Doing a fresh login works for next one hour since it uses new userIdToken .
How can I add token refresh functionality to the existing approach?
Error:
Microsoft.Identity.Client.MsalUiRequiredException: 'AADSTS500133: Assertion is not within its valid time range. Ensure that the access token is not expired before using it for user assertion, or request a new token. Current time: 2022-06-16T10:33:17.8173399Z, expiry time of assertion 2022-06-16T06:49:33.0000000Z.
Startup.cs
services.AddAuthentication("SomeCustomType")
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAD", options);
options.SaveTokens = true;
options.Events.OnTicketReceived = async context =>
{
var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
claimsIdentity.AddClaim(new Claim("DirectAadAuth", "true"));
try
{
claimsIdentity.AddClaim(new Claim("userIdToken", context.Properties.GetTokens().First().Value));
var authedUser = new ClaimsPrincipal(new ClaimsIdentity(claimsIdentity.Claims, "SomeCustomType"));
await context.HttpContext.SignInAsync("CustomType", authedUser, new AuthenticationProperties{IsPersistent = true});
}
catch (System.Exception)
{
}
};
}
)
.EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetSection("CustomConfig").Get<CustomConfigModel>().DownstreamApiScopes)
.AddInMemoryTokenCaches();
TokenClient.cs
public async Task<string> GetAccessTokenAsync(ClaimsPrincipal user)
{
var userIdToken = user.Claims.FirstOrDefault(c => c.Type == "userIdToken")?.Value;
var userAssertion = new UserAssertion(userIdToken);
var scopedResult = await clientApplication.AcquireTokenOnBehalfOf(scopes, userAssertion).ExecuteAsync();
return scopedResult.AccessToken;
}
I've built an ASP.NET core Web App calling an ASP.NET Core Web API that is secured using Azure AD as described here.
I've also built a Xamarin mobile application that authenticates to the Web App's Azure App Service and utilizes the downstream Web API as described here.
The Web App can access the API without issue; however, the Xamarin application fails with an HTTP Response Message: Unauthorized.
Utilizing the guidance from the Microsoft Identity team here, I was able to track down the cause of the failure.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
A snippet from the source code for the requirement shows where it fails. Specifically, it is on the IsAuthenticated check.
public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
{
var user = context.User;
var userIsAnonymous =
user?.Identity == null ||
!user.Identities.Any(i => i.IsAuthenticated);
if (!userIsAnonymous)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
Tracing the code from the working Web App, the context is generated as shown below. The critical point is the GetAccessTokenForUserAsync method gets an access token for a downstream API on behalf of the user account for which the claims are provided in the User member of the controller's HttpContext parameter.
...
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
await PrepareAuthenticatedClient();
var response = await httpClient.GetAsync($"https://example.com/api/mycontroller");
...
private async Task PrepareAuthenticatedClient()
{
var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { "api://xxxx/.default"});
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
Tracing the code from the failing Xamarin application, the context is generated as shown below.
...
PCA = PublicClientApplicationBuilder
.Create(ClientID)
.WithTenantId(TenantID)
.WithRedirectUri($"msal{ClientID}://auth")
.Build();
authResult = await App.PCA.AcquireTokenInteractive("api://xxxx/.default").ExecuteAsync();
var result = Network.GetHttpContentWithTokenAsync(authToken);
...
public async Task<string> GetHttpContentWithTokenAsync(string token)
{
HttpClient client = new HttpClient();
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/mycontroller");
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await client.SendAsync(message);
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
How do get the authenticated user into the HTTP context for the Xamarin application?
I have implemented the authentication with OWIN and bearer token and it works fine when the user login.
When \Token URL is called and username/password is passed to it, that gives token in response. But I would like to store this token in Database so instead of making another call to the server can I get the token in code? I am not able to get the value of the generated token in the ticket or any other object.
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
return Task.Factory.StartNew(() =>
{
var username = context.UserName;
var password = context.Password;
var userService = new UserService();
User user = userService.GetUserByCredentials(username, password);
if (user != null)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, user.userName),
new Claim("UserID", user.userName)
};
ClaimsIdentity oAutIdentity = new ClaimsIdentity(claims, Startup.OAuthOptions.AuthenticationType);
var ticket = new AuthenticationTicket(oAutIdentity, new AuthenticationProperties() { });
context.Validated(ticket);
}
else
{
context.SetError("invalid_grant", "Error");
}
});
}
I am debugging the code but surprisingly the access_token seems to be visible nowhere only getting it in postman results.
The token is not valid forever. A new token is given for every authentication and is valid for a set amount of time. There is no use in saving this token to the database.
Sure you can.
You just need to override the method TokenEndpointResponseinside your authServerProvider : OAuthAuthorizationServerProvider.
Inside OAuthTokenEndpointResponseContext, there is a field called accessToken that you can retrieve the token value.
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
// Summary:
// Called before the TokenEndpoint redirects its response to the caller.
return Task.FromResult<object>(null);
}
I'm trying to work with a application to use IdentityServer4, it has the basic setup of the identity server, MVC client, and web API.
I have a custom Profile service (which I've registered in Startup.cs) where I'm adding a custom claim, here's my GetProfileDataAsync method:
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim("TestFullName", user.FullName),
};
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
My problem is that when I log into the identity server, I can see the additional claim - but when I call my API from the MVC app, my custom claim isn't there. Here's the code in my MVC app to call the API:
public async Task<IActionResult> ClientAuthorizedAPICall(string token)
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("testAPI");
// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:5001/identity");
...
}
And the method on my API is simply:
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
Am I doing something wrong? Or should I be doing something different instead of using User.Claims?
Like rawel's comment says, you'll want to use the MVC app user's access token to make your API call. It would look something like this:
// get the current user's access token
var accessToken = await HttpContext.GetTokenAsync("access_token");
// call api
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync("http://localhost:5001/identity");
You can see a similar approach in the quickstart on hybrid flow.
To get your custom user claim into the access token for your API, you'll need to include it when defining the API resource. For example:
new ApiResource("testAPI", "Test API", new[] { "TestFullName" }),
I'm having trouble when using the Microsoft Graph API. Whenever I try to get a calendar, I get the following error message:
Exception thrown: 'Microsoft.Graph.ServiceException' in
System.Private.CoreLib.dll: 'Code: BadRequest Message: Current
authenticated context is not valid for this request
At first, I thought it was similar to this post, but my user is authenticated, so I believe it's not the case.
Here's my code:
EventController.cs
public async Task<Calendar> GetEventInfoAsync()
{
var accessToken = await getAcessTokenAsync();
DelegateAuthenticationProvider delegateAuthenticationProvider = new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return Task.FromResult(0);
}
);
GraphServiceClient graphClient = new GraphServiceClient(delegateAuthenticationProvider);
var calendar = await graphClient.Me.Calendar.Request().GetAsync();
return calendar;
}
And this is how I get the access token:
public async Task<string> getAcessTokenAsync()
{
if(User.Identity.IsAuthenticated)
{
var userId = User.FindFirst("MicrosoftUserId")?.Value;
ConfidentialClientApplication cca =
new ConfidentialClientApplication( Configuration["MicrosoftAuth:ClientId"],
String.Format(System.Globalization.CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}{1}", "common", "/v2.0"),
Configuration["MicrosoftAuth:RedirectUri"]+ "signin-oidc",
new Microsoft.Identity.Client.ClientCredential(Configuration["MicrosoftAuth:ClientSecret"]),
new SessionTokenCache(userId,_memoryCache).GetCacheInstance(),
null);
var token = await cca.AcquireTokenForClientAsync(new string[]{"https://graph.microsoft.com/.default"});
return token.AccessToken;
}
else
throw new Exception("User is not autenticated");
}
Finally, this is how the authentication options look in the startup file.
services.AddAuthentication().AddOpenIdConnect(openIdOptions =>
{
openIdOptions.ResponseType = OpenIdConnectResponseType.CodeIdToken;
openIdOptions.Authority = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}{1}", "common", "/v2.0");
openIdOptions.ClientId = Configuration["MicrosoftAuth:ClientId"];
openIdOptions.ClientSecret = Configuration["MicrosoftAuth:ClientSecret"];
openIdOptions.SaveTokens = true;
openIdOptions.TokenValidationParameters = new TokenValidationParameters{
ValidateIssuer = false
};
var scopes = Configuration["MicrosoftAuth:Scopes"].Split(' ');
foreach (string scope in scopes){
openIdOptions.Scope.Add(scope);
}
openIdOptions.Events = new OpenIdConnectEvents{
OnAuthorizationCodeReceived = async (context) =>
{
var userId = context.Principal.Claims.First(item => item.Type == ObjectIdentifierType).Value;
IMemoryCache memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();
ConfidentialClientApplication cca =
new ConfidentialClientApplication( Configuration["MicrosoftAuth:ClientId"],
String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}{1}{2}", "common", "/v2.0", "/adminconsent"),
Configuration["MicrosoftAuth:RedirectUri"]+ "signin-oidc",
new Microsoft.Identity.Client.ClientCredential(Configuration["MicrosoftAuth:ClientSecret"]),
new SessionTokenCache(userId,memoryCache).GetCacheInstance(),
null);
var code = context.ProtocolMessage.Code;
var result = await cca.AcquireTokenByAuthorizationCodeAsync(code,new string[]{"User.Read.All", "Calendars.ReadWrite"});
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
},
};
});
My app is registered in the Microsoft Application Registration Portal, and I do get a token when I request for it, so I'm not sure what could possibly be causing the problem.
Same issue with the previews thread. There are two kinds of token issued by Azure AD, delegate for use or app. The token you were acquire is using the client credentials flow which is delegate for app. There is no me context when you request using this kind of token(refer Get access on behalf of a user and Get access without a user for the difference).
To integrate Microsoft Graph with web app and delegate the user to call the Microsoft Graph, you need to use the code grant flow(OnAuthorizationCodeReceived event) as you config in the startup.cs file.