I'm working on an application which has Angular in the frontend, a few services in the backend and IS4 as a token provider. I keep all user data (email, passwords, avatar etc.) in the AspNetUsers table to which only IS4 has access.
In one of the web services, I implemented logic which requires to get a list of users who meet some business requirements. To do that I added the UserController to IS4 web service and exposed an endpoint that returns that list.
Problem is that when I decorate the controller with the [Authorize] attribute, I can't reach that endpoint and I get a 404 error and request URI is changed in the response to
https://localhost:5001/Identity/Account/Login?ReturnUrl=%2FUser%2FSearch%3Fcategory%3Dpremium%26area%3Dsap
When I remove the attribute, it works properly. I want to keep that attribute of course.
My question is: what do I need to do to make a successful call from web service to IS4 to get that user's list? Set authorization headers? I did that. Configure clients in IS4? I tried that but most likely I did that incorrectly. Is my design bad? Then how should it look like?
This is the code I have
Web service:
//_discoverySettings.IdentityUrl = https://localhost:5001
var url = $"{_discoverySettings.IdentityUrl}/User/Search?category={category}&area={area}";
var idpClient = _httpClientFactory.CreateClient("IDPClient");
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
idpClient.SetBearerToken(accessToken);
var content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
requestMessage.Content = content;
var response = await idpClient.SendAsync(requestMessage);
IS4 UserController:
[ApiController]
[Authorize]
[Route("[controller]")]
public class UserController : Controller
{
private readonly IdentityContext _identityContext;
.
.
.
[HttpGet("search")]
public Task<List<UserDto>> Search(string category, string area)
{
return _identityContext.Users
.Where(x => x.Category == category && x.Area == area)
.Select(x => new UserDto{ ... })
.ToListAsync();
}
}
If needed I can provide more code, configuration etc.
IS4 works on my nerve sometimes so please be as verbose as possible.
[EDIT]
I have followed MD Zand answer and I added missing configuration to IS. 'challenger' is backend service and 'challenger_web' is angular frontend. I added LocalApi.ScopeName scope to both back and front. Decorated the controller with [Authorize(LocalApi.PolicyName)]
And added this part services.AddLocalApiAuthentication(); in IS startup. I also added this to AddIdentityServer() method options
options.Discovery.CustomEntries.Add("challenger", "localhost:5002");
options.Discovery.CustomEntries.Add("challenger_web", "localhost:4002");
This time I got 401 - Unathorizaed with the followieng message in the IS console
[08:58:02 Error] IdentityServer4.Validation.TokenValidator
Checking for expected scope IdentityServerApi failed
{
//some info
}
[08:58:02 Information] IdentityServer4.Hosting.LocalApiAuthentication.LocalApiAuthenticationHandler
IdentityServerAccessToken was not authenticated. Failure message: insufficient_scope
I saw that the IdentityServerApi scope was added in the database. But when I deserialized the JWT this is what I saw
//rest omitted
"aud": [
"challenger",
"https://localhost:5001/resources"
],
"client_id": "challenger_web",
"idp": "local",
"scope": [
"openid",
"profile",
"challenger"
],
IdentityServerApi scope should be included in the scope array. So what I did is I added that to the scope array in the UserManagerSettings in angular project
scope: "openid profile challenger IdentityServerApi",
Hope that helps other people :)
Usually you need add this line to the identity server to have some endpoints in your IdentityServer settings:
builder.Services.AddLocalApiAuthentication( );
Also you may need some more configurations. Check here for more details;
Related
I have to implement authorization for my web api using another/external API. So I have to get the JWT from the request and call another API with that token to know whether the user is authorized.
presently my authentication is working, and I am using
IServiceCollection.AddAuthentication().AddJwtBearer() // removed code to set options
in sample above, I have removed code to provide options and setting the TokenValidationParameters. So my auth logic is working as expected.
Now i am looking to implement custom Authorization. In my custom authorization logic i have to make call to another/external API and pass some parameters. The parameters will be different for different action methods in my controller. The external API will return bool (indicating whether authorized or not), I don't have need to maintain any application role/claims in my code.
is using dynamic policy name and string parsing as mentioned in doc the only/recommended option.
So i have to get jwttoken from request and call another API with that token to know if user is authorized or not.
You should try to prevent having to make an an outbound API request for each request your API gets.
It seems like you have an external authentication service which lets your users log in and returns a token of sorts. You need to know how that third party signs their tokens, then obtain some form of (public) key from them.
With this key you can validate whether the token has been signed by the party you trust. For this, you configure the appropriate TokenValidationParameters which you pass to your services.AddAuthentication().AddJwtBearer() so you can let Identity validate their token using their keys.
See:
Authorize with a specific scheme in ASP.NET Core
Microsoft.AspNetCore.Authentication.JwtBearer Namespace
Ultimately you'd also configure some sort of background job that cycles the keys of the external provider when they do, if they do, which they hopefully do.
As for your updated question: you appear to want to use an external service for authorization, i.e. who can do what.
You'll have to implement this yourself. Usually this is done using scopes, which are retrieved once, during authentication. These can contain values like "finance" to give access to financial controllers, or "finance:orders:list finance:products".
[RequiredScope("finance:orders", "finance:orders:list")]
public IActionResult Index()
{
return View();
}
If the API you're talking to does not have a way to retrieve the relevant scopes, claims or permissions during authentication (or once per resource), then you can't, for example, cache the user's roles to your controllers or entities.
You need to realise this will incur extra overhead per API call, as well as your application being down when the authentication/authorization service is down.
If you still want to do this, the most trivial way to do async authorization on a controller would be a policy:
public class AuthorizeWithAuthServiceRequirement : IAuthorizationRequirement
{
public const string PolicyName = "external-authorization-service";
}
public class AuthorizeWithAuthServiceHandler : AuthorizationHandler<AuthorizeWithAuthServiceRequirement>
{
private IYourApiService _yourApiService;
public AuthorizeWithAuthServiceHandler(IYourApiService yourApiService/* your DI here */)
{
_yourApiService = yourApiService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeWithAuthServiceRequirement requirement)
{
var httpContext = context.Resource as HttpContext
?? throw new ArgumentException("Can't obtain HttpContext");
// Get the user or their claims or the ID from the route or something
var user = httpContext.User;
var claim = user.FindAll("foo-claim");
var allClaims = user.Claims;
var id = httpContext.Request.RouteValues["id"];
// TODO: response and error handling
var isUserAuthorized = _yourApiService.IsUserAuthorized(user, id, entity, claim, ...);
if (!isUserAuthorized)
{
context.Fail(/* TODO: reason */);
}
}
}
You register this with DI like this:
// Register the handler for dependency injection
services.AddSingleton<IAuthorizationHandler, AuthorizeWithAuthServiceHandler>();
// Register the policy
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizeWithAuthServiceRequirement.PolicyName, x => { x.AddRequirements(new AuthorizeWithAuthServiceRequirement()); });
});
And then apply it to a controller or action method:
[Authorize(Policy = AuthorizeWithAuthServiceRequirement.PolicyName)]
public class FooController : Controller
{
}
If you want more fine-grained control like custom attributes with parameters (like [CustomAuthorization(ApiPermission.Foo)]) per controller or action, or if you want to first load an entity and pass that to the handler, see Ilja in Asp.Net Core: Access custom AuthorizeAttribute property in AuthorizeHandler and their GitHub repository demonstrating three different approaches.
I'm presently developing an application with an ASP.NET Core (2.1) API server/backend and an Angular Single Page Application frontend.
For user authentication, I'm using ASP.NET Core Identity, along with several of the default methods provided within AccountController for login, logout, register, forgot password, etc. My Angular application accesses this API to perform these actions.
The Angular client files are hosted directly out of wwwroot in the ASP.NET Core application. I am hosting using IIS Express (I have tried IIS as well to no avail) under localhost:5000.
Authentication is configured in ASP.NET Core's Startup class with the following:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = false;
config.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Prevent API from redirecting to login or Access Denied screens (Angular handles these).
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.CompletedTask;
};
});
Register and Forgot Password both work without a hitch. The API is called and the appropriate actions are taken.
Login appears to work: The method is invoked within Angular:
#Injectable()
export class AuthService extends BaseService {
constructor(private httpClient: HttpClient) {
super(httpClient, `${config.SERVER_API_URL}/account`);
}
// Snip
login(login: Login): Observable<boolean> {
return this.httpClient.post<boolean>(`${this.actionUrl}/Login`, login)
.catch(this.handleError);
}
// Snip
}
The ASP.NET Core API gets the call, logs the user in, and returns success to indicate to Angular that login worked and they should proceed to redirect the user:
namespace ApartmentBonds.Web.Api.Account
{
[Route("api/[controller]/[action]")]
public class AccountController : APIControllerBase
{
// Snip
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginViewModel model, string returnUrl = null)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return Ok(true);
}
}
return Ok(false);
}
// Snip
}
}
The session cookie is created successfully, and I can see it within the browser (using Firefox Quantum in this screenshot, but have also tried in IE, in Firefox incognito, etc):
To test, I have another API method which simply returns whether or not the calling user is signed in:
// Angular
isLoggedIn(): Observable<boolean> {
return this.httpClient.get<boolean>(`${this.actionUrl}/IsLoggedIn`)
.catch(this.handleError);
}
// ASP.NET Core API
[HttpGet]
[AllowAnonymous]
public IActionResult IsLoggedIn()
{
return Ok(_signInManager.IsSignedIn(User));
}
I make this call within the browser, and it returns false. (All other aspects of the site that depend on the user being logged in also show the user is not logged in -- this method is just a quick and dirty way to verify this problem.) Note that the client is correctly sending the Identity cookie along with the request:
Since the only site in question here is localhost:5000 I'm not sure why this is not working.
Things I've tried
Within services.ConfigureApplicationCookie
Setting options.Events.Cookie.Domain to localhost:5000 explicitly
Setting options.Events.Cookie.Path to /api
Clearing all cookies and trying incognito/other browsers (to rule out other localhost site cookies potentially clobbering the request)
Hosting the site via IIS (localhost:8888) after Publishing the Core Application
Anyone have any ideas?
In order for authentication handlers to run on the request, you need app.UseAuthentication(); in the middleware pipeline (before middleware that requires it like MVC).
This is a longshot, but maybe the token needs: "Bearer " (with the space) in front of the actual token.
I think you shouldn't use User property from controller when you are using AllowAnonymousAttribute.
Try to do request to some authorize endpoint. If you are not sign in, then you will get status code 401(Unathorized).
I have spent considerable time looking into getting my WebApi application to use the Owin authentication model as I wish to take advantage of all of the social media login methods. My immediate problem is understanding what it is that I'm doing wrong with regards to authenticating a calling user with a Bearer token set.
My Startup.Auth has been trimmed back to only the following:
OAuthManager.OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
OAuthManager.OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AuthorizeEndpointPath = new PathString("/oauth/externallogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AccessTokenFormat = new MyJwtFormat(),
Provider = new MyOAuthServerProvider(),
};
app.UseOAuthAuthorizationServer(OAuthManager.OAuthServerOptions);
app.UseJwtBearerAuthentication(new MyJwtBearerOptions());
MyOAuthServerProvider looks like this:
public class OAuthServerProvider : OAuthAuthorizationServerProvider
{
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var result = base.GrantResourceOwnerCredentials(context);
return result;
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult(true);
}
}
Question 1 is the above code all that I require when calling the service with a Bearer token set in the Authorization header? Is this the correct place for this Jwt to be placed? The token should be of the form 'Bearer ey.....' etc I believe.
Question 2 should the caller be placing the Bearer token in an 'Authorization' header on the call or some other location?
Question 3 why with the above code in place is my OAuthServerProvider never getting hit on either method? What is it that makes the Owin authentication system say 'Hold on a minute, we've got an Authorization header so I should process that' what is it that my client should be doing differently?
Is it possible that my WebApiConfig is upsetting the Owin OAuth perhaps? I have set the following:
config.SuppressDefaultHostAuthentication();
Although I'm not sure I understand what that means...
I'm trying to test the above using an integration test which performs the following actions:
Creates a user
Logs in as that user (correctly receiving a Bearer token)
Makes an authenticated call with the Bearer token set on the 'Authorization' header (authentication is not set on the Current Principal, HttpContext or OwinContext)
Please ask me questions for clarification, this is costing me a lot of time! Many thanks for any help.
As it turns out, my WebApi configuration was upsetting the authentication process. I had neglected to include the very important line:
config.Filters.Add(new HostAuthenticationFilter(OAuthBearerOptions.AuthenticationType));
After which authentication sprung into action in the OWIN world. This problem occurred mainly because I attempted to retrofit OAuth to a pre-existing WebApi app that hadn't been set up using templates that would have done the above for me.
So just a reminder then that in your WebApi configuration include both lines:
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthManager.OAuthBearerOptions.AuthenticationType));
In my webapi application created from template in VS2013 I have added custom OAuthBearerAuthenticationProvider class in Startup.Auth.cs file:
public class CustomBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
public override Task ValidateIdentity(OAuthValidateIdentityContext context)
{
UserManager<ApplicationUser> userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
var user = userManager.FindById(context.Ticket.Identity.GetUserId());
var claims = context.Ticket.Identity.Claims;
if (claims.FirstOrDefault(claim => claim.Type == "AspNet.Identity.SecurityStamp") == null
|| claims.Any(claim => claim.Type == "AspNet.Identity.SecurityStamp"
&& !claim.Value.Equals(user.SecurityStamp)))
{
context.Rejected();
}
return Task.FromResult<object>(null);
}
}
Also I have added the variable:
public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
And in the ConfigureAuth(IAppBuilder app) method I have added the following lines of code to use the custom OAuthBearerAuthenticationProvider class:
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
OAuthBearerOptions.AccessTokenFormat = OAuthOptions.AccessTokenFormat;
OAuthBearerOptions.AccessTokenProvider = OAuthOptions.AccessTokenProvider;
OAuthBearerOptions.AuthenticationMode = OAuthOptions.AuthenticationMode;
OAuthBearerOptions.AuthenticationType = OAuthOptions.AuthenticationType;
OAuthBearerOptions.Description = OAuthOptions.Description;
OAuthBearerOptions.Provider = new CustomBearerAuthenticationProvider();
OAuthBearerOptions.SystemClock = OAuthOptions.SystemClock;
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
All those changes I have made to implement my own custom logic of bearer token verification. For some reason the SecurityStamp verification is not implemented in Webapi application created from template in VS 2013. I thought this should have been done by default.
To verify the SecurityStamp validation concept I have changed SecurityStamp in the database and after that called some webapi method from client using old bearer token, i.e. containing old SecurityStamp claim. Please note my webapi controller is annotated with [Authorize] attribute. After that ValidateIdentity(OAuthValidateIdentityContext context) method has been called and context.Rejected() line has been executed and I was expecting the webapi method should not be called after that and the 401 Unauthorized response should be send back to the client.
But nothing of this happened. Webapi method did get called and the client did successfully get sensitive data from server whereas should not, because the old bearer token the client sent to the server for authentication and authorization must not be valid after password change.
I thought if context.Rejected() have been called in ValidateIdentity method any [Authorize] decorated webapi method should not be called and the
client should receive something like 401 Unauthorized response.
Am I misunderstanding the whole thing? If I am could anyone explain how it works, please? Why after context.Rejected() has been called the [Authorize] annotated controller's webapi method gets called and successfully returns sensitive data? Why the 401 Unauthorized response has not been sent instead? How to achieve the goal which is the 401 Unauthorized response to be sent back to the client when SecurityStamp claim is not the same as in the database currently?
Finally I was able to find the explanation of how the things works. It is the Hongye Sun's comment to his answer to the stackoverflow question:
How do you reject a Katana Bearer token's identity
I cite it here:
"UseOAuthBearerTokens will register Bearer authentication middleware and authorization server middleware into the pipeline. If you call both methods, you will register two Bearer auth middlewares. You need to call UseOAuthAuthorizationServer to register authorization server only."
So I replaced this line of code:
app.UseOAuthBearerTokens(OAuthOptions);
To this one:
app.UseOAuthAuthorizationServer(OAuthOptions);
And things started to work as they should. I. e. the [Authorize] annotated controller's webapi method not called and the 401 Unauthorized response sent back after context.Rejected() has been called.
I'm having problem with getting ServiceStack [Authentication] attribute to work in ASP.Net MVC4 controller, pages / action methods with the attribute keep redirecting Users to the login page even after the login details are submitted correctly.
I've followed the SocialBootstrapApi example, with the difference being that all the authentication web service calls are made from the controllers:
this.CreateRestClient().Post<RegistrationResponse>("/register", model);
Other things that I've done so far:
Use my own user session implementation subclassing AuthUserSession (not too different from the example, but using my own implementation of User table)
Inherit ServiceStackController on my BaseController, overriding the default login URL
Enable Auth feature in AppHost with my user session implementation
Registration does work, user auth logic works (even though the session does not persist), and I can see the ss-id and ss-pid cookies in the request.
So my complete list of questions:
How do I make the [Authenticate] attribute work (or, what did I do wrong)?
How do I save and reuse the user session in an MVC controller? At the moment this.UserSession is always null.
How do I logout a user? this.CreateRestClient().Get<AuthResponse>("/auth/logout"); does not seem to work.
Update 1:
The session cookies (ss-id and ss-pid) gets created when I attempt to load the secured page (ones with [Authenticate] attribute), before any credentials get submitted. Is this the expected behaviour?
Update 2:
I can see that the session is saved in MemoryCacheClient, however trying to retrieve it in the base controller via this.Cache.Get<CustomUserSession>(SessionKey) returns null (where SessionKey is like: urn:iauthsession:1)
After much fiddling around, apparently the way to hook ServiceStack authentication is to call the AuthService via:
try {
authResponse = AuthService.Authenticate(new Auth{ UserName = model.UserName, Continue = returnUrl, Password = model.Password });
} catch (Exception ex) {
// Cut for brevity...
}
and NOT authResponse = this.CreateRestClient().Post<AuthResponse>("/auth/credentials", model);!
Where AuthService is defined in the base controller as:
public AuthService AuthService
{
get
{
var authService = ServiceStack.WebHost.Endpoints.AppHostBase.Instance.Container.Resolve<AuthService>();
authService.RequestContext = new HttpRequestContext(
System.Web.HttpContext.Current.Request.ToRequest(),
System.Web.HttpContext.Current.Response.ToResponse(),
null);
return authService;
}
}
Everything else (incl. session) works correctly now.
You can find how it could be done in the ServiceStack Use Cases repository. The following example is based on MVC4 but works perfectly for MVC3 either: CustomAuthenticationMvc.