Introspect
Closed this issue · 4 comments
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.
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.
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"
}
}
};
}
}
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.
Thank you, I understand.