Is it possible to block all calls to my web api that are not coming from the web-site itself?
I mean if my MVC app runs at : http://www.domain.com and the web api at http://www.domain.com/api/service, I want the web api to accept calls only from current application only. No external calls allowed.
I will guess maybe a message handler will be the best in this case?
Create a Controller for error page and catch all garbage requests like this:
config.Routes.MapHttpRoute("block", "{*something}", new { controller = "Error", action = "Get" });
You should implement token authorization using a delegating handler.
public class AuthorizationHeaderHandler : DelegatingHandler
{
public AuthorizationHeaderHandler(HttpConfiguration httpConfiguration)
{
//set the inner handler
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
IEnumerable<string> apiKeyHeaderValues = null;
if (request.Headers.TryGetValues("X-ApiKey", out apiKeyHeaderValues))
{
var apiKeyHeaderValue = apiKeyHeaderValues.First();
//based on the api-key get username whose session is stil valid.
var username = //code to get user based on apiKeyHeaderValue;
if (!string.IsNullOrEmpty(username))
{
var usernameClaim = new Claim(ClaimTypes.Name, username);
var identity = new ClaimsIdentity(new[] {usernameClaim}, "ApiKey");
var principal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = principal;
}
}
else
{
//You don't have an ApiKey from the request... can't proceed
var response = request.CreateResponse(HttpStatusCode.Forbidden,
new {Message = "You are not Authorized to access that resource"}); //new HttpResponseMessage(HttpStatusCode.Forbidden);
var tsc = new TaskCompletionSource<HttpResponseMessage>();
tsc.SetResult(response);
return tsc.Task;
}
return base.SendAsync(request, cancellationToken);
}
}
You can then register the handler in the WebApiConfig
public class WebApiConfig
{
public static void Init(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints:null,
handler: new AuthorizationHeaderHandler(GlobalConfiguration.Configuration)
);
}
}
you can then setup your login controller to authorize a user and assign then a token
public class UserController : ApiController
{
public async Task<HttpResponseMessage> Login([FromBody] UserDTO userDTO)
{
// first perform user authentication.
// clear all existing tokens for this authorized user
//create security token and save token of current user
//You can store this in a database and use a repository to create these.
// Tokens can be guids.
// await token creation
return Request.CreateResponse(HttpStatusCode.OK, new {LogingResult = result, token = token});
}
}
Once that user has the token, it can be used for Api requests by adding to the request header. In Angularjs it can take the following form.
'use strict';
(function () {
angular.module('App', ['ngRoute', 'ngCookies']);
//interceptor for server calls
var httpInterceptor = function ($q, $window, $location) {
return function(promise) {
var success = function(response) {
return response;
};
var error = function(response) {
if (response.status === 403) {
$location.url('/login');
}
return $q.reject(response);
};
return promise.then(success, error);
};
}
httpInterceptor['$inject'] = ['$q', '$window', '$location'];
angular.module('App').factory('httpInterceptor', httpInterceptor);
var api = function ($http, $cookies) {
return {
init: function (token) {
$http.defaults.headers.common['X-ApiKey'] = token || $cookies.token;
}
};
}
api['$inject'] = ['$http', '$cookies'];
angular.module('App').factory('api', api);
})();
Yes this is definitely possible. You have to create a custom handler and filter for the RemoteIpAddress found in the request. Here's an implementation using Owin Self-Host:
public class CustomerHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request?.GetClientIpAddress() != "127.0.0.1")
{
return await Task.FromResult(request.CreateResponse(HttpStatusCode.Unauthorized));
}
return await base.SendAsync(request, cancellationToken);
}
}
public static class HttpReqestMessageExtension
{
public static string GetClientIpAddress(this HttpRequestMessage request)
{
if (!request.Properties.ContainsKey("MS_OwinContext")) return null;
dynamic owinContext = request.Properties["MS_OwinContext"];
return owinContext.Request.RemoteIpAddress;
}
}
If you where using ASP.Net then you would use the appropriate key => MS_HttpContext
Now you simply add this to the startup of your Api:
var config = new HttpConfiguration();
config.MessageHandlers.Add(new CustomerHandler());
Related
For some reason my http calls to local api freezes on await when running MAUI application on my android device, but works as intended when running it on windows.
The flow:
public ICommand LoginCommand => _loginCommand ??=
new Command(async () =>
{
if (await _userService.Login(Username, Password))
{
Navigate("//chatrooms");
}
});
public async Task<bool> Login(string username, string password)
{
try
{
var response = await _barigaProApiClient.SignIn(
new SignInRequest
{
Username = username,
Password = password
});
}
catch (Exception)
{
return false;
}
return true;
}
public async Task<SignInResponse> SignIn(SignInRequest request)
{
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/api/User/SignIn", content); // stuck here
response.EnsureSuccessStatusCode();
return JsonConvert.DeserializeObject<SignInResponse>(await response.Content.ReadAsStringAsync());
}
At first I thought it could be a deadlock and unmanaged synchronization context.
Found an article with similar problem, got a suggestion to use .ConfigureAwait(false) to fix that, but it still did not work.
I'm trying to get a dual authentication approach working for my .NET6 website. For the front-end, I'm implementing Azure AD B2C, and for the back-end, Azure AD. Here's my code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options => {
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.Instance = "Instance1";
options.TenantId = "TenantId1";
options.ClientId = "ClientId1";
options.ClientSecret = "ClientSecret1";
options.CallbackPath = "/signin-oidc/aadb2b";
options.Scope.Clear();
options.Scope.Add(OpenIdConnectScope.OpenId);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Scope.Add(OpenIdConnectScope.Email);
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
ValidateIssuer = false
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
}, options => {
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}, "AADB2B.OpenIdConnect", "AADB2B.Cookies");
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options => {
options.Instance = "Instance2";
options.Domain = "Domain2";
options.TenantId = "TenantId2";
options.ClientId = "ClientId2";
options.ClientSecret = "ClientSecret2";
options.SignUpSignInPolicyId = "USUIP";
options.ResetPasswordPolicyId = "RPP";
options.EditProfilePolicyId = "EPP";
options.CallbackPath = "/signin-oidc/aadb2c";
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles"
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
}, options => {
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}, "AADB2C.OpenIdConnect", "AADB2C.Cookies");
// Added as an experiment, doesn't seem to help
services.AddAuthorization(options =>
options.DefaultPolicy =
new AuthorizationPolicyBuilder("AADB2B.OpenIdConnect")
.RequireAuthenticatedUser()
.Build());
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseNotFoundHandler();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseGetaCategories();
app.UseGetaCategoriesFind();
app.UseAnonymousId();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/LoginPath", async ctx => ctx.Response.Redirect("/")).RequireAuthorization(authorizeData: new AuthorizeAttribute { AuthenticationSchemes = "AADB2B.OpenIdConnect" });
endpoints.MapGet("/LogoutPath", async ctx => await MapLogout(ctx));
endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapContent();
});
}
public async Task MapLogout(HttpContext ctx)
{
await ctx.SignOutAsync("AADB2B.OpenIdConnect");
await ctx.SignOutAsync("AADB2B.Cookies");
ctx.Response.Redirect("/");
}
Controller.cs
[HttpGet]
[AllowAnonymous]
public IActionResult ExternalLogin(string scheme, string returnUrl)
{
return Challenge(new AuthenticationProperties { RedirectUri = string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl });
}
Controller is receiving a hyperlink with the QueryString scheme=AADB2B.OpenIdConnect and scheme=AADB2C.OpenIdConnect respectively.
Upon clicking the hyperlinks, the browser is properly redirected to the signin page for AAD B2C or AAD respectively, and then properly redirected back to the website. A breakpoint in the OnSignedIn event properly shows that the Principal.Identity is indeed a ClaimsIdentity, and IsAuthenticated is true. When arriving in the website, the cookies seem to exist:
However, after the page finishes loading, checking IHttpContextAccessor on subsequent pages shows that the HttpContext.User seems to be a brand-new one, and not the one that exists after the above authentication call.
I tried changing to this:
[HttpGet]
[AllowAnonymous]
public IActionResult ExternalLogin(string scheme, string returnUrl)
{
return Challenge(new AuthenticationProperties { RedirectUri = Url.Action("ExternalLoginCallback", new { scheme = scheme, returnUrl = returnUrl }) }, scheme);
}
[Authorize(AuthenticationSchemes = "AADB2B.OpenIdConnect,AADB2C.OpenIdConnect")]
public async Task<ActionResult> ExternalLoginCallback(string scheme, string returnUrl)
{
var authenticate = await HttpContext.AuthenticateAsync(scheme);
if (authenticate.Succeeded)
User.AddIdentity((ClaimsIdentity)authenticate.Principal.Identity);
return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
}
On the authenticate.Succeeded line, I see that my user was properly authenticated. The User.AddIdentity line properly adds the identity to that user. However, when I look on the subsequent page load, the above identity is gone.
I'm at wits end. Any suggestions would be greatly appreciated. Thanks!
Update 1
Navigating directly to a page that is decorated with [Authorize(AuthenticationSchemes = "AADB2C.OpenIdConnect")] DOES properly result in the page recognizing the user as being authenticated. However, from there, navigating anywhere else then shows them no longer being authenticated.
Update 2
Calling IHttpContextAccessor.HttpContext?.AuthenticateAsync("AADB2C.OpenIdConnect") in places where I couldn't decorate with the Authorize flag (due to requiring access for non-authenticated users as well) properly fetches the authenticated user and their information. So, now the only piece of this puzzle I need to solve is finding a way to get Authorize into areas of the code which I can't access, due to being hidden behind proprietary third-party code.
Update 3
I'm unsure why, but it appears as though if I use AddOpenIdConnect instead of AddMicrosoftIdentityWebApp, it ... works? It defaults to that and my back-end now properly recognizes my authentication.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = null;
options.DefaultSignInScheme = null;
}).AddCookie(options =>
{
options.Events.OnSignedIn = async ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
{
// Syncs user and roles so they are available to the CMS
var synchronizingUserService = ctx
.HttpContext
.RequestServices
.GetRequiredService<ISynchronizingUserService>();
await synchronizingUserService.SynchronizeAsync(claimsIdentity);
}
};
}).AddOpenIdConnect(options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.Authority = $"MyAuthority";
options.ClientId = "MyClientId";
options.ClientSecret = "MyClientSecret";
options.CallbackPath = "/signin-oidc/aadb2b";
options.Scope.Clear();
options.Scope.Add(OpenIdConnectScope.OpenId);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Scope.Add(OpenIdConnectScope.Email);
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles",
NameClaimType = "preferred_username",
ValidateIssuer = false
};
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Response.StatusCode == 401)
{
ctx.HandleResponse();
}
return Task.CompletedTask;
};
options.Events.OnAuthenticationFailed = ctx =>
{
ctx.HandleResponse();
ctx.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(ctx.Exception.Message));
return Task.CompletedTask;
};
});
So, to summarize the steps that I took to resolve this:
Adding [Authorize(AuthenticationSchemes = "MyScheme")] to controllers will properly force authentication when navigating using that controller route.
Calling IHttpContextAccessor.HttpContext?.AuthenticateAsync("MyScheme") returns details of the authenticated principal, allowing code-based control in places where the [Authorize] approach won't work (because it needs to allow both anonymous and authenticated users, and renders differently based on that condition).
For the specific back-end code I couldn't access due to it being hidden behind third-party proprietary code (EPiServer in this case), I was able to resolve the issue by switching to use AddOpenIdConnect instead of AddMicrosoftIdentityWebApp. I'm unsure why this worked, but for the moment I'm not going to question it further.
We have a azure function where we are calling few API's. its an eventhub trigger function. we have a scenario where the api which we are calling goes into schedule maintenance very often. we want to build a re try mechanism where we are re trying for x number of times and on failure we want to disable the function on runtime. is there a way we can do that in the function app itself?
Thank you Melissa and Ian Kemp. Posting your suggestions as answer to help other community members.
Use the below code to disable the Azure Function
public class FunctionsHelper
{
private readonly ClientSecretCredential _tokenCredential;
private readonly HttpClient _httpClient;
public FunctionsHelper(string tenantId, string clientId, string clientSecret, string subscriptionId, string resourceGroup, string functionAppName)
{
var baseUrl =
$"https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{functionAppName}/";
var httpClient = new HttpClient
{
BaseAddress = new Uri(baseUrl)
};
_httpClient = httpClient;
_tokenCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
}
private async Task SetAuthHeader()
{
var accessToken = await GetAccessToken();
_httpClient.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse($"Bearer {accessToken}");
}
private async Task<string> GetAccessToken()
{
return (await _tokenCredential.GetTokenAsync(
new TokenRequestContext(new[] {"https://management.azure.com/.default"}))).Token;
}
public async Task StopFunction(string functionName)
{
await SetFunctionState(functionName, isDisabled: true);
}
public async Task StartFunction(string functionName)
{
await SetFunctionState(functionName, isDisabled: false);
}
private async Task SetFunctionState(string functionName, bool isDisabled)
{
await SetAuthHeader();
var appSettings = await GetAppSettings();
appSettings.properties[$"AzureWebJobs.{functionName}.Disabled"] = isDisabled ? "1" : "0";
var payloadJson = JsonConvert.SerializeObject(new
{
kind = "<class 'str'>", appSettings.properties
});
var stringContent = new StringContent(payloadJson, Encoding.UTF8, "application/json");
await _httpClient.PutAsync("config/appsettings?api-version=2019-08-01", stringContent);
}
private async Task<AppSettings> GetAppSettings()
{
var res = await _httpClient.PostAsync("config/appsettings/list?api-version=2019-08-01", null);
var content = await res.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AppSettings>(content);
}
}
internal class AppSettings
{
public Dictionary<string, string> properties { get; set; }
}
Check the SO for further information.
I want to call the api and at the function decides what level of info to show/return based on user's roles.
Can someone give a sample on how to get logged user's roles in Azure Function on Azure Static Web App?
When deploying Azure Function via "Function App", I can get the roles and current username, but with "Static Web App" I haven't figured it out yet.
namespace Function1
{
public class Function1
{
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ClaimsPrincipal principal)
{
IEnumerable<string> roles = principal.Claims.Where(e => e.Type.Equals("roles")).Select(e => e.Value);
string name = principal.Identity.Name;
string responseMessage = $"Hello, {name}. This HTTP triggered function executed successfully. {string.Join(',', roles)}";
return new OkObjectResult(responseMessage);
}
}
}
You can access like this,
public static ClaimsPrincipal Parse(HttpRequest req)
{
var header = req.Headers["x-ms-client-principal"];
var data = header.FirstOrDefault();
if(data == null) {
return null;
}
var decoded = System.Convert.FromBase64String(data);
var json = System.Text.ASCIIEncoding.ASCII.GetString(decoded);
var principal = JsonSerializer.Deserialize<ClientPrincipal>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
principal.UserRoles = principal.UserRoles.Except(new string[] { "anonymous" }, StringComparer.CurrentCultureIgnoreCase);
if (!principal.UserRoles.Any())
{
return new ClaimsPrincipal();
}
var identity = new ClaimsIdentity(principal.IdentityProvider);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId));
identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails));
identity.AddClaims(principal.UserRoles.Select(r => new Claim(ClaimTypes.Role, r)));
return new ClaimsPrincipal(identity);
}
Here is a sample
I am using a login dialog with below code. How do I know if user is authenticated or not ?
namespace Sample.Dialogs
{
public class LoginDialog : ComponentDialog
{
private readonly BotStateService _botStateService;
public LoginDialog(string dialogId, BotStateService botStateService) : base(dialogId)
{
_botStateService = botStateService ?? throw new
System.ArgumentNullException(nameof(botStateService));
InitializeWaterfallDialog();
}
private void InitializeWaterfallDialog()
{
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = "",
Text = "Please login",
Title = "Login",
Timeout = 300000, // User has 5 minutes to login
}));
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStepAsync,
LoginStepAsync
}));
InitialDialogId = nameof(WaterfallDialog); //InitialDialogId = $"
{nameof(LoginDialog)}.mainFlow";
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse != null)
{
return await stepContext.EndDialogAsync();
}
else
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null , cancellationToken);
}
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
// if token exists
if (tokenResponse != null)
{
// welcome user
await stepContext.Context.SendActivityAsync(MessageFactory.Text("You are now logged
in."),cancellationToken);
// Display the name
await OAuthHelpers.GetUserDetail(stepContext.Context, tokenResponse);
return await stepContext.CancelAllDialogsAsync();
}
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful
please try again."), cancellationToken);
return await stepContext.EndDialogAsync();
}
}
}
Now, even after successful login user have to type something to know if they logged in or not. However, we want once the login is (Azure AD) successful message should push to user from bot saying that your name is this and you have successfully logged in.
How do we achieve this. ?
Thanks,