Search code examples
c#.netasp.net-core.net-coreduende-identity-server

IdentityServer - Configuring ApiResources


So I was following the quick starts from the latest IdentityServer/Duende official resources.

From what I understood, An Api Resource is essentially logical grouping of apiscopes and identityscopes. You can Have 3 ApiScopes "apiscope1", "apiscope2","apiscope3", wrapped in an ApiResource "api".

When setting the client, you need to specify "api" in scopes, which will automatically give you apiscope1, apiscope2, apiscope3.

1. Is this correct?

I have two Apis: MyApi and IdentityServerApi.

MyApi is configured as follows:

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        var idsvrConfig = builder.Configuration.GetSection("IdentityServer").Get<IdentityServerConfiguration>();

        options.Authority = idsvrConfig.Authority;
        options.ClientId = idsvrConfig.ClientId;
        options.ClientSecret = idsvrConfig.ClientSecret;
        options.ResponseType = idsvrConfig.ResponseType;
        options.Scope.Clear();
        //options.Scope.AddRange(idsvrConfig.Scopes);
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("verification");
        options.Scope.Add("apiResource"); //<-------Doesn't work when this is specified!!

        options.GetClaimsFromUserInfoEndpoint = idsvrConfig.GetClaimsFromUserInfoEndpoint;
        options.SaveTokens = idsvrConfig.SaveTokens;
        foreach (var claims in idsvrConfig.ClaimActionsMapJsonKey)
        {
            options.ClaimActions.MapJsonKey(claims.Key, claims.Value);
        }
    });

Now for the Identity Server API Configuration:

var apiResources = new List<ApiResource>()
{
    new ApiResource()
    {
        Name = "apiResource",
        DisplayName ="ApiResource",
        Scopes = new string[] {"test" }
    }
};


var apiScopes = new List<ApiScope>()
{
    new ApiScope()
    {
            Name = "test",
    }
};

var identityResources = new List<IdentityResource>
{
    new IdentityResources.OpenId(),
    new IdentityResources.Profile(),
    new IdentityResource()
    {
        Name = "verification",
        UserClaims = new List<string>
        {
            JwtClaimTypes.Email,
            JwtClaimTypes.EmailVerified
        }
    }
};


var clients = new List<Client>
{
    // interactive ASP.NET Core Web App
    new Client
    {
        ClientId = "api",
        ClientSecrets = { new Secret(){
            Value = "supersecretpass"
        }},

        AllowedGrantTypes = GrantTypes.Code,

        // where to redirect after login
        RedirectUris = { "https://localhost:44330/signin-oidc" },

        // where to redirect after logout
        PostLogoutRedirectUris = { "https://localhost:44330/signout-callback-oidc" },

        AllowedScopes = new List<string>
        {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "verification",
            "apiResource",
            "test"
        }
    }
};


// Add Identity Server services
builder.Services.AddIdentityServer()
    .AddInMemoryClients(clients)
    .AddInMemoryIdentityResources(identityResources)
    .AddInMemoryApiScopes(apiScopes)
    .AddInMemoryApiResources(apiResources)
    //.AddInMemoryClients(builder.Configuration.GetSection("IdentityServer:Clients"))
    //.AddInMemoryIdentityResources(builder.Configuration.GetSection("IdentityServer:IdentityResources"))
    //.AddInMemoryApiScopes(builder.Configuration.GetSection("IdentityServer:ApiScopes"))
    //.AddInMemoryApiResources(builder.Configuration.GetSection("IdentityServer:ApiResources"))
    .AddTestUsers(TestUsers.Users);

2. Why is it that when on MyApi I add the scope "apiResource", I get an invalid scope screen error but when I remove it from the scopes requested by the client the full login/logout flow works?

Any help/insight is appreciated.

Just in case the versions make any difference, I'm running the following packages:

Both Api are running .NET 7

For MyApi: Microsoft.AspNetCore.Authentication.OpenIdConnect 7.0.5

For Idsvr: Duende.IdentityServer 6.2.3


Solution

  • You set the Scope containing the apiResource field in the client, but did not configure it in the server, Invalid scope is caused because the Scope cannot match.

    You need to specify apiResource in AllowedScopes of Client in IdentityServerApi:

    new Client
    {
        ClientId = "api",
        ClientSecrets = { new Secret(){
            Value = "supersecretpass"
        }},
    
        AllowedGrantTypes = GrantTypes.Code,
    
        // where to redirect after login
        RedirectUris = { "https://localhost:44330/signin-oidc" },
    
        // where to redirect after logout
        PostLogoutRedirectUris = { "https://localhost:44330/signout-callback-oidc" },
    
        AllowedScopes = new List<string>
        {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "verification",
            "apiResource"
        }
    }
    

    Then you add the scope apiResource in MyApi, you should be able to successfully run.

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "<idsvr-url>";
    
        options.ClientId = "api";
        options.ClientSecret = "supersecretpass";
        options.ResponseType = "code";
    
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("verification");
        options.Scope.Add("apiResource");
        options.GetClaimsFromUserInfoEndpoint = true;
    
        options.SaveTokens = true;
    });
    

    For more details about ApiResource and ApiScope, you can refer to this link.

    Update:

    The scopes added in AllowedScopes should be defined. You only defined test in apiScopes, not apiResource.

    var apiScopes = new List<ApiScope>()
    {
        new ApiScope()
        {
            Name = "test",
        },
        //add this
        new ApiScope
        {
            Name = "apiResource"
        },
    };
    

    Regarding the "Aud" claim, you don't need to manually configure it. When you add an ApiResource and specify the corresponding Scope, when the client uses this Scope to make a request, the obtained Token will have the Aud claim by default.

    For example, I added two ApiResources and specified the corresponding Scope:

    new List<ApiResource>
    {
        new ApiResource()
        {
            Name = "apiResource",
            DisplayName ="ApiResource",
            Scopes = new string[] {"test" }
        },
        new ApiResource()
        { 
            Name = "paymentApi",
            DisplayName = "PaymentApi",
            Scopes= new string[] {"test2"}
        }
    };
    

    Then add the corresponding Scope in ApiScope:

    new List<ApiScope>
    {         
        new ApiScope()
        {
            Name = "test"
        },
        new ApiScope
        {
            Name = "apiResource"
        },
        new ApiScope
        { 
            Name = "test2"
        }
    };
    

    Configure Client in IdentityServerApi(I'm not sure whether your ClientSecrets can be verified, because I configured it according to the official document):

    new Client
    {
        ClientId = "web",
        ClientSecrets = { new Secret("secret".Sha256()) },
    
        AllowedGrantTypes = GrantTypes.Code,
                
        // where to redirect to after login
        RedirectUris = { "https://localhost:5002/signin-oidc" },
    
        // where to redirect to after logout
        PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
        AllowedScopes = new List<string>
         {        
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "apiResource",
            "test",
            "test2"
         }
    }
    

    In ClientApi:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://localhost:5001";
    
        options.ClientId = "web";
        options.ClientSecret = "secret";
        options.ResponseType = "code";
    
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("apiResource");
        options.Scope.Add("test");
        options.GetClaimsFromUserInfoEndpoint = true;
    
        options.SaveTokens = true;
    });
    

    I can see .Token.access_token after successfully logging in with the example built by referring to the official document: enter image description here

    Copy this access_token and paste in jwt.io, you can see that your ApiResource is included in the aud: enter image description here

    When I add a line in ClientApi:

    options.Scope.Add("test2");
    

    You can see both apiResource and paymentApi are included in aud: enter image description here