How to make HTTP requests to an API controller with AuthorizedAttribute? - c#

Short story
I have a Web API. I want to prevent unauthorized requests so I added [Authorize(Roles = "admin")] attribute to my ApiController.
How can I make requests to my own API from different apps in C# (from desktop app for example)?
Long story
I have a simple web app solution which contains 2 projects.
In project1 users can sign in via username and password and do stuff. Code for signing users is as shown here:
if (ModelState.IsValid)
{
var user = await _userManager.GetFromModelAsync(model); // just get user from DB
var claims = new List<Claim>
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.Code),
new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role?.Name)
};
var id = new ClaimsIdentity(
claims, "ApplicationCookie",
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
await _httpContextAccessor.HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(id)); // I use httpContextAccessor cause I have this code not in controller
}
With this authentication, the user can easily access to any controller with attribute [Authorize(Roles = "user")].
In Project2, I have Admin Web API which can change stuff that users did. I want to call this API from a desktop C# app and I also want that API to have an authorization requirement ([Authorize(Roles = "admin")] in my case).
For example, it looks something like this:
[Authorize(Roles = "user")]
public class AdminApiController : Controller
{
public async Task<IActionResult> Index()
{
return Ok("Ok");
}
}
And the question is: with such an authentication mechanism how should I make HTTP requests to such an API with HttpClient? The only way I know is to use WebClient and emulate authorization with UploadValues to special authorization form.
P.S. I tried setting HTTP Authorization header but it didn't work. Maybe the point is to use separate authentication mechanism for users and admins?
Any help on this is highly appreciated.

I think you can follow the authentication mechanism JWT(JSON web Token) instead of cookies as the desktop app is a different environment that not use a web browser
User authentication scenario
the desktop app login to web API by userName and password param
if passed generate a token for the authenticated user and back it in response
when desktop app hit API action decorated by authorizing role via token
For a full example check this Url
Here is an example code in desktop app:
using Newtonsoft.Json;
...
var client = new HttpClient();
var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.domain.com/action"),
Headers = {
{ HttpRequestHeader.Authorization.ToString(), "Bearer xxxxxxxxxxxxxxxxxxx" },
{ HttpRequestHeader.Accept.ToString(), "application/json" },
{ "X-Version", "1" }
},
Content = new StringContent(JsonConvert.SerializeObject(svm))
};
var response = client.SendAsync(httpRequestMessage).Result;

Related

.net core 6 OpenID(Steam) authentication from API

I'm trying to make it available for users to Authorize through Steam.
I'm currently working with a Frontend on port 8080 and a backend on port 5000. There is a local SSL on both services.
I'm able to sign in via Steam through the backend and retrieve information, the claims, about the user logged in. The problem is that the cookie or claims that is sent after the successful login is not available from the instance of the frontend, because when the user authorize with Steam it's entirely through the backend and therefore no WebToken, cookie or session is available to the frontend.
I'm using
AspNet.Security.OpenId (6.0.0)
AspNet.Security.OpenId.Steam (6.0.0)
I currently have the following setup in the backend
Startup.cs in ConfigureServices
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Authentication/SteamLogin";
options.LogoutPath = "/Authentication/SteamLogout";
options.Cookie.Domain = "https://localhost:8080"; //Tried to do some shared cookie thing but doesnt work
})
.AddSteam(options =>
{
options.ApplicationKey = "SECRET_APP_KEY";
});
AuthenticationController SteamAuthorize
So for connecting through steam you need to access this location directly
"https://localhost:5000/Authentication/SteamAuthorize".
Normally i would authenticate by sending a GET or POST call from the frontend so the frontend would have a relation to the backend. Here there is none because you access the action directly.
[AllowAnonymous]
[Route("SteamAuthorize")]
[HttpGet]
public IActionResult SteamAuthorize()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/", IsPersistent = true }, "Steam");
}
AuthenticationController Index
So after the user has successfully logged in through steam the response is sent to this location.
In here it's not a problem getting the acquired data, but now i need the claims and identity to be available when my frontend makes a request to the backend.
[HttpGet("~/")]
public async Task<ActionResult> Index()
{
var openIdSteamId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
var steam_userName = User.Identity.Name;
if (steam_userName != null && openIdSteamId != null)
{
var steam_userId = openIdSteamId.Value.Replace("https://steamcommunity.com/openid/id/", "");
//I've tried redirecting to the frontend which then makes a new call afterwards
//The frontend will make a new call to this "/" page and now the Claims are gone
return Redirect("https://localhost:8080/steamauthorizing");
}
return Redirect("https://localhost:8080/");
}
I've tried to set options.Cookie.Domain = "https://localhost:8080"; but that didn't work.
I've generally looked for how to make the principal and claims persist through the two services.
To sum the question up. How do I authenticate a third-party app, with OpenID, through my backend, with the Identity also available to my frontend?

Is it possible use AuthorizeAttribute with a external api call?

i'm working with asp mvc 5, and i have to schedule some tasks, so i want to create a simple methods to be called from a simple console program C# and schedule them with Windows Task Scheduler.
The think is, i'm using Identity with Authorize attribute to manage the user permissions.
For example, i have the next method:
[Authorize(Roles="Admin")]
public async Task<JsonResult> CriticalTask(string someParam)
{
//procesing data
return null;
}
The think, is:
I dont know how can i do the login to pass the validation from the Authorize(Roles="Admin")
I try creating a simple method to login before it, but that doesn't work
I'm trying some like this
const string URL = "http://localhost:53665/";
RestClient mClient = new RestClient(URL);
const string parameterUserName = "userName";
const string parameterPassword = "password";
const string ruta = "Usuarios/ApiLogin";
var request = new RestRequest(ruta);
request.AddParameter(parameterUserName, "userName");
request.AddParameter(parameterPassword, "password");
//Method to login
var result2 = mClient.Execute(request);
Console.WriteLine($"Login\n{result2.Content}");
//Method that needs Admin permissions
request = new RestRequest("Usuarios/Test");
var result3 = mClient.Execute(request);
Console.WriteLine($"Test\n{result3.Content}");
is that possible only with Authorize attribute? or i need to implement some token method to authorize this calls?
Thanks!
What you should do is to save the received token after login and then add the token to request header which needs to be authorized:
var result = mClient.Execute(request);
string resultContent = result.Content.ReadAsStringAsync().Result;
//This token will be used for authorization
var token = JsonConvert.DeserializeObject<TokenModel>(resultContent);
var request = new RestRequest("Usuarios/Test"); //add token to header of request
mClient.AddDefaultHeader("Authorization", string.Format("bearer {0}", token.Access_Token));
var result3 = mClient.Execute(request);
Token model:
public class TokenModel
{
...
public string Access_Token { get; set; }
...
}
The easiest solution would be to use BasicAuth - you pass Credentials in headers for each request, and each request is validated separately - search for MVC Basic auth fr sample setup. I's the easiest form - but also very insecure as you pass your credentials in each call in almost plain text (it is only base64 of your credentials)
I'd suggest you to use Identity Server 4 to authorize your client using bearer token.
this way before first call you request token from server and then pass it to following requests and use this to authorize your api calls.
see following tutorial about setup.
http://docs.identityserver.io/en/aspnetcore1/quickstarts/6_aspnet_identity.html
in following url you can see example of in memory users, but also token requests.it's easy but obsolete
https://neelbhatt.com/2018/03/04/step-by-step-setup-for-the-auth-server-and-the-client-identityserver4-with-net-core-part-ii/
you can also use some sort of following code to obtain token in less obsolete way:
using (var httpClient = new HttpClient()) {
var discovery = await _httpClient.GetDiscoveryDocumentAsync(_configuration["ApiBaseAddress"]);
if (discovery.IsError)
{
return false;
}
request.Address = discovery.TokenEndpoint;
request.ClientId = _configuration["AuthClientName"];
request.ClientSecret = _configuration["AuthClientSecret"];
var request = new PasswordTokenRequest
{
UserName = "yourUserName",
Password = "yourPassword"
};
var token = await _httpClient.RequestPasswordTokenAsync(request);
}
in token.AccessToken you have your access token - the one needed to be sent to call api. you also have your refresh token in token.RefreshToken - it will be useful later
then to send call simply add bearer token to your HttpRequestMessage and it's done
var _httpClient = new HttpClient();
//Configure your http client here
var req= new HttpRequestMessage("http request method", "request uri");
req.SetBearerToken("your access token goes here);
var result = await _httpClient.SendAsync(req);
keep in mind that after you receive permission denied it's better to refresh token than obtain another one (you don't need to send your credentials). You use your refresh token i mentioned earlier.
The code for refreshing token is very similar to obtaining token by login/password. Just instead of PasswordTokenRequest class use following class:
var request = new RefreshTokenRequest
{
RefreshToken = _refreshToken
};
and instead httpClient.RequestPasswordTokenAsync(request) use httpClient.RequestRefreshTokenAsync(request). rest of code may remain similar.
I end up creating a custom attribute, based on the reply posted by crrlos in StackOverflow in Spanish.
I will translate it as best I can so that it can serve others
What you can do is create a custom authorization attribute, for that
you create a class that inherits AuthorizeAttribute and override the
AuthorizeCore method. The modification consists in passing to the
route an additional parameter to indicate that it is calling from the
task scheduler, if the parameter is not found then it will perform the
normal validation, if the parameter is found (it must have a value or
if it will not be null ) Then take the credentials of the url and
perform the validation, if they are correct return a true allowing
access to the method.
public class CustomAuthorization : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//get special parameter indicating that the request was made from the task scheduler
var parametro = httpContext.Request.QueryString.Get("parametro_especial");
if(parametro != null)
{
// get access credentials and validate them
// if they are valid, return true
}
//if they are not valid, or aren't present
//try with deffault validate.
return base.AuthorizeCore(httpContext);
}
}
How to use it?
[CustomAuthorization (Roles = "Admin")]
public JsonResult CargarTodosLosArticulos()
{
return null;
}

Is there any way to change client_id and client_secret on the fly in IdentityServer4

We created a multi-tenant SaaS application and I need to change client_id dynamically. How can i change client_id and client_secret in OnRedirectToIdentityProvider event ?
As mentioned in the comment for #d_f it's a wrong approach.
The right approach to do that is passing the tenant id from client to IdentityServer using the event : OnRedirectToIdentityProvider
Example
options.Events.OnRedirectToIdentityProvider = (loginRedirectContext) =>
{
var servicesProvider = loginRedirectContext.HttpContext.RequestServices;
var tenantInfo = servicesProvider.GetRequiredService<IRequestContextTenantInfo>();
loginRedirectContext.ProtocolMessage.SetParameter("tenantId", tenantInfo.Id);
return Task.FromResult(0);
};
In The Identity Server You can read it in Login Action method like this :
var _interaction = context.RequestServices.GetRequiredService<IIdentityServerInteractionService>();
var returnUrl = context.Request.Query["ReturnUrl"].ToString();
var authContext = await _interaction.GetAuthorizationContextAsync(returnUrl);
tenantId = authContext.Parameters["tenantid"];
Now based on tenant id you have to know which tenant you have to connect with to verify the login process.
It's also a good practice to inject a specific custom middleware in IdentityServer4 to handle the multitenancy in the early stage in the request pipeline
Example in Configure method in Startup :
app.UseMultiTenancy();
app.UseIdentityServer();
and UseMultiTenancy() method will read the tenant parameter that is sent from client.

Register External Login Web API

I don't understand why their isn't a clear tutorial or guideline on this, so I hope my question can be answered here.
So, trying to register users from facebook or google, via the Web Api.
The problem is, at the RegisterExternal method, on this line:
var info = await Authentication.GetExternalLoginInfoAsync();
It returns null, and thus returning a BadRequest()
What I got so far:
In Startup.Auth.cs I've hadded the id's and the secrets, note that I have also tried using Microsoft.Owin.Security.Facebook
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
AppId = "103596246642104",
AppSecret = "1c9c8f696e47bbc661702821c5a8ae75",
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));
return Task.FromResult(0);
}
},
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "328779658984-t9d67rh2nr681bahfusan0m5vuqeck13.apps.googleusercontent.com",
ClientSecret = "ZYcNHxBqH56Y0J2-tYowp9q0",
CallbackPath = new PathString("/api/Account/ManageInfo")
});
facebookOptions source: this post
That extra facebookOptions did not solve the problem.
I am able to retrieve an access_token from both Google and Facebook. I'm also able to Authenticate with this access_token to api/Account/UserInfo
GET http://localhost:4856/api/Account/UserInfo
in the header:
Authorization: Bearer R9BTVhI0...
Which returns:
{"Email":"firstname lastname","HasRegistered":false,"LoginProvider":"Facebook"}
One issue I notice their, is that it returns my name as Email, not the actual Email adress.
Now I want to register the external login with a new user for my database, which I make a POST call like this:
POST http://localhost:4856/api/Account/RegisterExternal
[header]
authorization: bearer 6xcJoutY...
Content-Type: application/json
[body]
{"Email":"...#hotmail.com"}
source: this post
Now this returns a BadRequest on this code snippit, inside RegisterExternal():
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
//AuthenticationManger?
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
In debugging, the ExternalLoginConfirmationViewModel does contain my email adress.
What am I doing wrong? Do I have to add something to the Startup.cs? Is there something more I have to do in the Startup.Auth.cs? Am I incorrectly calling RegisterExternal? In MVC it goes so smooth, why not in the Web API?
Aso looked at this answer from this question, But I didn't understand how to implement this.
This method is not really practical, since you are developing an API, that will most likely be used for apps, you best way is to handle the login with facebook by the API consumer, and let them send you an facebook auth token.
Basically I was trying to do this:
Create external login link for facebook.
Send user to that link that will bring them to facebook login page.
After login facebook will redirect to api.
User would be registered, but how does the app/website that is consuming the API know?
What you want to do is this:
API consumer creates their own method to login with facebook (for apps via SDK's)
API consumer will send an facebook token to the API to register/login.
API will check token with facebook graph endpoint.
When succeeded, API will return an bearer token for the API to make further authenticated requests.
So for you as an API developer, you would verify the token like so:
var verifyTokenEndPoint = string.Format("https://graph.facebook.com/debug_token?input_token={0}&access_token={1}", accessToken, appToken);
And then get the userId
var client = new HttpClient();
var uri = new Uri(verifyTokenEndPoint);
var response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
dynamic jObj = (JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(content);
string user_id = jObj["data"]["user_id"];
string app_id = jObj["data"]["app_id"];
}
Eventually you would create or find a user like so:
IdentityUser user = await _userManager.FindAsync(new UserLoginInfo(provider, verifiedAccessToken.user_id));
And then it's all up to you how to create an bearer token, if you follow the tutorial listed below, you could have this:
var tokenExpiration = TimeSpan.FromMinutes(30);
ClaimsIdentity identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties()
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(tokenExpiration),
};
var ticket = new AuthenticationTicket(identity, props);
var accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
Source, with full tutorial here
I've also got the email via the SDK and send that along with the POST request, since I managed both the API and the consumer. Warning though: A facebook user might not want to give you an e-mail address.
Get e-mail after facebook login on Android and IOS

How to distinguish a mvc 5 response from an web api 2 response?

my situation is this:
I'm working with mvc5 and webapi2 inside the same project. I started working with mvc, and as i needed authentication and authorization features i decided to use identity. Then, because of the changing requirements, i needed to implement a web service, so i decided to use webapi2, but it had to be authenticated as well, so i picked basic authorization as my authentication method. For this i used a custom authentication filter. Everytime something goes wrong ("no credentials", "invalid credentials") this filter return an unauthorizedResult result through the context result property:
context.Result = new UnauthorizedResult(new AuthenticationHeaderValue[] { new AuthenticationHeaderValue("Basic") }, context.Request);
This actually send a 401 status code through the response (i checked it using the debugger). The problem is that in my client i'm receiving a 200 status code. i figured that this is because i'm receiving the login page as a result, and all because of this block of code, that i set when i started to configured my mvc app:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Cuenta/Login"),
});
somehow, in some context pipeline, my unauthorized webapi response is catched and treated as response that comes from a mvc controller. So please, how i avoid this? I need to receive a 401 status code when my web service try to get a secured resource, but i still need the "return to login if secured" funcionality for my secured resources in the mvc part of my app.
Sorry i'm new at this and also sorry for my writting. Thank you.
If and only if your web-api routes and not your MVC contain /api/ you can solve it by creating your custom CookieAuthenticationOptions.Provider.OnApplyRedirect like this:
var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
var provider = new CookieAuthenticationProvider();
var originalHandler = provider.OnApplyRedirect;
provider.OnApplyRedirect = context =>
{
if (!HttpContext.Current.Request.Url.AbsoluteUri.Contains("/api/"))
{
var routeValues = new RouteValueDictionary();
var uri = new Uri(context.RedirectUri);
var returnUrl = HttpUtility.ParseQueryString(uri.Query)[context.Options.ReturnUrlParameter];
routeValues.Add(context.Options.ReturnUrlParameter, returnUrl);
per.Action("login", "account", routeValues);
context.RedirectUri = newUri;
originalHandler.Invoke(context);
}
};
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(urlHelper.Action("Login", "Account")),
Provider = provider
});

Categories