I'm getting a 401 error when I try to access a resource from an API protected by IdentityServer3.
I can log in and get the access_token quietly from the Host application of IdentityServer3, but I cannot use the access_token to consume this resource.
I configured my Host of IdentityServer in Startup class like this:
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Trace()
.CreateLogger();
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// Configure IdentityServer3
app.Map("/identity", configuration =>
{
configuration.UseIdentityServer(new IdentityServerOptions
{
SiteName = "IdentityServer3 Sample",
SigningCertificate = LoadCertificate(),
Factory = ServiceFactory.Create(),
RequireSsl = true,
CspOptions = new CspOptions
{
Enabled = true,
FontSrc = "fonts.googleapis.com"
},
AuthenticationOptions = new AuthenticationOptions
{
EnablePostSignOutAutoRedirect = true,
}
});
});
}
In my ServiceFactory class, I have:
public static IdentityServerServiceFactory Create()
{
var factory = new IdentityServerServiceFactory
{
ScopeStore = new Registration<IScopeStore>(
new InMemoryScopeStore(Scopes.GetScopes())),
ClientStore = new Registration<IClientStore>(
new InMemoryClientStore(Clients.GetClients())),
CorsPolicyService = new Registration<ICorsPolicyService>(
new DefaultCorsPolicyService {AllowAll = true})
};
//factory.UseInMemoryUsers(Users.GetUsers());
ConfigureServices(factory);
return factory;
}
private static void ConfigureServices(IdentityServerServiceFactory factory)
{
factory.UserService = new Registration<IUserService, UserService>();
factory.Register(new Registration<BaseContext>(resolver => new BaseContext()));
factory.Register(new Registration<AppUserManager>(resolver => new AppUserManager(
new UserStore<User>(resolver.Resolve<BaseContext>()))));
}
The Scopes:
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.OfflineAccess,
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
Name = "ro",
Type = ScopeType.Resource,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
And Clients:
return new List<Client>
{
new Client
{
Enabled = true,
ClientName = "Hibrid Flow Client",
ClientId = AppIdentityConstants.ClientIdForHibridFlow,
Flow = Flows.Hybrid,
RequireConsent = false,
AccessTokenType = AccessTokenType.Reference,
UpdateAccessTokenClaimsOnRefresh = true,
ClientSecrets = new List<Secret>
{
new Secret(AppIdentityConstants.ClientSecret.Sha256())
},
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.Roles,
Constants.StandardScopes.OfflineAccess,
},
RedirectUris = new List<string>
{
AppIdentityConstants.IdentityAddress,
AppIdentityConstants.CRMAddress
},
PostLogoutRedirectUris = new List<string>
{
AppIdentityConstants.IdentityAddress,
AppIdentityConstants.CRMAddress
},
LogoutSessionRequired = true
},
new Client
{
Enabled = true,
ClientName = "Resource Owner Client",
ClientId = AppIdentityConstants.ClientIdForResourceOwnerFlow,
Flow = Flows.ResourceOwner,
RequireConsent = false,
AccessTokenType = AccessTokenType.Jwt,
UpdateAccessTokenClaimsOnRefresh = true,
AccessTokenLifetime = 3600,
ClientSecrets = new List<Secret>
{
new Secret(AppIdentityConstants.ClientSecret.Sha256())
},
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.Roles,
Constants.StandardScopes.OfflineAccess,
"ro"
},
AllowAccessTokensViaBrowser = true,
AbsoluteRefreshTokenLifetime = 86400,
SlidingRefreshTokenLifetime = 43200,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
RefreshTokenExpiration = TokenExpiration.Sliding
},
};
This is the source code for the Host Application of IdentityServer3.
And now I'll show you how I've set up my API. This is my Startup class:
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = AppIdentityConstants.IdentityBaseAddress,
RequiredScopes = new[] { "ro", "offline_access" },
ClientId = AppIdentityConstants.ClientIdForResourceOwnerFlow,
ClientSecret = AppIdentityConstants.ClientSecret,
});
}
The AppIdentityConstants.IdentityBaseAddress
is https://localhost:44342/identity
.
And, in Global.asax.cs I call these configurations:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Formatters.Remove(config.Formatters.XmlFormatter);
var formatters = GlobalConfiguration.Configuration.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
#if DEBUG
settings.Formatting = Formatting.Indented;
#endif
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
// Web API routes
config.MapHttpAttributeRoutes();
config.EnableCors(new EnableCorsAttribute("*", "*", "*"));
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
And AuthorizeAttribute filter:
public class FilterConfig
{
public static void RegisterGlobalFilters(HttpConfiguration configuration)
{
configuration.Filters.Add(new AuthorizeAttribute());
}
}
To test I did the following:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
<script>
function done(response) { console.log(response); }
function always(response) { console.log("always"); }
function fail(response) { console.log("fail"); }
var custom = {
client_id: "ro",
client_secret: "client_secret",
scope: "ro offline_access",
};
$(function () {
var settings = {
"async": true,
"crossDomain": true,
"url": "https://localhost:44342/identity/connect/token",
"method": "POST",
"headers": {
"content-type": "application/x-www-form-urlencoded",
"cache-control": "no-cache"
},
"data": {
"client_id": custom.client_id,
"client_secret": custom.client_secret,
"scope": custom.scope,
"username": "user@test.com",
"password": "123456",
"grant_type": "password"
}
}
$.ajax(settings).done(function (response){
done(response);
checkStatus(response.access_token);
}).always(always).fail(fail);
function checkStatus(access_token) {
var settings2 = {
"async": true,
"crossDomain": true,
"url": "https://localhost:44352/api/importer/status",
"method": "GET",
xhrFields: {
withCredentials: true
},
"headers": {
"Authorization": "Bearer " + access_token,
"cache-control": "no-cache"
}
}
$.ajax(settings2).done(done).always(always).fail(fail);
}
});
</script>
</body>
</html>
The first request, which is to obtain the access data, including the acess_token, is done successfully.
But the second request, which is made to the API, returns a 401 error.
And as I showed earlier, the API is protected with the AuthorizeAttribute.
What is wrong?