DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Introspect #1343

Closed AgPeHaJIuH1 closed 2 months ago

AgPeHaJIuH1 commented 3 months ago

https://github.com/DuendeSoftware/IdentityServer/blob/e9860c6488f90e8fbc11a4452b9dd111dbfae933/src/IdentityServer/Validation/Default/ApiSecretValidator.cs#L69

Which version of Duende IdentityServer are you using? 4.1.2

Which version of .NET are you using? .NET 7

Describe the bug

Why, when I try to check a token (Introspect), I pass data to the Client, but at the same time IdentityServer tries to find an ApiResource with the same ID as the Client and check exactly the ApiResource secret and its Scope. I honestly don't understand why this happens

Expected behavior I have a Client for an application that logs into IdentityServer4 and receives a JWT token. Then it passes the token to my API. My API has its own Client with extended powers, I'm trying to contact Introspect IdentityServer4 to make sure that the user token is valid. But IdentityServer4 doesn't let me through and says ApiResource not found. I expect that when I call from the API, IdentityServer4 will look for the Client by ID and check its secrets and scopes, and not the ApiResource with the same ID as the Client.

Log output/exception with stacktrace

[15:20:31 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Hosting.EndpointRouter | Request path /connect/introspect matched to endpoint type Introspection
[15:20:31 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Hosting.EndpointRouter | Endpoint enabled: Introspection, successfully created handler: IdentityServer4.Endpoints.IntrospectionEndpoint
[15:20:31 INF] [0HN5BRBRRU4PR:00000003] IdentityServer4.Hosting.IdentityServerMiddleware | Invoking IdentityServer endpoint: IdentityServer4.Endpoints.IntrospectionEndpoint for /connect/introspect
[15:20:33 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Endpoints.IntrospectionEndpoint | Starting introspection request.
[15:20:36 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Validation.BasicAuthenticationSecretParser | Start parsing Basic Authentication secret
[15:20:36 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Validation.ISecretsListParser | Parser found secret: BasicAuthenticationSecretParser
[15:20:36 DBG] [0HN5BRBRRU4PR:00000003] IdentityServer4.Validation.ISecretsListParser | Secret id found: api2ids4
[15:20:48 INF] [0HN5BRBRRU4PR:00000003] IdentityServer4.Events.DefaultEventService | {"ApiName":"api2ids4","Category":"Authentication","Name":"API Authentication Failure","EventType":"Failure","Id":1021,"Message":"Unknown API resource","ActivityId":"0HN5BRBRRU4PR:00000003","TimeStamp":"2024-07-24T12:20:48.0000000Z","ProcessId":16072,"LocalIpAddress":"::1:7141","RemoteIpAddress":"::1","$type":"ApiAuthenticationFailureEvent"}
[15:20:48 ERR] [0HN5BRBRRU4PR:00000003] IdentityServer4.Validation.ApiSecretValidator | No API resource with that name found. aborting
[15:20:53 ERR] [0HN5BRBRRU4PR:00000003] IdentityServer4.Endpoints.IntrospectionEndpoint | API unauthorized to call introspection endpoint. aborting.
AgPeHaJIuH1 commented 3 months ago

If I create an ApiResource with the same ID, Secret and Scopes as the Client, the request seems to work, but it gives other side effects. For example, when authorizing, the user receives several auds in the token.

AgPeHaJIuH1 commented 3 months ago

my configuration


public static class IdentityConfig
{
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("admin.api")
            {
                Scopes =
                {
                    "admin.api"
                }
            },
            new ApiResource("courier.api")
            {
                Scopes =
                {
                    "courier.api"
                }
            },
            new ApiResource("user.api")
            {
                Scopes =
                {
                    "user.api"
                }
            },
            new ApiResource("game.api")
            {
                Scopes =
                {
                    "game.api"
                }
            },
            new ApiResource("identity.api")
            {
                Scopes =
                {
                    "identity.api"
                }
            },
            //new ApiResource("api2ids4")
            //{
            //    ApiSecrets =
            //    {
            //        new Secret("111".Sha256())
            //    },
            //    Scopes =
            //    {
            //        IdentityServerConstants.StandardScopes.OpenId,
            //        IdentityServerConstants.StandardScopes.Profile,
            //        IdentityServerConstants.StandardScopes.OfflineAccess,
            //        "admin.api",
            //        "courier.api",
            //        "user.api",
            //        "game.api",
            //        "identity.api",
            //    }
            //},
        };
    }

    public static IEnumerable<ApiScope> GetApiScopes()
    {
        return new List<ApiScope>
        {
            new ApiScope("admin.api"),
            new ApiScope("courier.api"),
            new ApiScope("user.api"),
            new ApiScope("game.api"),
            new ApiScope("identity.api"),
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        int accessTokenLifeTimeIsSeconds = (int)TimeSpan.FromHours(1).TotalSeconds;
        int refreshTokenLifeTimeInSeconds = (int)TimeSpan.FromDays(15).TotalSeconds;

        return new List<Client>
        {
            new Client
            {
                ClientId = "api2ids4",
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                AllowOfflineAccess = true,
                RequirePkce = false,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AccessTokenLifetime = accessTokenLifeTimeIsSeconds,
                SlidingRefreshTokenLifetime = refreshTokenLifeTimeInSeconds,
                ClientSecrets =
                {
                    new Secret("111".Sha256())
                },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "admin.api",
                    "courier.api",
                    "user.api",
                    "game.api",
                    "identity.api",
                    "introspection"
                }
            },

            new Client
            {
                ClientId = "admin.client",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowOfflineAccess = true,
                RequirePkce = false,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AccessTokenLifetime = accessTokenLifeTimeIsSeconds,
                SlidingRefreshTokenLifetime = refreshTokenLifeTimeInSeconds,
                ClientSecrets =
                {
                    new Secret("222".Sha256())
                },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "admin.api"
                }
            },

            new Client
            {
                ClientId = "courier.client",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowOfflineAccess = true,
                RequirePkce = false,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AccessTokenLifetime = accessTokenLifeTimeIsSeconds,
                SlidingRefreshTokenLifetime = refreshTokenLifeTimeInSeconds,
                ClientSecrets =
                {
                    new Secret("333".Sha256())
                },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "courier.api"
                }
            },

            new Client
            {
                ClientId = "user.client",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowOfflineAccess = true,
                RequirePkce = false,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AccessTokenLifetime = accessTokenLifeTimeIsSeconds,
                SlidingRefreshTokenLifetime = refreshTokenLifeTimeInSeconds,
                ClientSecrets =
                {
                    new Secret("444".Sha256())
                },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "user.api"
                }
            },

            new Client
            {
                ClientId = "game.client",
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowOfflineAccess = true,
                RequirePkce = false,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                AccessTokenLifetime = accessTokenLifeTimeIsSeconds,
                SlidingRefreshTokenLifetime = refreshTokenLifeTimeInSeconds,
                ClientSecrets =
                {
                    new Secret("555".Sha256())
                },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "game.api"
                }
            }
        };
    }
}
RolandGuijt commented 2 months ago

IdentityServer 4 is out of support, sorry. Having said that: I understand your thinking here but we don't recommend doing it like this. The secure way would be to use an extension grant to let the first API exchange the access token it receives for another access token that can be used for the next API that is to be called. We have an example on that here. But please note this is for IdentityServer 7.

AgPeHaJIuH1 commented 2 months ago

Thank you, I understand.