I apologize in advance for asking this as I have next to no knowledge of security in general and IdentityServer in particular.
I am trying to set up IdentityServer to manage security for an Asp.Net MVC application.
I am following the tutorial on their website: Asp.Net MVC with IdentityServer
However, I am doing something slightly different in that I have a separate project for the Identity "Server" part, which leads to 2 Startup.cs files, one for the application and one for the Identity Server
For the application, the Startup.cs file looks like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(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,
Constants.ClaimTypes.GivenName,
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);
}
}
});
app.UseResourceAuthorization(new AuthorizationManager());
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi"}
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
For the identity server, the startup.cs file is
public class Startup
{
public void Configuration(IAppBuilder app)
{
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");
}
}
I am also setting up an Authorization Manager
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "Players":
return CheckAuthorization(context);
case "About":
return CheckAuthorization(context);
default:
return Nok();
}
}
private Task<bool> CheckAuthorization(ResourceAuthorizationContext context)
{
switch(context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "LevelOneSubscriber"));
default:
return Nok();
}
}
}
So for instance, if I define a controller method that is decorated with the ResourceAuthorize attribute, like so
public class HomeController : Controller
{
[ResourceAuthorize("Read", "About")]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
}
Then, when I first try to access this method, I will be redirected to the default login page.
What I don't understand however, is why when I login with the user I have defined for the application (see below),
public class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "LevelOneSubscriber")
}
}
};
}
}
I get a 403 error, Bearer error="insufficient_scope".
Can anybody explain what I am doing wrong?
Any subsequent attempt to access the action method will return the same error. It seems to me that the user I defined has the correct claims to be able to access this method. However, the claims check only happens once, when I first try to access this method. After I login I get a cookie, and the claims check is not made during subsequent attempts to access the method.
I'm a bit lost, and would appreciate some help in clearing this up.
Thanks in advance.
EDIT: here are the scoles and client classes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
Name = "baseballStatsApi",
Description = "Access to baseball stats API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
And the Client class
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "Baseball Stats Emporium",
ClientId = "baseballStats",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44300/"
}
},
new Client
{
Enabled = true,
ClientName = "Baseball Stats API Client",
ClientId = "baseballStats_Api",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("secret".Sha256())
},
Flow = Flows.ClientCredentials
}
};
}
}
I have also created a custom filter attribute which I use to determine when the claims check is made.
public class CustomFilterAttribute : ResourceAuthorizeAttribute
{
public CustomFilterAttribute(string action, params string[] resources) : base(action, resources)
{
}
protected override bool CheckAccess(HttpContextBase httpContext, string action, params string[] resources)
{
return base.CheckAccess(httpContext, action, resources);
}
}
The breakpoint is hit only on the initial request to the url. On subsequent requests, the filter attribute breakpoint is not hit, and thus no check occurs. This is surprising to me as I assumed the check would have to be made everytime the url is requested.
Identity in MVC 5 Identity is a secured way of authentication methods in web applications. It is used for identifying the authorized user. Background. There are different ways of creating an Identity in applications, but this article explains how to create it using OWIN in ASP.NET MVC.
Creating the quickstart IdentityServerStart by creating a new ASP.NET Core project. Then select the “Empty Web” option. IdentityServer currently only targets ASP.NET Core 1.1. IdentityServer uses the usual pattern to configure and add services to an ASP.NET Core host.
You need to request the scopes required by the api when the user logs in.
Scope = "openid profile roles baseballStatsApi"
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
ResponseType = "id_token token",
RedirectUri = "https://localhost:44300/",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With