No shared secret configured for client for IdentityServer4's reference token - identityserver4

I'm using IdentityServer4 with IdentityServer4.AccessTokenValidation for handling Reference Token.
This is what I have done in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add identity server 4.
services.AddIdentityServer()
.AddProfileService<IdentityServerProfileService>()
.AddInMemoryClients(LoadInMemoryIdentityServerClients())
.AddInMemoryApiResources(LoadInMemoryApiResources())
.AddInMemoryIdentityResources(LoadInMemoryIdentityResource())
.AddProfileService<IdentityServerProfileService>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddDeveloperSigningCredential();
// Add jwt validation.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:44386";
options.ClaimsIssuer = "https://localhost:44386";
// name of the API resource
options.ApiName = "api1";
options.ApiSecret = "web-api-secret";
options.RequireHttpsMetadata = false;
});
}
protected static IEnumerable<Client> LoadInMemoryIdentityServerClients()
{
var clients = new List<Client>();
var client = new Client();
client.ClientId = "web-api-client";
client.AllowedGrantTypes = GrantTypes.ResourceOwnerPassword;
client.ClientSecrets = new[] {new Secret("web-api-secret".Sha256())};
client.AccessTokenType = AccessTokenType.Reference;
client.AllowedScopes = new[]
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
"api1"
};
clients.Add(client);
return clients;
}
protected static IEnumerable<IdentityResource> LoadInMemoryIdentityResource()
{
//var profileIdentityResource = new IdentityResource("repository-read", "repository-read", new List<string> { "claim-01", "age" });
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
//profileIdentityResource
};
}
protected static IEnumerable<ApiResource> LoadInMemoryApiResources()
{
var apiResources = new List<ApiResource>();
var apiResource = new ApiResource("api1", "My API");
apiResource.UserClaims = new[]
{
"age"
};
apiResources.Add(apiResource);
return apiResources;
}
When I make a request with structure shown in the below image:
I received a token.
After using the received token to make a request to protected api resource api/user/search. It gave me 401 status code.
In visual studio output. This is what I saw:
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:56219/api/user/search application/json 5
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 10.9132ms 307
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST https://localhost:44386/api/user/search application/json 5
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST https://localhost:44386/connect/introspect application/x-www-form-urlencoded 143
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Debug: AuthenticationScheme: Bearer was not authenticated.
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Debug: AuthenticationScheme: Bearer was not authenticated.
IdentityServer4.Hosting.EndpointRouter:Debug: Request path /connect/introspect matched to endpoint type Introspection
IdentityServer4.Hosting.EndpointRouter:Debug: Endpoint enabled: Introspection, successfully created handler: IdentityServer4.Endpoints.IntrospectionEndpoint
IdentityServer4.Hosting.IdentityServerMiddleware:Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.IntrospectionEndpoint for /connect/introspect
IdentityServer4.Endpoints.IntrospectionEndpoint:Debug: Starting introspection request.
IdentityServer4.Validation.BasicAuthenticationSecretParser:Debug: Start parsing Basic Authentication secret
IdentityServer4.Validation.PostBodySecretParser:Debug: Start parsing for secret in post body
IdentityServer4.Validation.SecretParser:Debug: Parser found secret: PostBodySecretParser
IdentityServer4.Validation.SecretParser:Debug: Secret id found: api1
IdentityServer4.Validation.HashedSharedSecretValidator:Debug: No shared secret configured for client.
IdentityServer4.Validation.SecretValidator:Debug: Secret validators could not validate secret
IdentityServer4.Validation.ApiSecretValidator:Error: API validation failed.
IdentityServer4.Endpoints.IntrospectionEndpoint:Error: API unauthorized to call introspection endpoint. aborting.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 57.8551ms 401
IdentityModel.AspNetCore.OAuth2Introspection.OAuth2IntrospectionHandler:Error: Error returned from introspection endpoint: Unauthorized
IdentityModel.AspNetCore.OAuth2Introspection.OAuth2IntrospectionHandler:Information: BearerIdentityServerAuthenticationIntrospection was not authenticated. Failure message: Error returned from introspection endpoint: Unauthorized
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Information: Bearer was not authenticated. Failure message: Error returned from introspection endpoint: Unauthorized
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Information: Bearer was not authenticated. Failure message: Error returned from introspection endpoint: Unauthorized
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Information: Bearer was not authenticated. Failure message: Error returned from introspection endpoint: Unauthorized
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executing endpoint 'QrApi.Controllers.UserController.SearchUsersAsync (QrApi)'
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "SearchUsersAsync", controller = "User"}. Executing action QrApi.Controllers.UserController.SearchUsersAsync (QrApi)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
IdentityModel.AspNetCore.OAuth2Introspection.OAuth2IntrospectionHandler:Information: AuthenticationScheme: BearerIdentityServerAuthenticationIntrospection was challenged.
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler:Information: AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action QrApi.Controllers.UserController.SearchUsersAsync (QrApi) in 10.8603ms
Microsoft.AspNetCore.Routing.EndpointMiddleware:Information: Executed endpoint 'QrApi.Controllers.UserController.SearchUsersAsync (QrApi)'
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 135.7991ms 401
I have found tutorials about reference token, but none of them help me to solve this case.
What am I missing ?
Thank you,

Seems to be my configuration is invalid for API Resource.
This is my original setting for API Resources:
protected static IEnumerable<ApiResource> LoadInMemoryApiResources()
{
var apiResources = new List<ApiResource>();
var apiResource = new ApiResource("api1", "My API");
apiResource.UserClaims = new[]
{
"age"
};
apiResources.Add(apiResource);
return apiResources;
}
After having added the shared secret key which has been defined in client.ClientSecrets = new[] {new Secret("web-api-secret".Sha256())}; to apiResource:
protected static IEnumerable<ApiResource> LoadInMemoryApiResources()
{
//...
var apiResource = new ApiResource("api1", "My API");
api1Resource.ApiSecrets.Add(new Secret("web-api-secret".Sha256()));
//...
}
I could make request to protected resources successfully.
Hope this helps someone who is struggling with IdentityServer4 as I did.

My solution was to add the secret when instantiating the API resource.
protected static IEnumerable<ApiResource> LoadInMemoryApiResources()
{
var apiResources = new List<ApiResource>();
var apiResource = new ApiResource("api1", "My API"){
ApiSecrets = new List<Secret>{
new Secret("web-api-secret".Sha256())
},
Scopes = {
new Scope("openid")
}
};
apiResources.Add(apiResource);
return apiResources;
}

It looks like the issue could be that you don't have an API secret configured. In your config file change the API resource to match the configuration below. I believe to communicate with the introspection endpoint the api secret is required.
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
ApiSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
}
};

Related

Configure OpenAPI/Swagger to get access_token from Azure AD with client credentials flow

We are trying to configure swagger in our .NET 6 API project so that it automatically retrieves the access_token from Azure token endpoint with "client credentials flow". Here is the configuration part in startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "register_api", Version = "v1" });
c.SchemaFilter<EnumSchemaFilter>();
var jwtSecurityScheme = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Scheme = "bearer",
BearerFormat = "JWT",
Flows = new OpenApiOAuthFlows
{
ClientCredentials = new OpenApiOAuthFlow
{
TokenUrl = new Uri(#"https://login.microsoftonline.com/512024a4-8685-4f03-8086-14a61730e818/oauth2/v2.0/token"),
Scopes = new Dictionary<string, string>() { { #"api://e92b626c-f5e7-422b-a8b2-fd073b68b4a1/.default", ".default" } }
}
}
};
c.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, jwtSecurityScheme);
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ jwtSecurityScheme, new string[] { #"api://e92b626c-f5e7-422b-a8b2-fd073b68b4a1/.default" } }
});
}
It looks as follows when the user clicks the "Authorize" button the first time. But then, after entering the client_id and client_secret and clicking Authorize button, it shows up the message "Auth Error TypeError: Failed to fetch"
There is something weird with the request that is sent to the token endpoint. The payload includes just the grant_type and the scope. But the client_id and client_secret are base64 encoded and sent in Authorization header:
Is it the reason that the Azure token endpoint refuses to generate the access_token? I have used the same token endpoint and succeeded to get token with postman, but I included all the parameters in the payload.
If that is the case, is it possible to change the configuration of Swagger so that client_id and client_secret are sent in the payload instead (together with the grant_type and the scope) ?

how can I add an error_response when identityserver sends unauthorized 401 message

I am using identityserver4 to secure my apis.
When the token expires, in addition to sending error message 401, how can I send a json object in the body with the error description? I am using .net core.
There is actually a spec for how to do this:
https://www.rfc-editor.org/rfc/rfc6750#section-3
You can provide additional info in the WWW-Authenticate response header.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",error="invalid_token",error_description="The access token expired"
It should be possible to replace this header if token validation fails via the middleware JwtBearerEvents object.
As other response mentioned this is possible by setting WWW-Authenticate header in Unauthorized response. Here is code on API's startup.cs to make this happen:
services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
options =>
{
options.Authority = "http://localhost:5000";
options.Audience = "api1";
options.RequireHttpsMetadata = false;
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = OnAuthenticationFailed
};
});
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
if (context.Exception is SecurityTokenExpiredException expiredException)
{
context.Response.Headers.TryAdd(HeaderNames.WWWAuthenticate,
new StringValues(new[] {
JwtBearerDefaults.AuthenticationScheme,
"error=\"invalid_token\"",
"error_description=\"The access token expired\""
}));
}
return Task.CompletedTask;
}
The JWT handler is from Microsoft and IdentityServer using it internally.

IdentityServer4: Quickstart always returns Unauthorized for API endpoint, with message "The audience 'https://localhost:5001/resources' is invalid"

Following the instructions at https://docs.identityserver.io/en/latest/quickstarts/1_client_credentials.html verbatim, I am unable to execute a client. Always returns a 401 Unauthorized.
The token generated is:
{
"alg": "RS256",
"kid": "57EDAEBEC68F3CAACE869E3FA226C0FF",
"typ": "at+jwt"
}.{
"nbf": 1593466354,
"exp": 1593469954,
"iss": "https://localhost:5001",
"aud": "https://localhost:5001/resources",
"client_id": "client",
"jti": "C76BC9CB471ED81832A56B78059421FB",
"iat": 1593466354,
"scope": [
"api1"
]
}.[Signature]
But I see no way to set the audience. :s
My Api
Startup.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace api
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
// Use our IS4 implementation as the authentication source.
options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScopePolicy", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
MyEndpointController
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers
{
[Route("MyEndpoint")]
[Authorize]
public class MyEndpointController : ControllerBase
{
[HttpGet]
[Route("Get")]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
}
My Identity Server implementation
Startup.cs
public class Startup
{
public IWebHostEnvironment Environment { get; }
public Startup(IWebHostEnvironment environment)
{
Environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
// uncomment, if you want to add an MVC-based UI
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddJwtBearerClientAuthentication()
;
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// uncomment if you want to add MVC
app.UseStaticFiles();
app.UseRouting();
//-----------------------------------
app.UseIdentityServer();
// uncomment, if you want to add MVC
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
//-----------------------------------
}
}
Config.cs
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId()
};
public static IEnumerable<ApiResource> Apis =>
new List<ApiResource>
{
new ApiResource("api1", "My API")
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope() {
Description = "An example scope",
DisplayName = "api1",
Enabled = true,
Name = "api1",
ShowInDiscoveryDocument = true,
UserClaims = new string[] {"UserClaim1", "UserClaim2"}
}
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client() {
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
}
And my console program:
program.cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Newtonsoft.Json.Linq;
namespace ClientConsoleApp
{
class Program
{
static async Task Main(string[] args)
{
// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
Console.WriteLine("============================================================================================");
Console.WriteLine("Discovery Document:");
Console.WriteLine("============================================================================================");
Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(disco));
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine("============================================================================================");
Console.WriteLine("Token Response:");
Console.WriteLine("============================================================================================");
Console.WriteLine(tokenResponse.Json);
const string endpoint = "https://localhost:6001/MyEndpoint/Get";
Console.WriteLine("============================================================================================");
Console.WriteLine($"Calling api endpoint {endpoint}");
Console.WriteLine("============================================================================================");
// call api
var apiClient = new HttpClient(new LoggingHandler(new HttpClientHandler()));
apiClient.SetBearerToken(tokenResponse.AccessToken);
Console.WriteLine("============================================================================================");
Console.WriteLine("Request");
Console.WriteLine("============================================================================================");
var response = await apiClient.GetAsync(endpoint);
Console.WriteLine("============================================================================================");
Console.WriteLine("Response");
Console.WriteLine("============================================================================================");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}
}
}
The output is:
============================================================================================
Discovery Document:
============================================================================================
{"Policy":{"LoopbackAddresses":["localhost","127.0.0.1"],"Authority":"https://localhost:5001","AuthorityValidationStrategy":{},"RequireHttps":true,"AllowHttpOnLoopback":true,"ValidateIssuerName":true,"ValidateEndpoints":true,"EndpointValidationExcludeList":[],"AdditionalEndpointBaseAddresses":[],"RequireKeySet":true},"KeySet":{"Keys":[{"alg":"RS256","e":"AQAB","key_ops":[],"kid":"57EDAEBEC68F3CAACE869E3FA226C0FF","kty":"RSA","n":"oFo6iB0Kd-wzEFeR-fY12_8cF2uirsHI5FAtTAAOlAWUm5MRIPJjpXy8D4R9ZjU5750JUqcotQii8YF4DP_lN8Ro3SKFtI9HD4IazsX65ici2hhKSdAl4MEdUBRIgEdCwolQJgDOAhqls6WNqLRsh1Ify0EKI9AVKInwTbEXgCaHSsqGw8zubx8fSdQ4lgxQZGii792XYPVhFXMoom-6dVY9_7z5o5Or2sATdqaEAuLPLZLqMNVT284S9vMd4hxolIxVbuRgKQV4MZ-1mBK_C-GqjishVxdew6d_GasmRAt_2s0R4JlgZgeqzd7U2Agu5RETxpv6WUiDC9qCZnmXjQ","use":"sig","x5c":[],"KeySize":2048,"HasPrivateKey":false}]},"Issuer":"https://localhost:5001","AuthorizeEndpoint":"https://localhost:5001/connect/authorize","TokenEndpoint":"https://localhost:5001/connect/token","UserInfoEndpoint":"https://localhost:5001/connect/userinfo","IntrospectionEndpoint":"https://localhost:5001/connect/introspect","RevocationEndpoint":"https://localhost:5001/connect/revocation","DeviceAuthorizationEndpoint":"https://localhost:5001/connect/deviceauthorization","JwksUri":"https://localhost:5001/.well-known/openid-configuration/jwks","EndSessionEndpoint":"https://localhost:5001/connect/endsession","CheckSessionIframe":"https://localhost:5001/connect/checksession","RegistrationEndpoint":null,"FrontChannelLogoutSupported":true,"FrontChannelLogoutSessionSupported":true,"GrantTypesSupported":["authorization_code","client_credentials","refresh_token","implicit","urn:ietf:params:oauth:grant-type:device_code"],"CodeChallengeMethodsSupported":["plain","S256"],"ScopesSupported":["openid","api1","offline_access"],"SubjectTypesSupported":["public"],"ResponseModesSupported":["form_post","query","fragment"],"ResponseTypesSupported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"ClaimsSupported":["sub","UserClaim1","UserClaim2"],"TokenEndpointAuthenticationMethodsSupported":["client_secret_basic","client_secret_post","private_key_jwt"],"HttpResponse":{"Version":"1.1","Content":{"Headers":[{"Key":"Content-Type","Value":["application/json; charset=UTF-8"]}]},"StatusCode":200,"ReasonPhrase":"OK","Headers":[{"Key":"Date","Value":["Mon, 29 Jun 2020 20:14:49 GMT"]},{"Key":"Server","Value":["Kestrel"]},{"Key":"Transfer-Encoding","Value":["chunked"]}],"TrailingHeaders":[],"RequestMessage":{"Address":"https://localhost:5001","ClientId":null,"ClientSecret":null,"ClientAssertion":{"Type":null,"Value":null},"ClientCredentialStyle":1,"AuthorizationHeaderStyle":0,"Parameters":{},"Version":"1.1","Content":null,"Method":{"Method":"GET"},"RequestUri":"https://localhost:5001/.well-known/openid-configuration","Headers":[{"Key":"Accept","Value":["application/json"]}],"Properties":{}},"IsSuccessStatusCode":true},"Raw":"{\"issuer\":\"https://localhost:5001\",\"jwks_uri\":\"https://localhost:5001/.well-known/openid-configuration/jwks\",\"authorization_endpoint\":\"https://localhost:5001/connect/authorize\",\"token_endpoint\":\"https://localhost:5001/connect/token\",\"userinfo_endpoint\":\"https://localhost:5001/connect/userinfo\",\"end_session_endpoint\":\"https://localhost:5001/connect/endsession\",\"check_session_iframe\":\"https://localhost:5001/connect/checksession\",\"revocation_endpoint\":\"https://localhost:5001/connect/revocation\",\"introspection_endpoint\":\"https://localhost:5001/connect/introspect\",\"device_authorization_endpoint\":\"https://localhost:5001/connect/deviceauthorization\",\"frontchannel_logout_supported\":true,\"frontchannel_logout_session_supported\":true,\"backchannel_logout_supported\":true,\"backchannel_logout_session_supported\":true,\"scopes_supported\":[\"openid\",\"api1\",\"offline_access\"],\"claims_supported\":[\"sub\",\"UserClaim1\",\"UserClaim2\"],\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\",\"implicit\",\"urn:ietf:params:oauth:grant-type:device_code\"],\"response_types_supported\":[\"code\",\"token\",\"id_token\",\"id_token token\",\"code id_token\",\"code token\",\"code id_token token\"],\"response_modes_supported\":[\"form_post\",\"query\",\"fragment\"],\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"private_key_jwt\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"subject_types_supported\":[\"public\"],\"code_challenge_methods_supported\":[\"plain\",\"S256\"],\"request_parameter_supported\":true}","Json":{"issuer":"https://localhost:5001","jwks_uri":"https://localhost:5001/.well-known/openid-configuration/jwks","authorization_endpoint":"https://localhost:5001/connect/authorize","token_endpoint":"https://localhost:5001/connect/token","userinfo_endpoint":"https://localhost:5001/connect/userinfo","end_session_endpoint":"https://localhost:5001/connect/endsession","check_session_iframe":"https://localhost:5001/connect/checksession","revocation_endpoint":"https://localhost:5001/connect/revocation","introspection_endpoint":"https://localhost:5001/connect/introspect","device_authorization_endpoint":"https://localhost:5001/connect/deviceauthorization","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["openid","api1","offline_access"],"claims_supported":["sub","UserClaim1","UserClaim2"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","urn:ietf:params:oauth:grant-type:device_code"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","private_key_jwt"],"id_token_signing_alg_values_supported":["RS256"],"subject_types_supported":["public"],"code_challenge_methods_supported":["plain","S256"],"request_parameter_supported":true},"Exception":null,"IsError":false,"ErrorType":0,"HttpStatusCode":200,"HttpErrorReason":"OK","Error":null}
============================================================================================
Token Response:
============================================================================================
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3RURBRUJFQzY4RjNDQUFDRTg2OUUzRkEyMjZDMEZGIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE1OTM0NjE2OTAsImV4cCI6MTU5MzQ2NTI5MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoiY2xpZW50IiwianRpIjoiNEY5QzczMDZBRjdFMURDNjI3QkRBQTdCRjg4MjlDNTMiLCJpYXQiOjE1OTM0NjE2OTAsInNjb3BlIjpbImFwaTEiXX0.FxvjG89zv1a83MtyjJvzCA26g_VLO6HTElJuOSi1FOp_My1RGHB-mbg53E6jZF9Xq_pkAOak5SC73tMC0b3hcEGx9O1qsd9c_Q9ish2ffmCZZ34svkpsfZp3wjbS-xNyxq7mjSOg0JGpf3ML_eUz3TUcOa5Aba_evzmRDaVgAvEtsdM8D7lK_udnQmw0cDimc8vYaGSLIXJDfOhM9pb-8I67deElCxaIEG93CwRZV5bwQQQC3dLwihb51wndv962Kw0dPkIXrt1n7jwEQ4KAhBqVcP9DAgPTqem1Kix8Uq_P4wBTm_cMY7U7bCa-j6mvRZ8t7TxWARpylzlL-ojy7g",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "api1"
}
============================================================================================
Calling api endpoint https://localhost:6001/MyEndpoint/Get
============================================================================================
============================================================================================
Request
============================================================================================
Request:
Method: GET, RequestUri: 'https://localhost:6001/MyEndpoint/Get', Version: 1.1, Content: <null>, Headers:
{
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjU3RURBRUJFQzY4RjNDQUFDRTg2OUUzRkEyMjZDMEZGIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE1OTM0NjE2OTAsImV4cCI6MTU5MzQ2NTI5MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoiY2xpZW50IiwianRpIjoiNEY5QzczMDZBRjdFMURDNjI3QkRBQTdCRjg4MjlDNTMiLCJpYXQiOjE1OTM0NjE2OTAsInNjb3BlIjpbImFwaTEiXX0.FxvjG89zv1a83MtyjJvzCA26g_VLO6HTElJuOSi1FOp_My1RGHB-mbg53E6jZF9Xq_pkAOak5SC73tMC0b3hcEGx9O1qsd9c_Q9ish2ffmCZZ34svkpsfZp3wjbS-xNyxq7mjSOg0JGpf3ML_eUz3TUcOa5Aba_evzmRDaVgAvEtsdM8D7lK_udnQmw0cDimc8vYaGSLIXJDfOhM9pb-8I67deElCxaIEG93CwRZV5bwQQQC3dLwihb51wndv962Kw0dPkIXrt1n7jwEQ4KAhBqVcP9DAgPTqem1Kix8Uq_P4wBTm_cMY7U7bCa-j6mvRZ8t7TxWARpylzlL-ojy7g
}
Response:
StatusCode: 401, ReasonPhrase: 'Unauthorized', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
{
Date: Mon, 29 Jun 2020 20:14:50 GMT
Server: Kestrel
WWW-Authenticate: Bearer error="invalid_token", error_description="The audience 'https://localhost:5001/resources' is invalid"
Content-Length: 0
}
============================================================================================
Response
============================================================================================
Unauthorized
IdentityServer output:
[14:05:23 Information]
Starting host...
[14:05:24 Information] IdentityServer4.Startup
Starting IdentityServer4 version 4.0.0+1acafade44176bf817412aa4309d5dff6587a741
[14:05:24 Information] IdentityServer4.Startup
You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation.
[14:05:24 Information] IdentityServer4.Startup
Using the default authentication scheme idsrv for IdentityServer
[14:05:24 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for authentication
[14:05:24 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for sign-in
[14:05:24 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for sign-out
[14:05:24 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for challenge
[14:05:24 Debug] IdentityServer4.Startup
Using idsrv as default ASP.NET Core scheme for forbid
[14:05:24 Information] Microsoft.Hosting.Lifetime
Now listening on: https://localhost:5001
[14:05:24 Information] Microsoft.Hosting.Lifetime
Application started. Press Ctrl+C to shut down.
[14:05:24 Information] Microsoft.Hosting.Lifetime
Hosting environment: Development
[14:05:24 Information] Microsoft.Hosting.Lifetime
Content root path: C:\Source\Repos\IdentityServer\code\IdentityServer
[14:05:26 Debug] IdentityServer4.Startup
Login Url: /Account/Login
[14:05:26 Debug] IdentityServer4.Startup
Login Return Url Parameter: ReturnUrl
[14:05:26 Debug] IdentityServer4.Startup
Logout Url: /Account/Logout
[14:05:26 Debug] IdentityServer4.Startup
ConsentUrl Url: /consent
[14:05:26 Debug] IdentityServer4.Startup
Consent Return Url Parameter: returnUrl
[14:05:26 Debug] IdentityServer4.Startup
Error Url: /home/error
[14:05:26 Debug] IdentityServer4.Startup
Error Id Parameter: errorId
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /.well-known/openid-configuration matched to endpoint type Discovery
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryEndpoint
[14:05:33 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
[14:05:33 Debug] IdentityServer4.Endpoints.DiscoveryEndpoint
Start discovery request
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /.well-known/openid-configuration/jwks matched to endpoint type Discovery
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryKeyEndpoint
[14:05:33 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryKeyEndpoint for /.well-known/openid-configuration/jwks
[14:05:33 Debug] IdentityServer4.Endpoints.DiscoveryKeyEndpoint
Start key discovery request
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/token matched to endpoint type Token
[14:05:33 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Token, successfully created handler: IdentityServer4.Endpoints.TokenEndpoint
[14:05:33 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
[14:05:33 Debug] IdentityServer4.Endpoints.TokenEndpoint
Start token request.
[14:05:33 Debug] IdentityServer4.Validation.ClientSecretValidator
Start client validation
[14:05:33 Debug] IdentityServer4.Validation.BasicAuthenticationSecretParser
Start parsing Basic Authentication secret
[14:05:33 Debug] IdentityServer4.Validation.PostBodySecretParser
Start parsing for secret in post body
[14:05:33 Debug] IdentityServer4.Validation.ISecretsListParser
Parser found secret: PostBodySecretParser
[14:05:33 Debug] IdentityServer4.Validation.ISecretsListParser
Secret id found: client
[14:05:33 Debug] IdentityServer4.Stores.ValidatingClientStore
client configuration validation for client client succeeded.
[14:05:33 Debug] IdentityServer4.Validation.ISecretsListValidator
Secret validator success: HashedSharedSecretValidator
[14:05:33 Debug] IdentityServer4.Validation.ClientSecretValidator
Client validation success
[14:05:33 Debug] IdentityServer4.Validation.TokenRequestValidator
Start token request validation
[14:05:33 Debug] IdentityServer4.Validation.TokenRequestValidator
Start client credentials token request validation
[14:05:33 Debug] IdentityServer4.Validation.TokenRequestValidator
client credentials token request validation success
[14:05:33 Information] IdentityServer4.Validation.TokenRequestValidator
Token request validation success, {"ClientId": "client", "ClientName": null, "GrantType": "client_credentials", "Scopes": "api1", "AuthorizationCode": null, "RefreshToken": null, "UserName": null, "AuthenticationContextReferenceClasses": null, "Tenant": null, "IdP": null, "Raw": {"grant_type": "client_credentials", "scope": "api1", "client_id": "client", "client_secret": "***REDACTED***"}, "$type": "TokenRequestValidationLog"}
[14:05:33 Debug] IdentityServer4.Services.DefaultClaimsService
Getting claims for access token for client: client
[14:05:33 Debug] IdentityServer4.Endpoints.TokenEndpoint
Token request success.
[14:05:34 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /.well-known/openid-configuration matched to endpoint type Discovery
[14:05:34 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryEndpoint
[14:05:34 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
[14:05:34 Debug] IdentityServer4.Endpoints.DiscoveryEndpoint
Start discovery request
[14:05:34 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /.well-known/openid-configuration/jwks matched to endpoint type Discovery
[14:05:34 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryKeyEndpoint
[14:05:34 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryKeyEndpoint for /.well-known/openid-configuration/jwks
[14:05:34 Debug] IdentityServer4.Endpoints.DiscoveryKeyEndpoint
Start key discovery request
Ok, I found the way to fix this error but it opens up other issues.
First, looked at the generated JWT token. It shows Audience as "https://localhost:5001/resources". So I scanned the code for where the audience validation is done, which is in the API that authenticates the caller via IS4's Startup.cs file. Changed the audience value to the expected value, and it worked:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
// Use our IS4 implementation as the authentication source.
options.Authority = "https://localhost:5001";
options.RequireHttpsMetadata = false;
//options.Audience = "api1";
options.Audience = "https://localhost:5001/resources";
});
This does not, however, tell me how that value was generated or where to override it, but does fix the issue.
With What your current settings on API, you need is an aud as api1 in the access_token. To fix just add the scopes to the API resource. Verify the generated token on https://jwt.ms/
public static IEnumerable<ApiResource> Apis =>
new List<ApiResource>
{
new ApiResource("api1", "My API")
{
Scopes = { "api1"}
}
};
and revert back your API settings to have options.Audience = "api1";.
Read more about API resource here
I had the same issue and found how to fix it. In Identity server 4 configuration you must add scope with name which you want to use.
Here is the example how to declare an api.
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
new ApiResource("adminApi", "Admin Panel Service")
{
Scopes = {
"adminApi"
}
}
};
And add this to your configuration.
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>()
.AddJwtBearerClientAuthentication();
As far as I understood the documentation, the value "'https://localhost:5001/resources" is generated when you use options.EmitStaticAudienceClaim = true;.
If you need an aud claim, you can enable the EmitStaticAudience
setting on the options. This will emit an aud claim in the
issuer_name/resources format. If you need more control of the aud
claim, use API resources.
I found it here:
Authorization based on Scopes
To have "api1" in your "aud" section of the token you need to follow nahidf's and Vesko I's suggestions:
In you Config.cs File add an ApiResource:
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("api1", "Test API")
{
Scopes = { "api1.read", "api1.write" }
}
};
and in you Startup.cs add that resource to you ApiResources registration:
.AddInMemoryApiResources(Config.ApiResources)
After doing so, Identity Server will create tokens that contain this:
"aud": [
"api1",
"https://localhost:44300/resources"
],

Redirect to Identity Server Login page from AngularJs http web api request

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

SignalR authentication failed when passing "Bearer" through query string

I'd like to enable authentication in SignalR while the server was hosted in ASP.NET WebAPI which I'm using OAuth Bearer authrntication and the client is AngularJS.
On client side I originally pass the Bearer token through HTTP header and it works well with the WebAPI. But since SignalR JavsScript doesn't support adding HTTP headers in connection (it's because WebSocket doesn't support specifying HTTP headers) I need to pass the Bearer token through query string by using the code like self.connection.qs = { Bearer: 'xxxxxx' };
The problem is on the WebAPI side my SignalR always returned 401 Unauthorized.
Below is what I did on the WebAPI side.
1, I specified OAuthBearerAuthenticationOptions.Provider to QueryStringEnabledOAuthBearerAuthenticationProvider, which is a class I created inherited from OAuthBearerAuthenticationProvider that can retrieve Bearer token from query string. Code as below.
public class QueryStringEnabledOAuthBearerAuthenticationProvider : OAuthBearerAuthenticationProvider
{
private readonly string _name;
public QueryStringEnabledOAuthBearerAuthenticationProvider()
: this(OAuthDefaults.AuthenticationType)
{
}
public QueryStringEnabledOAuthBearerAuthenticationProvider(string name)
{
_name = name;
}
public override Task RequestToken(OAuthRequestTokenContext context)
{
// try to read token from base class (header) if possible
base.RequestToken(context).Wait();
if (string.IsNullOrWhiteSpace(context.Token))
{
// try to read token from query string
var token = context.Request.Query.Get(_name);
if (!string.IsNullOrWhiteSpace(token))
{
context.Token = token;
}
}
return Task.FromResult(null);
}
}
And registered it as below while WebAPI was started.
var options = new OAuthBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = AuthenticationType,
Provider = new QueryStringEnabledOAuthBearerAuthenticationProvider(),
AccessTokenFormat = _accessTokenFormat,
};
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
app.UseOAuthBearerAuthentication(options);
2, In SignalR part I created an authorize attribute as below. Nothing changed just to be used to add break point.
public class BearerAuthorizeAttribute : AuthorizeAttribute
{
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
return base.AuthorizeHubConnection(hubDescriptor, request);
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
return base.AuthorizeHubMethodInvocation(hubIncomingInvokerContext, appliesToMethod);
}
}
And registered it when WebAPI started as well.
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
// EnableJSONP = true
EnableJavaScriptProxies = false
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
// Require authentication for all hubs
var authorizer = new BearerAuthorizeAttribute();
var module = new AuthorizeModule(authorizer, authorizer);
GlobalHost.HubPipeline.AddModule(module);
});
I found, when SignalR connected my QueryStringEnabledOAuthBearerAuthenticationProvider.RequestToken was invoked and it retrieved Bearer token successfully. But then when SignalR BearerAuthorizeAttribute.AuthorizeHubConnection was invoked the parameter request.User still not authenticated. So it returned 401.
Can anyone give me some ideas on what's wrong I did, thanks.
I'm using headers, this is how I solved it
var authData = localStorageService.get('authorizationData');
var token = authData.token;
$.signalR.ajaxDefaults.headers = { Authorization: "Bearer " + token };
Hope it helps
I resolved this problem by unprotect the Bearer token from query string in my AuthorizeAttribute, and set the user principal into a new ServerRequest. For detailed information please check http://blog.shaunxu.me/archive/2014/05/27/set-context-user-principal-for-customized-authentication-in-signalr.aspx
This might not be the best solution but it worked.

Resources