I have an ASP.NET 5 Project with API implemented using Swagger.
I have added multiple OpenAPI Servers within AddSwaggerGen() like following:
services.AddSwaggerGen(options =>
{
for (int i = 0; i < ServersSettings.getSettings().Count; i++)
{
Dictionary<string, string> data = getServers();
if (data.ElementAt(i).Value.DatabaseType == "Server")
{
if (data.ElementAt(i).Value.ApiEnabled == true)
{
options.AddServer(new OpenApiServer() { Url = ("/" + data.ElementAt(i).Key), Description = data.ElementAt(i).Value});
}
}
}
});
And Created an API Controller with a single function to get userdata from database using a tag like this:
[ApiController]
[ApiExplorerSettings(IgnoreApi = false)]
[Route("Api")]
public class ApiController : ControllerBase
{
[HttpGet("Player/{tag}")]
[ProducesResponseType(typeof(UserData), 200)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetUserByTag(string tag)
{
UserData userData = await getServerDatabase("openapi server name here").getUserByTag(tag);
if (userData != null)
{
return Ok(userData);
}
return BadRequest();
}
Now the problem I am facing is that on the SwaggerUI I am getting an option to choose from the multiple servers which is great but when I try out the Get /Api/Player/Tag function, the response adds the server key/name before the /Api/ like http://localhost:5000/ServerName/Api/Player/anythinghere and it is unable to get to the function as the controller is expecting http://localhost:5000/Api/Player/anythinghere.
I have tried adding {variables} within the route, input in the GetUserByTag() function but I am unable to figure out how to get the ServerName part into the ApiController.
Swagger UI: https://gyazo.com/3f405a572bfd67b972de4763f0728f0d
Related
I'm trying to handle google-signin to my existing ASP.NET MVC application. It has a forms authentication developped customely and it works fine for username and password.
I want to add Google-sign-in to this project. I currently added the button, created the app in google developper console got my id and secret and set up the urls.
I can see the button in login page, click, i see my account and picture, select account.
At this point google posts me some data. I received in an Action method an string array object called "credential" which has a string in first position. But I don't know what to do from here on...
Can somebody help me with this? Which document I should use?
I'm reading this: https://developers.google.com/identity/gsi/web till now but i'm stuck.
What I dont want is this:
https://learn.microsoft.com/en-us/aspnet/mvc/overview/security/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on
I want to handle the requests needed myself (with cookies, database checks and keeping track of tokens) and get the user information google provides by myself in my controller's action methods.
Here is a part of the razor view code
<div id="g_id_onload"
data-client_id="my id is here"
data-login_uri="#Url.Action("LoginWithGoogle","Login")"
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left"
data-auto_prompt="true"
>
</div>
I added this script:
<script src="https://accounts.google.com/gsi/client" async defer></script>
This Action method can catch the string:
[HttpPost]
public ActionResult LoginWithGoogle(string[] credential)
{
ViewBag.Data = credential;
///I will do necessary stuff here.
return View();
}
Notes:
-Identity is not installed and will not be used (if unless impossible without using it).
-My .Net Framework version: 4.7.2
Thanks for the help.
I don't use "full" default auth code too, here is how I handle Google/Yandex/Discord/OAuth response:
[HttpPost]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider, string? returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), null, new { returnUrl });
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null)
{
var info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return this.Redirect(LoginPath);
}
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
var name = info.Principal.FindFirstValue(ClaimTypes.Name) ?? info.Principal.Identity.Name;
// your own actions & checks with email, name etc
This still required some "default" preparations in Startup:
services.AddIdentity<User, UserStoreService.UserRole>()
.AddUserStore<UserStoreService>()
.AddRoleStore<UserStoreService>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddGoogle(...)
But here User is my own class, UserRole is own (end empty) class, and UserStoreService is my own implementation of IDisposable, IUserStore<User>, IUserEmailStore<User>, IUserClaimStore<User>, IUserSecurityStampStore<User>, IRoleStore<UserStoreService.UserRole> (you may modify this list according to your needs)
This is example how to process external authentication using owin. First of all, you have to setup your startup.cs and update configuration with something like this. Of course you have to import required packages into your project.
public void Configuration(IAppBuilder app)
{
....
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "YourGoogleClientId",
ClientSecret = "YourGoogleClientSecret",
});
}
Then in your account controller (example name) create method that will handle your login with google button. For example:
public ActionResult ExternalLogin(string provider)
{
var returnUrl = Request.Url.GetLeftPart(UriPartial.Authority) + "/Account/ExternalLoginCallback";
return new ChallengeResult(provider, returnUrl);
}
Create your ChallengeResult method:
internal class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUrl)
{
LoginProvider = provider;
RedirectUri = redirectUrl;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
var owin = context.HttpContext.GetOwinContext();
owin.Authentication.Challenge(properties, LoginProvider);
}
}
Add your new authentication method in your account controller. For example:
public async Task<ActionResult> ExternalLoginCallback()
{
try
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null || string.IsNullOrEmpty(loginInfo.Email))
{
return RedirectToAction("ExternalLoginFailed", "WebAccount");
}
else
{
// handle external login, means process user login in your system
}
}
catch (Exception ex)
{
// handle possible exceptions
}
return RedirectToAction("ExternalLoginFailed", "WebAccount");
}
private IAuthenticationManager AuthenticationManager
{
get { return HttpContext.GetOwinContext().Authentication; }
}
I've been trying to use Namespace routing to build some APIs dynamically without the need to worry about hardcoding the routes. However, I did find an example from MSDN to use namespaces and folder structure as your API structure. Here's the sample that I have to use Namespace routing:
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
private readonly string _baseNamespace;
public NamespaceRoutingConvention(string baseNamespace)
{
_baseNamespace = baseNamespace;
}
public void Apply(ControllerModel controller)
{
var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
if (hasRouteAttributes)
{
return;
}
var namespc = controller.ControllerType.Namespace;
if (namespc == null) return;
var templateParts = new StringBuilder();
templateParts.Append(namespc, _baseNamespace.Length + 1, namespc.Length - _baseNamespace.Length - 1);
templateParts.Replace('.', '/');
templateParts.Append("/[controller]/[action]/{environment}/{version}");
var template = templateParts.ToString();
foreach (var selector in controller.Selectors)
{
selector.AttributeRouteModel = new AttributeRouteModel()
{
Template = template
};
}
}
}
And here's the controller:
namespace Backend.Controllers.Api.Project.Core
{
public class UserController : ApiBaseController
{
public UserController()
{
}
[HttpPost]
public IActionResult Login(LoginInput loginInput) // <-- loginInput properties return null
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
}
}
in Startup.cs
namespace Backend
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Let's use namespaces as the routing default way for our APIs
services.AddControllers(options =>
{
options.Conventions.Add(new NamespaceRoutingConvention(typeof(Startup).Namespace + ".Controllers"));
});
}
}
}
Everything works ok except that when I trigger a POST api call to Login action the LoginInput doesn't get populated the values I'm sending through Postman i.e. {"username": "value", "password": "sample"} and it always returns null value. I'm not sure what am I doing wrong with the NamespaceRoutingConvention. Bear in mind if I remove it and hard-code the route in the controller like:
[ApiController]
[Route("api/project/core/[controller]/[action]/proda/v1")]
It works as expected. Any ideas?
Try to use this instead:
[HttpPost]
public IActionResult Login([FromBody]LoginInput loginInput)
{
if (!ModelState.IsValid) return BadRequest();
return Ok(user);
}
I think that by setting AttributeRouteModel, you're preventing the middleware invoked by having ApiControllerAttribute in the Controller to do its job, and so the defaults of treating object parameters as body is not applied.
This is a guess though, I haven't been able to find the corresponding code in the source code.
I have a project using .NET core version 3.1 and I'm using token for logging in. Everything works perfectly when testing with Postman, it created token and I can use it to access the Home page.
The problem is, when I started testing on client side, it doesn't work. I debugged and saw after logging in, the token is generated but I can't access the HomeController because of [Authorize] attribute.
This is my code to generate token:
public async Task<HttpResponse<LoginResult>> GetTokenAsync(LoginRequest loginInfo)
{
var audience = await _audiences.FindAsync(a => a.Id == loginInfo.ClientId);
string message = string.Empty;
if (audience != null)
{
bool audienceIsValid = _jwtProvider.ValidateAudience(audience.Issuer
, audience.SecretKey
, ref message);
if (audienceIsValid)
return await GenerateToken(loginInfo);
else
message = ErrorMessages.Login_AudienceInvalid;
}
else
message = string.Format(ErrorMessages.Login_Not_Permitted, "Your client Id");
return HttpResponse<LoginResult>.Error(message, HttpStatusCode.BadRequest);
}
I guess that token couldn't be stored correctly.
What am I missing?
UPDATE
This is my code in login
[HttpPost]
[Route("login")]
[AllowAnonymous]
public async Task<ActionResult> Login([FromForm]LoginRequest model)
{
model.ClientId = 1;
var response = await _services.GetTokenAsync(model);
if (response.StatusCode == 200)
{
return RedirectToAction("Index", "Home");
}
return RedirectToAction("Login");
}
And this is what I'm trying to access
[HttpGet]
[Route("index")]
[Authorize]
public IActionResult Index()
{
return View();
}
You need to create a custom policy to specify in the Authorize attribute that is configured to use a custom requirement handler
First you lay out the requirement of the custom policy via a class that inherits IAuthorizationRequirement
public class TokenRequirement : IAuthorizationRequirement
{
}
This is where you would optionally accept parameters if you need them. But normally you pass a token in the header of a request which your custom policy's requirement handler would have access to without the need for explicit parameters.
Your requirement's requirement handler to be used by your custom policy would look something like this
public class TokenHandler : AuthorizationHandler<TokenRequirement>
{
//Some kind of token validator logic injected into your handler via DI
private readonly TokenValidator _tokenValidator;
//The http context of this request session also injected via DI
private readonly IHttpContextAccessor _httpCtx;
//The name of the header your token can be found under on a Http Request
private const string tokenHeaderKey = "authToken";
//Constructor using DI to get a instance of a TokenValidator class you would
//have written yourself, and the httpContext
public TokenHandler(TokenValidator tokenValidator, IHttpContextAccessor httpCtx)
{
_tokenValidator = tokenValidator;
_httpCtx = httpCtx;
}
//Overriden implementation of base class AuthorizationHandler's HandleRequirementAsync method
//This is where you check your token.
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context
,TokenRequirement requirement)
{
if (context.Resource is Endpoint endpoint)
{
HttpRequest httpReqCtx = _httpCtx.HttpContext.Request;
string token =
httpReqCtx.Headers.TryGetValue(tokenHeaderKey, out StringValues tokenVal)
? tokenVal.FirstOrDefault()
: null;
if (string.IsNullOrWhitespace(token))
{
context.Fail();
}
else
{
bool tokenIsValid = await _tokenValidator.ValidToken(token);
if(tokenIsValid)
context.Succeed(requirement);
else
context.Fail();
}
}
return Task.CompletedTask;
}
}
You'd register your custom requirement handler on a custom policy name in Startup.cs like so
//This is a framework extension method under Microsoft.Extensions.DependencyInjection
services.AddHttpContextAccessor();
//Your custom handler
services.AddSingleton<IAuthorizationHandler, TokenHandler>();
//Your custom policy
services.AddAuthorization(options =>
{
options.AddPolicy(
//Your custom policy's name, can be whatever you want
"myCustomTokenCheckerPolicy",
//The requirement your policy is going to check
//Which will be handled by the req handler added above
policy => policy.Requirements.Add(new TokenRequirement())
);
});
The impl on the attribute would look like this
[HttpGet]
[Route("index")]
[Authorize(Policy = "myCustomTokenCheckerPolicy")]
public IActionResult Index()
{
return View();
}
I am trying to post data from angular 5 component to action method of .net core. I am able to hit the action method but values are null. As per below example Usr.FirstName is null in Sush action method.
Model-
namespace VModels
{
public class UserVM
{
public long UserId { get; set; }
public string FirstName { get; set; }
}
}
Action method of .net core-
[HttpPost]
public IActionResult Sush(UserVM Usr)
{
UserVM objOutput = new UserVM();
CommonGetPostMethod<UserVM, UserVM> objCommonMthd = new CommonGetPostMethod<UserVM, UserVM>();
UserVM objvm = new UserVM();
objvm.FirstName = "Susheel " + DateTime.Now.Ticks.ToString();
objCommonMthd.SaveData(Usr, "https://localhost:44303/api/UserAPI", ref objOutput);
return View(Usr);
}
Post method in angular 5-
SaveUser(userofrm: NgForm) {
var model = userofrm.value;
var values = JSON.stringify(model);
this.hpclient.post<UserVM>("https://localhost:44321/User/Users/Sush", values).subscribe(success => { }, fail => { });
}
Based on the code that you provided, I did a test with the following simple example, which work well on my side, you can refer to it.
// make request with testing data
var model = {'FirstName':'Test'};
var values = JSON.stringify(model);
console.log(values);
this.hpclient.post<UserVM>("https://localhost:44305/api/data/Sush", values, httpOptions).subscribe(success => { console.log(success);}, fail => { });
Define and configure headers for request(s)
import { HttpClient, HttpHeaders } from '#angular/common/http';
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
API controller action
[HttpPost("Sush")]
public IActionResult Sush(UserVM Usr)
{
Usr.UserId = 100;
return Ok(Usr);
}
Test Result
To troubleshoot the issue, you can try to make request with same data from postman etc tool and check if you can get expected data within action method. Or make request with testing data (as I did) rather than value inside userofrm, then check if it can work for you.
I have a .Net mobile service API that accepts an Id and a bool. If I call the API from Postman with the correct parameters and headers, it works fine. When I call it from my AngularJs app, I get a 404 error and I cannot figure out why.
My backend API method looks like this:
[HttpPost]
public async Task<IHttpActionResult> SetHoldStatus(string id, bool isHeld)
=> Ok(new
{
Transfer = await _workItemRepository.SetTransferHoldStatus(id, isHeld)
});
My AngularJs controller looks like this:
(function () {
angular
.module("pinnacleWarehouser")
.controller("TransferListCtrl",
["transfers",
"transferWorkItemHoldResource",
TransferListCtrl]);
function TransferListCtrl(transfers, transferWorkItemHoldResource) {
var vm = this;
vm.transfers = transfers;
vm.length = transfers.length;
if (vm.length > 0) {
vm.branch = transfers[0].branchId;
for (var i = 0; i < vm.transfers.length; i++) {
vm.transfers[i].transferId = vm.transfers[i].transferId.trim();
};
}
vm.changeHold = function (transferId, isHeld) {
transferWorkItemHoldResource.save({ Id: transferId, IsHeld: isHeld });
};
}}());
My resource looks like this:
(function () {
"use strict";
angular
.module("common.services")
.factory("transferWorkItemHoldResource",
["$resource", transferWorkItemHoldResource]);
function transferWorkItemHoldResource($resource) {
return $resource("https://my-test-url.net/api/TransferWorkItem/SetHoldStatus", {}, {
save: {
method: 'POST',
headers: { 'ZUMO-API-VERSION': '2.0.0' }
}
});
}}());
So, when I just call the API from Postman, it updates the record with the bool that I send as a parameter. It works.
When I run the app, and call the API, this is what I get:
Looking at the request header in dev tools, I can see that my parameters are in a request payload:
I've never tried using $resource to POST with a custom header and parameters, so I may be missing something. I'm hoping someone can point out what I'm doing wrong.
You should define the action parameter as complex object.
Change it
public async Task<IHttpActionResult> SetHoldStatus(string id, bool isHeld)
to
public async Task<IHttpActionResult> SetHoldStatus(SetHoldStatusInput input)
public class SetHoldStatusInput
{
public string Id { get; set; }
public bool IsHeld { get; set; }
}