0

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

Style
  • 67
  • 9

1 Answers1

2

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

Chen
  • 4,499
  • 1
  • 2
  • 9
  • I have updated the code to reflect your changes, but still not working. Thanks also for the link, I did read that actually prior to you referring to it, however it does not show a c# client connecting to idsvr using api resource. Furthermore the post mentions "Aud" claim, and it seems like I am supposed to add this during the request? That made me confused as no identity server documentation mention that I believe. Hence me asking questions 1 and 2 :D – Style May 09 '23 at 15:55
  • Hi @Style, I updated my answer, you can check it. – Chen May 10 '23 at 08:16
  • I think I had this totally the other way round. I was under the impression that if I have an api resource called "A", which has "b" and "c" as scopes, in my client I can then just send a list of scope with just A, and the resulting token would have scopes b and c automatically. Am I correct in saying that the actual request should be b and c, and then A shows up in the AUD? – Style May 10 '23 at 17:05
  • I have re read that link you sent in your original answer, and I think I finally understood the aim of api resources. The aim is that for example API A (which has corresponding API Resource A with scopes B and C), can request scopes B and C and in the access token the AUD claim is populated. API A can then validate that the AUD is actually A, because if not, the access token used in the current request means that the user has managed to get scope B and C through some other means and isnt valid? Again thanks for being so patient with me. – Style May 10 '23 at 17:18
  • 1
    You can understand that if the request sent by the **client** contains one of scopes `B` and `C`, then the Token returned by **IdentityServer** will contain `API Resource A`. `API Resource A` cannot be accessed if it is not included. – Chen May 11 '23 at 08:54