Setting up IdentityServer wtih Asp.Net MVC Application

6.8k views Asked by At

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.

1

There are 1 answers

9
rawel On BEST ANSWER

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,