About OAuth.
Frontend SPA react
MVC OAuth backend, signs user's into 3rd party providers, works nicely, return token.
from my SPA I can do window.open and redirect the user to a sign-in page, NB: has to be a new window as xframeoptions is set to deny.
How do I return the token & correlate with SPA, as they are in separate windows/sessions?
options I'm looking at
content security policy - set the caller's domain
set same site cookie
Using aspnet-contrib/AspNet.Security.OAuth.Providers
Samples
Startup.cs
public class Startup
{
private const string policyName = "Cors";
public Startup(IConfiguration configuration, IHostEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
}
public IConfiguration Configuration { get; }
private IHostEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddCors(opt =>
{
opt.AddPolicy(name: policyName, builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyOrigin()
.AllowAnyMethod();
});
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddGitHub(options =>
{
options.ClientId = Configuration["GitHub:ClientId"];
options.ClientSecret = Configuration["GitHub:ClientSecret"];
options.Scope.Add("user:email");
options.Scope.Add("read:org");
options.Scope.Add("workflow");
options.SaveTokens=true;
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
if (HostingEnvironment.IsDevelopment())
{
// IdentityModelEventSource.ShowPII = true;
}
// Required to serve files with no extension in the .well-known folder
//var options = new StaticFileOptions()
//{
// ServeUnknownFileTypes = true,
//};
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
});
app.UseCors(policyName);
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
});
Authentication Controller
public class AuthenticationController : Controller
{
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
// Note: the "provider" parameter corresponds to the external
// authentication provider choosen by the user agent.
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
Home Controller
public class HomeController : Controller
{
public async Task<IActionResult> IndexAsync()
{
var accessToken = await HttpContext.GetTokenAsync("GitHub", "access_token");
var refreshToken = await HttpContext.GetTokenAsync("GitHub", "refresh_token");
return View();
}
}
Home Page (Index.cshtml)
<div class="jumbotron">
#if (User?.Identity?.IsAuthenticated ?? false)
{
<h1>Welcome, #User.Identity.Name</h1>
<p>
#foreach (var claim in Context.User.Claims)
{
<div><code>#claim.Type</code>: <strong>#claim.Value</strong></div>
}
</p>
<a class="btn btn-lg btn-danger" href="/signout?returnUrl=%2F">Sign out</a>
}
else
{
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" href="/signin?returnUrl=%2F">Sign in</a>
}
</div>
Thanks for looking
It seems (correct me if I'm mistaken) that the main issue is launching the window for github auth and sending that token back to your site which is a different window (i.e. the parent window of the popup).
One option is to set up your auth request's redirect_uri parameter so that the 3rd party identity provider redirects back to a URL on your site (within the popup window), i.e. your ~/signin endpoint. This allows your server-side to grab hold of the tokens and do things like create a session, store a cookie, etc. Cookies set within the popup window will be available to your site in the original window (assuming it's the same domain) once the parent window has refreshed.
Next, once the popup window has been redirected back to your ~/signin endpoint and you've created the session or stored a cookie, etc, you may wish to close that popup and refresh the parent window so that it recognises the cookie / new session. You can do this by returning a page from the ~/signin request (still inside the popup) which includes the following JavaScript:
window.opener.document.location.reload();
// alternatively send the user to an authenticated homepage:
window.opener.document.location.href = '/signed-in-user-homepage';
// and then close the popup
window.close();
I don't think it's your goal but for completeness, if you wish to perform OAuth authentication/authorisation from the SPA itself, so the server side doesn't get the tokens, you may wish to let a Javascript library like oidc-client do the heavy lifting. This launches its own window to perform authentication and hands back the token to the calling SPA itself. These tokens are not visible to the server side.
One resource which really helped me figure this out was the IdentityServer4 quickstart for JavaScript clients, worth stepping through if this is your use case:
OpenID Connect auth from JavaScript clients quickstart
Sample code for the above quickstart
Once you've got a token on the client side (i.e. in JavaScript), you could pass it to the server side to use to make requests on behalf of the user but this isn't good practice as it impersonation rather than properly delegated authorisation, and you'd need to work out what to do when the access token expires (passing the refresh token to the server side effectively allows your server to impersonate the user indefinitely).
My preference is to allow the server-side to perform an auth-code authorisation flow using AspNet.Security.OAuth.Providers, receive the auth code to the ~/signin page, perform the back channel request to get the tokens, use the identity information to create my own profile for the user and perform requests on behalf of the user using authorisation delegated to my service.
Let me know if any of this needs more explaining but hope it's useful as-is.
Related
I have a problem with my asp .net webapp Im developing right now. I added the possibilty to login with a microsoft account. But I have the problem, that it doesn't take my custom redirect url. In my Azure Ad application the redirect url is configured to /Profile, but the request redirect url it gets from my login button is everytime /signin-microsoft
My Authentication in my startup.cs looks like this
services.AddAuthentication("Cookies")
.AddCookie(opt =>
{
opt.Cookie.Name = "AuthCookie";
})
.AddMicrosoftAccount(opt => {
opt.SignInScheme = "Cookies";
opt.AuthorizationEndpoint = _configuration["AzureAd:AuthorizationEndpoint"];
opt.TokenEndpoint = _configuration["AzureAd:TokenEndpoint"];
opt.ClientId = _configuration["AzureAd:ClientId"];
opt.ClientSecret = _configuration["AzureAd:ClientSecret"];
});
I dont know if this is important but my used options in applicationsettings are:
"AzureAd": {
"ClientId": "<clientId>",
"ClientSecret": "<clientSecret>",
"AuthorizationEndpoint":"https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/authorize",
"TokenEndpoint": "https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token"
}
Ofc i entered the correct IDs in this
My Login Controller:
[HttpGet("microsoft")]
public async Task<ActionResult>Login(string RedirectUri)
{
AuthenticationProperties props = new AuthenticationProperties
{
RedirectUri = RedirectUri
};
return Challenge(props, MicrosoftAccountDefaults.AuthenticationScheme);
}
And my login button:
<NotAuthorized>
<li class="nav-item">
<a class="nav-link" href="Login/microsoft?RedirectUri=/Profile">
Login
</a>
</li>
</NotAuthorized>
As you can see the Redirect paramenter should be /profile and I set it also in the Authentication Properties to this value, but when i click the login button the url is always:
https://login.microsoftonline.com/%5C\<tenantId>/oauth2/v2.0/authorize...&redirect_uri=https%3A%2F%2Flocalhost%3A5000%2Fsignin-microsoft&...
So why doesnt it take /Profile as redirect Url?
It is expected that the redirect uri parameter is localhost:5000/Profile
I tried to reproduce the same in my environment.
Make sure to use the following in the order in startup.cs
app.UseAuthentication();
//app.UseIdentityServer();
app.UseAuthorization();
When I configure it the other way:
app.UseAuthorization();
app.UseAuthentication();
//app.UseIdentityServer();
I was continuously redirected to the Microsoft login page even after signing in.
Make sure the redirect must have the below format:
pattern: "{controller=Home}/{action=Index}/");
i.e;
RedirectUri = "/home/about"
Or
RedirectUri = "/home/profile"
Below is the result ,if action part is directly requested in browser.
Always recheck and set the redirects in the portal such that it matches the redirects in your code.
In startup.cs , include the scopes required for the operations.
public void ConfigureServices(IServiceCollection services)
{
…..
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
})
.AddMicrosoftAccount(microsoftOptions =>
{
o.ClientId = Configuration["microsoftaccount:clientid"];
o.ClientSecret = Configuration["microsoftaccount:clientsecret"];
o.SaveTokens = true;
o.Scope.Add("offline_access profile ");
o.CallbackPath ("/signin-oidc"); //microsoft-signin
o.Events = new OAuthEvents()
{
OnRemoteFailure = HandleOnRemoteFailure
};
})
…..
}
The scopes for delegated Api permissions required must be granted admin consent .
Then the user is authenticated successfully
If the user is authenticated , then only it is redirected to the specified redirect page/uri.
Reference : How to redirect to particular page after Azure AD Login? - Stack Overflow
I have a web application that uses asp.net core (3.1) backend and angular front end (8.2.11). It uses asp.net Identity framework for user authentication. It store authentication token in local storage to be used as authentication header in requests. Everything is working in the sense controller endpoints are only accessible when a user is logged in, if logged out, typing an endpoint directly into browser would be rejected.
I am still not certain if such a setup prevent the Cross-Site Request Forgery (XSRF/CSRF) attacks. I know using cookie to store authentication token is susceptible to CSRF and I tried a little bit with the [ValidateAntiForgeryToken] attribute on some endpoint, it broke those endpoints of course. I know in Razor page, a form is automatically injected with anti-forgery token. So, do I need to set it up in my angular front-end? and if yes, how? (I've searched a bit on the web and the instructions are all over the place, quite messy with no clear consensus).
Step 1
Add a middleware to your middleware pipeline that generates an AntiforgeryToken, and embeds the token in a non-http-only cookie that's attached to the response:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAntiforgery(options => {
options.HeaderName = "X-XSRF-TOKEN";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
...;
app.Use((context, next) => {
var tokens = antiforgery.GetAndStoreTokens(httpContext);
httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
});
}
}
I created a little package for this that contains this middleware.
Step 2
Configure your angular app to read the value of the non-http-only cookie (XSRF-TOKEN) through javascript, and pass this value as a X-XSRF-TOKEN header for requests sent by the HttpClient:
#NgModule({
declarations: [...],
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
...
],
providers: [...],
bootstrap: [AppComponent]
})
export class AppModule { }
Step 3
Now you can decorate your controller methods with the [ValidateAntiforgeryToken] attribute:
[ApiController]
[Route("web/v1/[controller]")]
public class PersonController : Controller
{
private IPersonService personService;
public PersonController(IPersonService personService)
{
this.personService = personService;
}
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<ActionResult<Person>> Post([FromBody] Person person)
{
var new_person = await personService.InsertPerson(person);
return Ok(new_person);
}
}
Step 4
Make sure the requests you're sending have the following type of url as stated here:
/my/url
//example.com/my/url
Wrong url:
https://example.com/my/url
Note
I use Identity Cookie authentication:
services.AddAuthentication(/* No default authentication scheme here*/)
Since the ASP.NET Core Authentication middleware only looks after the XSRF-TOKEN header, and not the X-XSRF-TOKEN cookie, you're no longer susceptible to Cross-site request forgery.
Spoiler
You will notice that right after signing in/out, the first webrequest that's being sent will still be blocked by XSRF protection. This is because the Identity does not change during the lifetime of the webrequest. So when sending the Login webrequest, the response will attach a cookie with a csrf-token. But this token is still generated with the identity from when you weren't signed in yet.
The same counts for sending the Logout webrequest, the response will contain a cookie with a csrf-token as if you're still signed in.
To solve this, you have to simply send another webrequest that does literally nothing, every time you've signed in/out. During this request you'll once again have the correct Identity in order to generate the csrf-token.
logoutClicked() {
this.accountService.logout().then(() => {
this.accountService.csrfRefresh().then(() => {
this.activeUser = null;
});
}).catch((error) => {
console.error('Could not logout', error);
});
}
Same for login
this.accountService.login(this.email, this.password).then((loginResult) => {
this.accountService.csrfRefresh().then(() => {
switch (loginResult.status) {
case LoginStatus.success:
this.router.navigateByUrl(this.returnUrl);
this.loginComplete.next(loginResult.user);
break;
default:
this.loginResult = loginResult;
break;
}
});
});
The contents of the csrfRefresh method
public csrfRefresh() {
return this.httpClient.post(`${this.baseUrl}/web/Account/csrf-refresh`, {}).toPromise();
}
Server-side
[HttpPost("csrf-refresh")]
public async Task<ActionResult> RefreshCsrfToken()
{
// Just an empty method that returns a new cookie with a new CSRF token.
// Call this method when the user has signed in/out.
await Task.Delay(5);
return Ok();
}
This is where I login the user in my own app
Angular provides built-in enabled by default anti CSRF/XSRF protection.
Angular's HttpClient has built-in support for the client-side half of this technique. Read about it more in the HttpClient guide
Note that the CSRF/XSRF protection is enabled by default on the HttpClient but only works if the backend sets a cookie named XSRF-TOKEN with a random value when the user authenticates.
I am connecting to google android managment API sucessfully and returning data as expected when ussing swagger. The example I followed is Google API Call .net core
The issue I am having is when I call my middle ware API from my front end I get a cors error:
Access to XMLHttpRequest at
'https://accounts.google.com/o/oauth2/v2/auth?CLINET_ID_REMOVED'
(redirected from 'https://localhost:44347/api/Device/Get') from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Once I login by clicking the link the front end works as expected. How can I get the login to popup if the user is not yet logged in?
What am I missing in this implamentation? The example at Google.Apis.Auth.AspNetCore3.IntegrationTests is for MVC (which I am not using) and contains some generic login pages, do I need to implament that too?
StartUp.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Add Cors
services.AddCors();
//services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
//{
// builder.AllowAnyOrigin()
// .AllowAnyMethod()
// .AllowAnyHeader();
//}));
// This configures Google.Apis.Auth.AspNetCore3 for use in this app.
services
.AddAuthentication(o =>
{
// This forces challenge results to be handled by Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This forces forbid results to be handled by Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// Default scheme that will handle everything else.
// Once a user is authenticated, the OAuth2 token info is stored in cookies.
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
options.ClientId = authClientID;
options.ClientSecret = authClientSecret;
});
//services.AddDataProtection();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AndroidManagmentAPI", Version = "v1" });
});
}
Get Enterprise Method
// GET: api/<EnterpriseController>
[HttpGet("Get")]
[Authorize]
[GoogleScopedAuthorize(AndroidManagementService.ScopeConstants.Androidmanagement)]
public async Task<Enterprise> Get([FromServices] IGoogleAuthProvider auth)
{
SignupData signupData = JsonConvert.DeserializeObject<SignupData>(GetEnvirmentVarable(Utilities.SignupDetails));
Enterprise enterprise = new Enterprise();
enterprise.Name = signupData.EnterpriseName;
var er = await GetEnterprisesResource(auth);
var result = er.Get(enterprise.Name);
var ent = result.Execute();
return ent;
}
Good day! I am currently creating a website which utilises the Google authentication to enable content personalisation. I have no problem with sign-in and retrieving the logged in user's info, but .NET is not signing the user out completely when I call the SignOutAsync() function, as the user could immediately log in when clicking on the Login button again. Once I clear the browser cache, the user will be redirected to the Google sign-in page when clicking on the Login button.
The services configuration at Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Configure authentication service
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Google";
})
.AddCookie("Cookies")
.AddGoogle("Google", options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IRecommender, OntologyRecommender>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
The middleware configuration at Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Login action at the UserController.cs:
public IActionResult Login()
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" });
}
Logout action at the UserController.cs:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
HttpContext.Response.Cookies.Delete(".AspNetCore.Cookies");
return RedirectToAction("Index", "Home");
}
I am new to the ASP.NET Core authentication area, so I would appreciate if anyone could just assist me on this matter, thank you!
you need to loop thru the application cookies - here is a sample code snippet:
if (HttpContext.Request.Cookies[".MyCookie"] != null)
{
var siteCookies = HttpContext.Request.Cookies.Where(c => c.Key.StartsWith(".MyCookie"));
foreach (var cookie in siteCookies)
{
Response.Cookies.Delete(cookie.Key);
}
}
You can redirect user to Google's logout endpoint to logout :
await HttpContext.SignOutAsync();
HttpContext.Response.Cookies.Delete(".AspNetCore.Cookies");
return Redirect("https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=https://localhost:44310");
Replace "https://localhost:44310" with your own website url . After that , when user click login again , user will be redirected to the Google sign-in page .
This is not a .NET issue but rather how Google works. It happens simply because there is only a single account logged into Google Accounts, which gets defaulted by the process. Either log out of your google account from google accounts or login with other accounts as well.
Be sure to do this from the browser you're using for development though.
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.