Setting up IdentityServer wtih Asp.Net MVC Application
Asked Answered
P

1

7

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.

Preamplifier answered 11/6, 2015 at 3:18 Comment(2)
Can you add scopes.cs and clients.cs to the question? The insufficiet_scope error means "The request requires higher privileges than provided by the access token."Subdivision
Hi, I added the classes you requested. Maybe the scope type is wrong?Preamplifier
S
3

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,
Subdivision answered 12/6, 2015 at 0:10 Comment(9)
Hi, thanks for your answer. I actually made a mistake in copying the Scope and Client. I have updated the post now. Sorry about that.Preamplifier
baseballStatsApi scope is required by your api, but your application() doesn't request that scope, can you please check modifying Scope = "openid profile roles baseballStatsApi",Subdivision
Still no luck. I have made the changes you suggested and updated the original post to reflect the state of the code. I still get the same error.Preamplifier
Is there a way I can get the full source of your sample?. If you can share I can have a look.Subdivision
Can I send you a zip file somehow?Preamplifier
I figured out why it's not working. If I remove UseIdentityServerBearerTokenAuthentication section from the startup.cs file, then it works correctly. I'm just not sure what to do to get bearer token authentication/authorization to work. I think I need to add a claim to the user, just not sure which one.Preamplifier
Glad you some how get it running and thanks for voting up. But if you remove UseIdentityServerBearerTokenAuthentication your api is no longer secured with tokens, but I believe, as you are hosting api and application in same host is is protected via cookie authentication. I can suggest you two things to get it working with token authentication. 1. Use separate host for the WebAPI, 2. Use owin-katana app.map to host the api in separate url in the same host,Subdivision
yes, if you like please send me the zip or link to the zip to [email protected].Subdivision
I think the combination of your initial answer and subsequent comments are enough to answer the question I initially posed. I have come to the same conclusion as you. However, I think I can also achieve what I'm trying to do if I add a bearer token to the Http request headers via the Owin middleware. I'm going to try that, and if I can't get it to work, I will consider hosting the API in a separate app. Thanks for your help!Preamplifier

© 2022 - 2024 — McMap. All rights reserved.