I am trying to set up integration tests in a project, where the application is secured by AzureAd (Microsoft.Identity.Web) and 2FA. Usually the user would log in to the application using a SPA, and would be greeted by the login popup or redirect. While testing controller endpoints, there is no SPA and we are not using the On-behalf-of flow, so I thought I could simply obtain a token the usual way for testing purposes:
[HttpPost]
public async Task<IActionResult> GetTokenAsync([FromBody] TokenRequest model)
{
_client.BaseAddress = new Uri("https://login.microsoftonline.com/");
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
/* ... */
}
var response = await _client.PostAsync($"{_options.TenantId}/oauth2/v2.0/token", content);
var responseContent = await response.Content.ReadAsStringAsync();
var token = JsonConvert.DeserializeObject<dynamic>(responseContent);
return Ok(new TokenResponse { Token = token.id_token });
}
However, due to 2FA being enabled, I am receiving the following error:
{{
"error": "invalid_grant",
"error_description": "AADSTS50158: External security challenge not satisfied. User will be redirected to another page or authentication provider to satisfy additional authentication
"error_codes": [
50158
],
"timestamp": "2020-12-04 12:41:10Z",
"trace_id": "xxxxxxxxxxxxxx",
"correlation_id": "xxxxxxxxxxxxxxxxxxx",
"error_uri": "https://login.microsoftonline.com/error?code=50158",
"suberror": "basic_action",
"claims": "{ /* ... */}"
}}
A more human-readable-friendly version: https://login.microsoftonline.com/error?code=50158
Here is the relevant part for the authentication configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddMicrosoftIdentityWebApi(this.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(this.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
/* ... */
}
Is there a way to satisfy the 2FA requirement during testing or circumvent it, while keeping the basic token mechanism intact?
Update
To clear up any possible confusion: I'm targeting automated tests, without human intervention. I know, that I can use a mock authentication scheme with a custom WebApplicationFactory, but I'd go for a less fake approach if possible.
Related
.NET5 has a ton of middleware and infrastructure for authentication. You can include a handful of NuGet packages, wire some configuration in Startup.cs, and your application understands cookies and sessions. All the examples I'm finding online for JWT-based authentication has me manually instantiating a JwtSecurityDescriptor and setting properties inside of it. This really feels like something that should be part of the SignInManager.PasswordSignInAsync flow. Examples would be:
https://www.faciletechnolab.com/Blog/2021/4/5/how-to-implement-jwt-token-authentication-in-aspnet-core-50-web-api-using-jwt
https://weblog.west-wind.com/posts/2021/Mar/09/Role-based-JWT-Tokens-in-ASPNET-Core
http://www.binaryintellect.net/articles/1fdc8b3f-06a1-4f36-8c0b-7852bf850f52.aspx
Am I missing something? Is this a case where the online examples are skipping several steps in favor of "simplifying" the demonstration? Every one of the preceding examples creates the JWT manually but delegates a lot of the interpretation of it to the middleware.
To clarify, I am not looking for instruction on how to validate passwords (I do that with IPasswordHasher) or locate users (I do that with IUserPasswordStore). I am specifically wondering if there is something I am missing to cause ASP.NET to handle the JWT generation workflow for me. I can't tell if the online examples are eliding a lot of functionality "for demonstration" or not (similar to how a lot of guides online put DB calls directly inside controllers for "simplicity").
Generally, when we using JWT authentication, the workflow as below:
Client sends a request (which contains the user information, such as: name and password) to server for token
Server receives the user information and checking for authorization. If validated success, server generates a JWT token.
Client receives the token and stores it somewhere locally.
Client sends the token in the future requests.
Server gets the token from request header, computes Hash again by using a) Header from token b) payload from token c) secret key which server already has.
If ("newly computed hash" = "hash came in token"), token is valid otherwise it is tempered or not valid
You could refer to the following steps to use JWT Authentication In ASP.NET Core.
Configure the authentication schema with JWT bearer options.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddControllersWithViews();
}
In this example, I have stored these values in appsettings.json file.
{
"Jwt": {
"Key": "ThisismySecretKey",
"Issuer": "Test.com"
}
}
Call the app.UseAuthentication() method in the Configure method of startup class.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Generate JSON Web Token
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Test.Models;
namespace Test.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private IConfiguration _config;
public LoginController(IConfiguration config)
{
_config = config;
}
[AllowAnonymous]
[HttpPost]
public IActionResult Login([FromBody] UserModel login)
{
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { token = tokenString });
}
return response;
}
private string GenerateJSONWebToken(UserModel userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.Username),
new Claim(JwtRegisteredClaimNames.Email, userInfo.EmailAddress),
new Claim("DateOfJoing", userInfo.DateOfJoing.ToString("yyyy-MM-dd")),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private UserModel AuthenticateUser(UserModel login)
{
UserModel user = null;
//Validate the User Credentials
//Demo Purpose, I have Passed HardCoded User Information
if (login.Username == "Jignesh")
{
user = new UserModel { Username = "Jignesh Trivedi", EmailAddress = "test.btest#gmail.com" };
}
return user;
}
}
}
Then, if you request the "API/login" method to generate the token, you have to passed the following JSON in the request body.
{"username": "Jignesh", "password": "password"}
Then, after getting the JWT token, you could add the "Authorization" property in the request header when you access other API controller(with the [Authorize] attribute).
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKaWduZXNoIFRyaXZlZGkiLCJlbWFpbCI6InRlc3QuYnRlc3RAZ21haWwuY29tIiwiRGF0ZU9mSm9pbmciOiIwMDAxLTAxLTAxIiwianRpIjoiYzJkNTZjNzQtZTc3Yy00ZmUxLTgyYzAtMzlhYjhmNzFmYzUzIiwiZXhwIjoxNTMyMzU2NjY5LCJpc3MiOiJUZXN0LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.8hwQ3H9V8mdNYrFZSjbCpWSyR1CNyDYHcGf6GqqCGnY
More detail information, refer the followng links:
JWT Authentication In ASP.NET Core
JWT Validation on separate web Api
I have a Xamarin.Forms application that I'm using to connect to an App Service backend, and I'm attempting to authenticate using Auzre B2C JWT tokens.
Through various tutorials I have managed to get B2C setup using microsoft accounts, and I am able to create users, change passwords, and generate access tokens.
My next step was to add the [Authorize] attribute to my controller and attempt to pass that token to my app service and authorize users, but no matter what I try I get a 401 Unauthorized response from my service.
I'm adding the JWT token to the Authorization header of my HttpClient, and it's getting to the service.
I can paste my token into https://jwt.ms/, and it correctly tells me what's in my token.
I've implemented this code in an attempt to figure out what's wrong.
ConfigureServices in startup.cs looks like this:
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
options.Audience = Configuration["Authentication:AzureAd:ClientId"];
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = AuthenticationFailed
};
options.Authority = $"https://{tenant name}.b2clogin.com/{tenant id}/{Configuration["Authentication:AzureAd:Policy"]}";
options.Events = new JwtBearerEvents {
OnAuthenticationFailed = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
message += "From OnAuthenticationFailed:\n";
message += FlattenException(ctx.Exception);
return Task.CompletedTask;
},
OnChallenge = ctx =>
{
message += "From OnChallenge:\n";
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
ctx.Response.ContentType = "text/plain";
return ctx.Response.WriteAsync(message);
},
OnMessageReceived = ctx =>
{
message = "From OnMessageReceived:\n";
ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken);
if (BearerToken.Count == 0)
BearerToken = "no Bearer token sent\n";
message += "Authorization Header sent: " + BearerToken + "\n";
return Task.CompletedTask;
},
OnTokenValidated = ctx =>
{
Debug.WriteLine("token: " + ctx.SecurityToken.ToString());
return Task.CompletedTask;
}
};
});
services.AddMvc();
}
Configure looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
} else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
And I've also added this call to AuthenticationFailed, so I'll know if my authentication is working or not:
Task AuthenticationFailed(AuthenticationFailedContext arg) {
Console.WriteLine(arg.Exception.Message);
return Task.FromResult(0);
}
With my current setup I'm getting a 401 error from the server, and that's right after it hits the OnChallenge event wired up in Startup.cs. According to the link above, that's what gets called right before it returns a 401 to the user, so it seems like the service is receiving the proper token, and authenticating, but maybe I don't have the correct rights set up?
I'm not sure where to go from here, but any guidance would be appreciated.
Edit:
As mentioned in a comment below, I was able to curl my website using the access token generated after logging in through my app like this:
curl https://mywebsite.azurewebsites.net/api/Values -i --header "Authorization: Bearer [TOKEN]"
And that seems to work with no issue, so it seems like it's something with how I'm making a call to the controller through my app, not the authentication itself.
Edit 2 (solution):
So, as per Edit 1, I was correct in that it was just how I was adding the token to the authorization header. It wasn't my brightest moment, but I wasn't calling .Value on the claim that contained my Access Token. I was only calling .ToString() on the claim itself, so the "token" was actually the entire claim text "Access Token: ". I didn't think much of it at the time when I was debugging my service, because I didn't realize it shouldn't have that text there.
Once I corrected that issue the service started working as expected.
So, in the end, I guess it was all working as expected. I was, in fact, not sending the expected token, so I was ... unauthorized.
As requested the line of code that I had to change was this:
So, this won't be 100% applicable to most because I'm using a business library called CSLA, but the idea is the same regardless.
After my b2c call returns the token I store it in the ApplicationContext.User.Identity that's built into the CSLA library. That allows me to get the access token claim later. The important part to take away from this is that I'm storing the token some place that I can access it later when I want to add it to the authorization header.
Later, when I'm making the call with my httpclient I need to get that token, so originally, I was doing this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").ToString());
This isn't correct. This was sending the "token" as with value "Access Token: [token value]. Essentially, it was adding the words "Access Token" to the token I needed to authenticate, and that was failing, because the words "Access Token" are not actually supposed to be part of the token you use to authenticate.
After I changed my call to this:
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ((ClaimsIdentity)ApplicationContext.User.Identity).Claims.FirstOrDefault(c => c.Type == "AccessToken").Value);
It started getting only the token value, and when that was added to the authorization header, it worked just fine.
Edit 2 explains the answer to my problem.
I wasn't adding the token correctly to the authorization header, so the service wasn't able to authenticate the token, or rather, it saw the token as invalid.
I've made an Azure bot application using the BotFramework v4 and used the WebChat control as an interface. I noticed that the bot server's dotnetcore app had a wwwroot folder with a placeholder HTML page in it, so thought it might be expedient to host the webchat client there. But now seems counter-intuitive that my webchat client is using DirectLine to send activities back to the same back-end that served it.
I had chosen the webchat client because I need to customise the appearance of the client. I also need the MVC app that serves the bot client to include Azure Active Directory B2C authentication (which it does). Users should see the webchat client before and after authentication but the bot back-end (handling the activities) needs to know whether the user is logged in and modify its behaviour accordingly (and I am struggling to achieve that part with DirectLine).
So my first question (ever on StackOverflow) is: With the Bot back-end and the webchat client front-end being hosted in the same, single Azure web app, is it necessary to use DirectLine, or is there a simpler way of doing this?
Relevant code in my Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
app.UseStaticFiles(); // to allow serving up the JS, CSS, etc., files.
app.UseBotFramework(); // to add middleware to route webchat activity to the bot back-end code
app.UseSession(); // to enable session state
app.UseAuthentication(); // to enable authentication (in this case AAD B2C)
app.UseMvcWithDefaultRoute(); // to add MVC middleware with default route
}
Also in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// standard code to add HttpContextAssessor, BotServices, BotConfigs and memory storage singletons ommitted for brevity ...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
services.AddMvc();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.CookieHttpOnly = true;
});
// Create and add conversation state.
var conversationState = new ConversationState(dataStore);
services.AddSingleton(conversationState);
var userState = new UserState(dataStore);
services.AddSingleton(userState);
services.AddBot<MyBot>(options =>
{
options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
options.ChannelProvider = new ConfigurationChannelProvider(Configuration);
// Catches any errors that occur during a conversation turn and logs them to currently
// configured ILogger.
ILogger logger = _loggerFactory.CreateLogger<RucheBot>();
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};
});
}
My controller's Index method:
public async Task<ActionResult> Index()
{
string userId;
if (User.Identity.IsAuthenticated)
{
string aadb2cUserId = User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
Users.EnsureAccountExists(aadb2cUserId); // ensure account with given AAD identifier is know locally (by creating it if not)
userId = $"ia_{aadb2cUserId}";
}
else
{
userId = $"na_{Guid.NewGuid()}";
}
HttpClient client = new HttpClient();
string directLineUrl = $"https://directline.botframework.com/v3/directline/tokens/generate";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, directLineUrl);
// TODO: put this in the config somewhere
var secret = "<the secret code from my bot's DirectLine channel config in the Azure portal>";
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);
string jsonUser = JsonConvert.SerializeObject(new { User = new { Id = userId } });
request.Content = new StringContent(jsonUser, Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
string token = string.Empty;
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
}
var config = new ChatConfig()
{
Token = token,
UserId = userId,
};
return View(config);
}
And finally the code in the associated view:
<div id="webchat"></div>
<script type="text/javascript">
...
/// Called asynchronously during the page load
function renderWebChat( withSound )
{
var webchatOptions =
{
directLine: window.WebChat.createDirectLine( { secret: '#Model.Token'} ),
userID: '#Model.UserId'
};
if ( withSound )
{
webchatOptions.webSpeechPonyfillFactory = window.WebChat.createBrowserWebSpeechPonyfillFactory();
}
window.WebChat.renderWebChat( webchatOptions, document.getElementById( 'webchat' ) );
document.querySelector( '#webchat > *' ).focus();
}
</script>
I'm going to disagree with Nicolas R a little bit. When it comes to directly accessing your bot, you might like to have a look at this: https://www.npmjs.com/package/offline-directline
There's also the option of hosting a bot in the browser, which I think may facilitate the sort of direct communication you're looking for.
Long question, but the answer will be a lot shorter!
So my first question (ever on StackOverflow) is: With the Bot back-end
and the webchat client front-end being hosted in the same, single
Azure web app, is it necessary to use DirectLine, or is there a
simpler way of doing this?
Yes, it is necessary. In fact, all the channels types are using the Bot Connector to communicate with your backend (your bot code), there is no direct access possible. There are a lot of reasons for that, one is for example the billing!
I have the following application at GitHub and have deployed it to https://stratml.services on an Azure App Service with Authentication defined as Microsoft Account with anymous requests requiring a Microsoft Account sign in. In "prod" this challenge occurs, however https://stratml.services/Home/IdentityName returns no content.
I have been following this and this however I do not want to use EntityFramework and from the latter's description it seems to imply if I configure my Authentication scheme correctly I do not have to.
This following code is in my Start class:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
microsoftOptions.CallbackPath = new PathString("/.auth/login/microsoftaccount/callback");
});
Update: Thanks to the first answer I was able to get, it now authorizes to Microsoft and attempts to feedback to my application however I receive the following error:
InvalidOperationException: No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Cookies
Please visit https://stratml.services/Home/IdentityName and the GitHub has been updated.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
I have checked this issue on my side, based on my test, you could confgure your settings as follows:
Under the ConfigureServices method, add the cookie and MSA authentication services.
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
})
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
Under the Configure method, add app.UseAuthentication().
TEST:
[Authorize]
public IActionResult Index()
{
return Content(this.User.Identity.Name);
}
When I checking your online website, I found that you are using the Authentication and authorization in Azure App Service and Authenticate with Microsoft account.
AFAIK, when using the app service authentication, the claims could not be attached to current user, you could retrieve the identity name via Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"] or you could follow this similar issue to manually attach all claims for current user.
In general, you could either manually enable authentication middle-ware in your application or just leverage the app service authentication provided by Azure without changing your code for enabling authentication. Moreover, you could Remote debugging web apps to troubleshoot with your application.
UPDATE:
For enable the MSA authentication in my code and test it when deployed to azure, I disabled the App Service Authentication, then deployed my application to azure web app. I opened a new incognito window and found that my web app could work as expected.
If you want to simulate the MSA login locally and use Easy Auth when deployed to azure, I assumed that you could set a setting value in appsettings.json and manually add the authentication middle-ware for dev and override the setting on azure, details you could follow here. And you could use the same application Id and configure the following redirect urls:
https://stratml.services/.auth/login/microsoftaccount/callback //for easy auth
https://localhost:44337/signin-microsoft //manually MSA authentication for dev locally
Moreover, you could follow this issue to manually attach all claims for current user. Then you could retrieve the user claims in the same way for the manually MSA authentication and Easy Auth.
If you are using App Service Authentication (EasyAuth), according to Microsoft documentation page:
App Service passes some user information to your application by using special headers. External requests prohibit these headers and will only be present if set by App Service Authentication / Authorization. Some example headers include:
X-MS-CLIENT-PRINCIPAL-NAME
X-MS-CLIENT-PRINCIPAL-ID
X-MS-TOKEN-FACEBOOK-ACCESS-TOKEN
X-MS-TOKEN-FACEBOOK-EXPIRES-ON
Code that is written in any language or framework can get the information that it needs from these headers. For ASP.NET 4.6 apps, the ClaimsPrincipal is automatically set with the appropriate values.
So basically, if you are using ASP.NET Core 2.0, you need to set the ClaimPrincipal manually. What you need to use in order to fetch this headers and set the ClaimsPrincipal is AuthenticationHandler
public class AppServiceAuthenticationOptions : AuthenticationSchemeOptions
{
public AppServiceAuthenticationOptions()
{
}
}
internal class AppServiceAuthenticationHandler : AuthenticationHandler<AppServiceAuthenticationOptions>
{
public AppServiceAuthenticationHandler(
IOptionsMonitor<AppServiceAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult(FetchAuthDetailsFromHeaders());
}
private AuthenticateResult FetchAuthDetailsFromHeaders()
{
Logger.LogInformation("starting authentication handler for app service authentication");
if (Context.User == null || Context.User.Identity == null || Context.User.Identity.IsAuthenticated == false)
{
Logger.LogDebug("identity not found, attempting to fetch from the request headers");
if (Context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
var headerId = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var headerName = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
var claims = new Claim[] {
new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", headerId),
new Claim("name", headerName)
};
Logger.LogDebug($"Populating claims with id: {headerId} | name: {headerName}");
var identity = new GenericIdentity(headerName);
identity.AddClaims(claims);
var principal = new GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal,
new AuthenticationProperties(),
Scheme.Name);
Context.User = principal;
return AuthenticateResult.Success(ticket);
}
else
{
return AuthenticateResult.Fail("Could not found the X-MS-CLIENT-PRINCIPAL-ID key in the headers");
}
}
Logger.LogInformation("identity already set, skipping middleware");
return AuthenticateResult.NoResult();
}
}
You can then write an extension method for the middleware
public static class AppServiceAuthExtensions
{
public static AuthenticationBuilder AddAppServiceAuthentication(this AuthenticationBuilder builder, Action<AppServiceAuthenticationOptions> configureOptions)
{
return builder.AddScheme<AppServiceAuthenticationOptions, AppServiceAuthenticationHandler>("AppServiceAuth", "Azure App Service EasyAuth", configureOptions);
}
}
And add app.UseAuthentication(); in the Configure() method and put following in the ConfigureServices() method of your startup class.
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "AppServiceAuth";
options.DefaultChallengeScheme = "AppServiceAuth";
})
.AddAppServiceAuthentication(o => { });
If you need full claims details, you can retrieve it on the AuthenticationHandler by making request to /.auth/me and use the same cookies that you've received on the request.
I am having a web api using Identity server 4. I dont know where to start writing Integration test. I am having a Login Controller taking in Username and password which is used for ResourceOwnerPassword Grant type. Below is my code.
Controller.
[Route("Authentication/Login")]
public async Task<IActionResult> WebApiLogin(string username, string password)
{
var accessToken = await UserAccessToken.GenerateToken(username, password);
return new JsonResult(accessToken);
}
Class to generate token
public async Task<string> GenerateToken(string username, string password)
{
//discover endpoint for metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
//request token
var clientToken = new TokenClient(disco.TokenEndpoint, "client", "secret");
//var tokenResponse = await clientToken.RequestClientCredentialsAsync("Payment");
var tokenResponse = await clientToken.RequestResourceOwnerPasswordAsync(username, password, "IntegrapayAPI");
if (tokenResponse.IsError)
{
//Error tokenResponse.Error
return tokenResponse.Error;
}
return tokenResponse.Json.ToString();
}
IdentityServer Project startup class.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
//.AddTestUsers(Config.GetUsers());
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
// Add framework services.
//services.AddMvc();
}
You can take a look at this answer: https://stackoverflow.com/a/39409789/147041
Disclaimer: my own question, my answer.
It contains a link to a GitHub repo where integration tests are set up against an API, but it will work for MVC as well of course. The essence is to use an in-memory IdentityServer to act as your token generator and validator.
Besides that, you should not mix your API with IdentityServer. Use IdentityServer to generate your tokens, then your API will validate those tokens agains the identityserver.
There are a lot of good samples out there to get you started.