I have an SPA app built with AngularJS, the backend is WebApi2. I´m struggling with Authentication and Authorization. What I want in the long run is to enable authentication against Active Directory. But for now, I just trying to enable authorization for my APiControllers and setting a Cookie with Owin.
Here is my Owin Identity Helper class, I´m only adding 1 claim that is the serialized user info:
public void SignIn(bool rememberMe, T user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.UserData, JsonConvert.SerializeObject(user)),
};
var claimsIdentity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = rememberMe }, claimsIdentity);
}
Here is authentication in controller:
[HttpGet, HttpPost]
[AllowAnonymous]
[Route("authenticate")]
public HttpResponseMessage Authenticate()
{
var authenticated = IdentityContext.Current.IsAuthenticated;
if (!authenticated)
{
var user = new User();
user.Email = "roger#moore.com";
user.Name = "Roger Moore";
user.Id = 23;
IdentityContext.Current.SignIn(true, user);
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(user),
Encoding.UTF8,
"application/json"
)
};
}
else
{
//return the user if authenticated
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(IdentityContext.Current.User), //from claim
Encoding.UTF8,
"application/json"
)
};
}
}
My StartUp class
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/signedout")
});
}
}
When I call the authenticate user I´m setting signing in, but when calling a controller with [Authorize] attribute, im not signed in. Furthermore, when having fiddler running I get the error:
"[Fiddler] Response Header parsing failed. This can be caused by an illegal HTTP response earlier on this reused server socket-- for instance, a HTTP/304 response which illegally contains a body. Response Data:"
Does anyone have any suggestions, or alternatives with example code for using JWT Token Authentication and Authorization From Angular to WebApi2?
Related
I'm using .NET Core 3.1 with Identity Server 4 and connecting to Azure AD via OpenIdConnect. I'm using a Vue.js front-end and .NET Core API. IdentityServer, the front-end, and the API are all hosted on-prem on the same server (same domain). Everything uses https. I'm using an Oracle database with EF model first, with fully-customized IdentityServer stores and a custom user store (I implemented the interfaces). I'm using IdentityServer's Quickstart, edited a little to hook up my custom user store instead of the test user. I'm running this in my dev environment.
If I type in the url to the IdentityServer, I'm redirected to Azure AD, signed-in successfully, and shown this page:
Grants - successful login
The claims are coming back from Azure AD and the auto-provisioning is successful. It is written successfully to the database.
Authenticating through my JS client hits IdentityServer, redirects to Azure AD, I sign-in, then it redirects to IdentityServer's ExternalController, then redirects back to a Microsoft url, then proceeds to repeat until it finally fails with this page:
Sign-in failure from Azure AD
My guess is I messed up a redirect uri somewhere. Here is my code and the IdentityServer log:
IdentityServer Log
That block of logging repeats 6-10 times. No errors or anything different at the end.
I had to break up the C# code because the site couldn't handle one of my long options lines.
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Client OIDC config:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
Callback method being hit for ExternalController:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD config:
redirect uri: [IdentityServer url]/callback-aad
Database table data:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
Please let me know if you need any additional information. Thank you
The problem was in my custom UserStore. I was getting the user by the Azure AD SubjectId instead of the UserSubjectId. So in the ExternalController, the ApplicationUser object was coming up as null. Instead of an exception, it kept going back to Azure AD to try to get the user again, but obviously that just creates an infinite loop. I didn't think to look there since my user was successfully provisioned with Id's and claims.
I am trying to login user as soon as he/she registers.
below is the scenario
1)Registration page is not on identity server.
2)Post user details to Id server from UI for user creation.
3)On successful user creation login the user and redirect.
4)Trying to do it on native app.
I tried it with javascript app but redirection fails with 405 options call.
(tried to redirect to /connect/authorize)
on mobile app, don't want user to login again after signup for UX.
Has anyone implemented such behavior
tried following benfoster
Okay so finally i was able to get it working with authorization code flow
Whenever user signs up generate and store a otp against the newly created user.
send this otp in post response.
use this otp in acr_value e.g acr_values=otp:{{otpvalue}} un:{{username}}
client then redirects to /connect/authorize with the above acr_values
below is the identity server code which handles the otp flow
public class SignupFlowResponseGenerator : AuthorizeInteractionResponseGenerator
{
public readonly IHttpContextAccessor _httpContextAccessor;
public SignupFlowResponseGenerator(ISystemClock clock,
ILogger<AuthorizeInteractionResponseGenerator> logger,
IConsentService consent,
IProfileService profile,
IHttpContextAccessor httpContextAccessor)
: base(clock, logger, consent, profile)
{
_httpContextAccessor = httpContextAccessor;
}
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
var processOtpRequest = true;
var isAuthenticated = _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated;
// if user is already authenticated then no need to process otp request.
if (isAuthenticated)
{
processOtpRequest = false;
}
// here we only process only the request which have otp
var acrValues = request.GetAcrValues().ToList();
if (acrValues == null || acrValues.Count == 0)
{
processOtpRequest = false;
}
var otac = acrValues.FirstOrDefault(x => x.Contains("otp:"));
var un = acrValues.FirstOrDefault(x => x.Contains("un:"));
if (otac == null || un == null)
{
processOtpRequest = false;
}
if (processOtpRequest)
{
var otp = otac.Split(':')[1];
var username = un.Split(':')[1];
// your logic to get and check opt against the user
// if valid then
if (otp == { { otp from db for user} })
{
// mark the otp as expired so that it cannot be used again.
var claimPrincipal = {{build your principal}};
request.Subject = claimPrincipal ;
await _httpContextAccessor.HttpContext.SignInAsync({{your auth scheme}}, claimPrincipal , null);
return new InteractionResponse
{
IsLogin = false, // as login is false it will not redirect to login page but will give the authorization code
IsConsent = false
};
}
}
return await base.ProcessInteractionAsync(request, consent);
}
}
dont forget to add the following code in startup
services.AddIdentityServer().AddAuthorizeInteractionResponseGenerator<SignupFlowResponseGenerator>()
You can do that by using IdentityServerTools class that IdentityServer4 provide to help issuing a JWT token For a Client OR a User (in your case)
So after the user signs up, you already have all claims needed for generating the token for the user:
including but not limited to: userid, clientid , roles, claims, auth_time, aud, scope.
You most probably need refresh token if you use hybrid flow which is the most suitable one for mobile apps.
In the following example, I am assuming you are using ASP.NET Identity for Users. The IdentityServer4 Code is still applicable regardless what you are using for users management.
public Constructor( UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IClientStore clientStore,
IdentityServerTools identityServerTools,
IRefreshTokenService refreshTokenService)
{// minimized for clarity}
public async Task GenerateToken(ApplicationUser user
)
{
var principal = await _signInManager.CreateUserPrincipalAsync(user);
var claims = new List<Claim>(principal.Claims);
var client = await clientStore.FindClientByIdAsync("client_Id");
// here you should add all additional claims like clientid , aud , scope, auth_time coming from client info
// add client id
claims.Add(new Claim("client_id", client.ClientId));
// add authtime
claims.Add(new Claim("auth_time", $"{(Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds}"));
// add audiences
var audiences = client.AllowedScopes.Where(s => s != "offline_access" && s != "openid" && s != "profile");
foreach (var audValue in audiences)
{
claims.Add(new Claim("aud", audValue));
}
// add /resources to aud so the client can get user profile info.
var IdentityServiceSettings = _configuration.GetSection("IdentityService").Get<IdentityServiceConsumeSettings>();
claims.Add(new Claim("aud", $"{IdentityServiceUrl}/resources"));
//scopes for the the what cook user
foreach (var scopeValue in client.AllowedScopes)
{
claims.Add(new Claim("scope", scopeValue));
}
//claims.Add(new Claim("scope", ""));
claims.Add(new Claim("idp", "local"));
var accesstoken = identityServerTools.IssueJwtAsync(100, claims);
var t = new Token
{
ClientId = "client_id",
Claims = claims
};
var refereshToken = refreshTokenService.CreateRefreshTokenAsync(principal, t, client);
}
This is just a code snippet that needs some changes according to your case
I've got a self-hosted web api application with an angular front end, and I need to now start authenticating users via Azure Active Directory.
I've downloaded the SinglePageApp example and I've set this up and have it running successfully.
https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi
When applying the necessary changes to my own app, I can successfully redirect the user to the Azure login screen and get back the userProfile using adal.js/adal_angular.js. I'm getting 401 unauthorized errors whenever I call my API, however using Fiddler, I can see that the bearer token is added to the HTTP header in each call.
Here is my AdalAngular setup:
.config(["$httpProvider", "adalAuthenticationServiceProvider", ($httpProvider, adalProvider) => {
adalProvider.init(
{
instance: "https://login.microsoftonline.com/",
tenant: "<snip>.onmicrosoft.com",
clientId: "<snip>",
extraQueryParameter: "nux=1",
cacheLocation: "localStorage" // enable this for IE, as sessionStorage does not work for localhost.
},
$httpProvider);
Here is my startup.cs code:
public void Configuration(IAppBuilder appBuilder)
{
ConfigureWebApi(appBuilder);
ConfigureAuth(appBuilder);
ConfigureFileSystem(appBuilder);
appBuilder.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
}
private void ConfigureWebApi(IAppBuilder appBuilder)
{
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
}
private void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ActiveDirectoryTenant"],
Audience = ConfigurationManager.AppSettings["ActiveDirectoryApplicationId"]
});
}
private void ConfigureFileSystem(IAppBuilder appBuilder)
{
//Set the Welcome page to test if Owin is hosted properly
appBuilder.UseWelcomePage("/welcome.html");
appBuilder.UseErrorPage(new Microsoft.Owin.Diagnostics.ErrorPageOptions() { ShowExceptionDetails = true });
var physicalFileSystem = new PhysicalFileSystem(#".\wwwroot");
if (ConfigurationManager.AppSettings.AllKeys.Contains("ContentPath"))
{
var path = ConfigurationManager.AppSettings["ContentPath"];
physicalFileSystem = new PhysicalFileSystem(path);
}
FileServerOptions fileOptions = new FileServerOptions();
fileOptions.EnableDefaultFiles = true;
fileOptions.RequestPath = PathString.Empty;
fileOptions.FileSystem = physicalFileSystem;
fileOptions.DefaultFilesOptions.DefaultFileNames = new[] { "index.html" };
fileOptions.StaticFileOptions.FileSystem = fileOptions.FileSystem = physicalFileSystem;
fileOptions.StaticFileOptions.ServeUnknownFileTypes = true;
appBuilder.UseFileServer(fileOptions);
}
Where ActiveDirectoryTenant and ActiveDirectoryApplicationId are in my app.config and match what is configured in my angular adalProvider.init code exactly.
Finally, my ApiController looks like this:
[Authorize]
[RoutePrefix("api/connection")]
public class ServerConnectionController : ApiController
{
[Route("all")]
[HttpGet]
public HttpResponseMessage GetAllConnections()
{
HttpResponseMessage response;
try
{
string owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var connections = _iDataAccess.GetAllConnections().ToList();
response = Request.CreateResponse(HttpStatusCode.OK, connections);
}
catch (Exception ex)
{
response = GetExceptionResponseMessage(ex);
}
return response;
}
}
As mentioned the HTTP request header captured by Fiddler looks ok, and the aud property on my ADAL.js userInfo.profile is the correct appid.
Any suggestions on what might be missing?
Note that this is not a native web based app, it's self-hosted, which means the web service is running on localhost as a windows service, and not in IIS.
I have configured the site to use HTTPS, but I get the same problem regardless of HTTP or HTTPS traffic.
Thanks for listening!
You need to declare the ConfigureAuth(appBuilder); as the first line in the Startup.cs Configuration method. You can find a good explanation here on why it need to be declared as the first.
I am trying to redirect to Identity Server's default login page when calling an API controller method from Angular's $http service.
My web project and Identity Server are in different projects and have different Startup.cs files.
The web project Statup.cs is as follows
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
var openIdConfig = new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.GivenName,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
n.Request.Headers.SetValues("Authorization ", new string[] { "Bearer ", n.ProtocolMessage.AccessToken });
}
}
};
app.UseOpenIdConnectAuthentication(openIdConfig);
app.UseResourceAuthorization(new AuthorizationManager());
app.Map("/api", inner =>
{
var bearerTokenOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi" }
};
inner.UseIdentityServerBearerTokenAuthentication(bearerTokenOptions);
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
inner.UseWebApi(config);
});
}
}
You will notice that the API is secured with bearer token authentication, whereas the rest of the app uses OpenIdConnect.
The Identity Server Startup.cs class is
public class Startup
{
public void Configuration(IAppBuilder app)
{
var policy = new System.Web.Cors.CorsPolicy
{
AllowAnyOrigin = true,
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true
};
policy.ExposedHeaders.Add("Location");
app.UseCors(new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(policy)
}
});
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
Notice that I have added a CorsPolicy entry in order to allow the Web App to hopefully redirect to the Login page. In addition, the Cors policy exposes the Location request header, since it contains the url that I would like to redirect to.
The Web Api controller method is secured using the Authorize Attribute, like so
[HttpPost]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[Authorize]
public PlayerData GetFilteredPlayers(PlayerInformationParameters parameters)
{
var playerInformation = composer.Compose<PlayerInformation>().UsingParameters(parameters);
var players = playerInformation.Players
.Select(p => new {
p.NameLast,
p.NameFirst,
p.Nickname,
p.BirthCity,
p.BirthState,
p.BirthCountry,
p.BirthDay,
p.BirthMonth,
p.BirthYear,
p.Weight,
p.Height,
p.College,
p.Bats,
p.Throws,
p.Debut,
p.FinalGame
});
var playerData = new PlayerData { Players = players, Count = playerInformation.Count, Headers = GetHeaders(players) };
return playerData;
}
The angular factory makes a call to $http, as shown below
baseballApp.factory('playerService', function ($http, $q) {
return {
getPlayerList: function (queryParameters) {
var deferred = $q.defer();
$http.post('api/pitchingstats/GetFilteredPlayers', {
skip: queryParameters.skip,
take: queryParameters.take,
orderby: queryParameters.orderby,
sortdirection: queryParameters.sortdirection,
filter: queryParameters.filter
}).success(function (data, status) {
deferred.resolve(data);
}).error(function (data, status) {
deferred.reject(status);
});
return deferred.promise;
}
}});
When this call occurs, the response status is 200, and in the data, the html for the login page is returned.
Moreover, I can see on Chrome's Network tab that the response has a Location header with the url of the Login page. However, if I set up an http interceptor, I only see the Accept header has been passed to the javascript.
Here are the http headers displayed in Chrome's network tab:
The response does not have the Access-Control-Allow-Origin header for some reason.
So I have the following questions:
Is there a way I could get access to the Location header of the response in the angular client code to redirect to it?
How might I be able to get the server to send me a 401 instead of 200 in order to know that there was an authentication error?
Is there a better way to do this, and if so, how?
Thanks for your help!
EDIT:
I have added a custom AuthorizeAttribute to determine what http status code is returned from the filter.
The custom filter code
public class BearerTokenAutorizeAttribute : AuthorizeAttribute
{
private const string AjaxHeaderKey = "X-Requested-With";
private const string AjaxHeaderValue = "XMLHttpRequest";
protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var headers = actionContext.Request.Headers;
if(IsAjaxRequest(headers))
{
if (actionContext.RequestContext.Principal.Identity.IsAuthenticated)
actionContext.Response.StatusCode = System.Net.HttpStatusCode.Forbidden;
else
actionContext.Response.StatusCode = System.Net.HttpStatusCode.Unauthorized;
}
base.HandleUnauthorizedRequest(actionContext);
var finalStatus = actionContext.Response.StatusCode;
}
private bool IsAjaxRequest(HttpRequestHeaders requestHeaders)
{
return requestHeaders.Contains(AjaxHeaderKey) && requestHeaders.GetValues(AjaxHeaderKey).FirstOrDefault() == AjaxHeaderValue;
}
I have observed two things from this: first, the X-Requested-With header is not included in the request generated by the $http service on the client side. Moreover, the final http status returned by the base method is 401 - Unauthorized. This implies that the status code is changed somewhere up the chain.
Please don't feel like you have to respond to all the questions. Any help would be greatly appreciated!
You have probably configured the server correctly since you are getting
the login page html as a response to the angular $http call -> it is
supposed to work this way:
angularjs $http
Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning that the outcome (success or error) will be determined by the final response status code.
You are getting a 200 OK response since that is the final response as the redirect is instantly followed and it's result resolved as the $http service outcome, also the response headers are of the final response
One way to achieve the desired result - browser redirect to login page:
Instead of redirecting the request server side (from the web project to the Identity Server) the web api controller api/pitchingstats/GetFilteredPlayer could return an error response (401) with a json payload that contains a {redirectUrl: 'login page'} field or a header that could be read as response.headers('x-redirect-url')
then navigate to the specified address using window.location.href = url
Similar logic can often be observed configured in an $httpInterceptors that handles unauthorized access responses and redirects them to the login page - the redirect is managed on the client side
I have two main projects in my Web application:
WebApi project as back-end to serve authentication and authorization for the Web project,using OWIN 2 with bearer tokens.
Web project uses Angularjs.
The Web project works as expected(authentication and authorization are working)
Method: store token to localstorage, and send it using interceptors each request.
Now I want to add authentication and authorization to the the WebApi project,which would serve other modules like Hangfire,Elmah and Help pages.
I added the same login logic, which works(Authorizing) and then redirect to Dashboard page(using Angularjs) which works.
But going to any other page(one of the mentioned modules) don't work.By not working: The user from the Owin context always null/empty.(see code)
For my understanding, I need somehow to send the token with each request which doesn't happen here.
Questions:
How can I achieve that(sending/getting the token)?
If cookie is the only/better approach ↴
How can I integrate cookie for project 1 and token for project 2?(Tried to use cookies, but it seems I'm doing it wrong, or does it work simultaneously with bearer tokens?)
Code:
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
GlobalConfiguration.Configure(WebApiConfig.Register);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
AreaRegistration.RegisterAllAreas();
app.UseHangfire(hangfireConfig =>
{
config.UseAuthorizationFilters(
new AuthorizationFilter { Users = "admin, superuser", Roles = "advanced" },
new ClaimsBasedAuthorizationFilter("name", "value")
);
hangfireConfig.UseSqlServerStorage("Context");
hangfireConfig.UseServer();
});
}
I tried for testing purposes:
public class HFAuthorizationFilter : Hangfire.Dashboard.IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
var context = new OwinContext(owinEnvironment);
if (context.Authentication.User == null)
return false;//Always null
return context.Authentication.User.HasClaim(ClaimTypes.Role, "SuperAdmin")
|| context.Authentication.User.HasClaim(ClaimTypes.Role, "Admin");
}
}
and in Configuration:
app.UseHangfire(hangfireConfig =>
{
hangfireConfig.UseAuthorizationFilters(
new HFAuthorizationFilter()
);
hangfireConfig.UseSqlServerStorage("Context");
hangfireConfig.UseServer();
});
Potential duplicate:
Passing and verifying the OWIN Bearer token in Query String in WebAPI
if i understood correctly, you are looking to implement token generation in one api and use the same token in other api. if that is the case then you need master api to be the token generator and child or dependent api to consume the token. Please find master and child API config for oauth
Master API config:
public void ConfigureOAuth(IAppBuilder app)
{
//configure OAuth using owin framework
var oAuthOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
Provider = new KatanaAuthorizationServerProvider()
};
app.UseOAuthAuthorizationServer(oAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
Child API config:
public void ConfigureAuth(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}