Skip to content
This repository was archived by the owner on Mar 6, 2025. It is now read-only.

Problems migrating from Identity Server 3 to Identity Server 4 (4.0.4) - Broken introspect endpoint? #4742

Closed
kikaragyozov opened this issue Aug 12, 2020 · 15 comments
Labels

Comments

@kikaragyozov
Copy link

kikaragyozov commented Aug 12, 2020

I couldn't find a documentation with all the breaking changes, nor any newly added features.

I understood that in the old version, when hitting the token endpoint (to get access/refresh tokens), you could supply the key "scope" with the list of scopes, or supply it as "scopes" and both worked, whereas in the new version only "scopes" now works.

Now I'm stuck at the introspect endpoint. When I hit it, it always returns "isActive": "false" as a response.

I've debugged and it correctly reads the token from the database, but why it returns that 24/7 of the time (if my data is correct) - I don't know. It should return data for the token,as it's not expired.

The token read from the database has all the fields correctly assigned, I even checked if its caused due to an expiration in the date we're saving, but it's not that. Its type is correctly set to "reference_token", the only fields that have no value/are null, are the newly added in PersistedGrant object properties - Description, SessionId, ConsumedTime.

I'm using the resource owner password grant.

My configuration features a simple ApiResource declared with a secret, as well as a list of Client objects configured to my liking.

Example client configuration:

                Enabled = true,
                ClientId = "client",
                ClientSecrets = "secret"
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowOfflineAccess = true,
                AccessTokenType = AccessTokenType.Reference,
                RequireConsent = false,
                RequirePkce = false,
                UpdateAccessTokenClaimsOnRefresh = true,
                RefreshTokenExpiration = TokenExpiration.Absolute,
                AbsoluteRefreshTokenLifetime = 123456,
                RefreshTokenUsage = TokenUsage.ReUse,
                AccessTokenLifetime = 600000,
                AllowedScopes = { "helloAPI", StandardScopes.OfflineAccess },

The endpoint is being tested by Postman (not API)

EDIT: Just for info - when using JWT instead of reference tokens, everything works correctly.

@kikaragyozov kikaragyozov changed the title Problems migrating from Identity Server 3 to Identity Server 4 (4.0.4) Problems migrating from Identity Server 3 to Identity Server 4 (4.0.4) - Broken introspect endpoint? Aug 12, 2020
@brockallen
Copy link
Contributor

brockallen commented Aug 12, 2020

Not sure. I just tested it with our RO Console sample (I changed it to use reference tokens), and here's the result from introspection:

{
"iss":"https://localhost:5001",
"nbf":1597242427,
"exp":1597246027,
"aud":["resource1","resource2"],
"client_id":"roclient",
"sub":"88421113",
"auth_time":1597242427,
"idp":"local",
"amr":"pwd",
"name":"Bob Smith",
"email":"[email protected]",
"jti":"4A59C1B22EEC41AAE329D473BD25D8B7",
"iat":1597242427,
"active":true,
"scope":"resource1.scope1"
}

Notice it's active, not isActive.

So, I'd suggest try to repro with our samples and then match what's different in your solution.

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 13, 2020

Not sure. I just tested it with our RO Console sample (I changed it to use reference tokens), and here's the result from introspection:

{
"iss":"https://localhost:5001",
"nbf":1597242427,
"exp":1597246027,
"aud":["resource1","resource2"],
"client_id":"roclient",
"sub":"88421113",
"auth_time":1597242427,
"idp":"local",
"amr":"pwd",
"name":"Bob Smith",
"email":"[email protected]",
"jti":"4A59C1B22EEC41AAE329D473BD25D8B7",
"iat":1597242427,
"active":true,
"scope":"resource1.scope1"
}

Notice it's active, not isActive.

So, I'd suggest try to repro with our samples and then match what's different in your solution.

I found the issue and fixed it - I had to define a scope in the Scopes property of an ApiResource. This was only mandatory for reference tokens, which baffles me.

On another note, could you please help clear out the difference between an identity resource and an api scope?

From what I've gathered, both serve the same purpose, i.e identity resources are scopes, in which you can define and group a lot of claims. What's the distinguishing aspect of an ApiScope then? Do we even need them in the equation?

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 13, 2020

There also seems to be some weird behavior that doesn't make much sense.

If I have defined an ApiResource with a list of ApIScope's it needs to access, why do I have to wrap it intentionally in a ApiScope with the scope name being the ApiResource's name, and display name the same as ApiResource's, if I want a client to be able to have access to the ApiResource?

Why didn't it work as it did before, by simply supplying the ApiResource's name in the Client.AllowedScopes parameter? It made perfect sense allowing a client access to a specific resource.

Are we even using the name of an ApiResource right now, seeing that the previous functionality no longer works, or am I missing something here?

@brockallen
Copy link
Contributor

could you please help clear out the difference between an identity resource and an api scope?

Here are our docs: https://identityserver4.readthedocs.io/en/latest/topics/resources.html

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 13, 2020

Thanks! It seems that an identity resource is a thing tied specifically to identity tokens. Nowhere in the page you linked is that ever mentioned, hence the major confusion.

What about the above-mentioned problems with wrapping a whole ApiResource's Allowed scopes in an ApiScope?

@brockallen
Copy link
Contributor

Nowhere in the page you linked is that ever mentioned, hence the major confusion.

Yea, that's something we generally expect people to know from the protocols.

@kikaragyozov
Copy link
Author

No worries, we cleared that out of the equation. The elephant in the room right now is the weird wrapping. Perhaps @leastprivilege can answer that?

@brockallen
Copy link
Contributor

I don't know/follow what you're trying to do with ApiResources. I'd suggest not using them at all unless you have a protocol reason to do so.

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 13, 2020

Okay, let's define the following Configuration:

public static IEnumerable<ApiScope> GetApiScopes() =>
	new List<ApiScope>
	{
		new ApiScope(
			name: "Scope1",
			displayName: "scope1 description",
			userClaims: new[] { "claim1" }),
		new ApiScope(
			name: "Scope2",
			displayName: "scope2 description",
			userClaims: new[] { "claim2", "claim3", "claim4"}),
		new ApiScope(
			name: "Scope3",
			displayName: "scope3 description",
			userClaims: new[] { "claim5" }),
		new ApiScope(
			name: "Scope4",
			displayName: "scope4 description",
			userClaims: new[] { "claim6" })
	};
	
public static IEnumerable<ApiResource> GetApiResources() =>
	new List<ApiResource>
	{
		new ApiResource("MyApi", "MyApi description")
		{
			ApiSecrets = { new Secret("secret").Sha256() },
			Scopes =
			{
				"Scope1",
				"Scope2",
				"Scope3",
				"Scope4"
			}
		}
	};
	
public static IEnumerable<Client> GetClients() =>
	new List<Client>
	{
		new Client
		{
			Enabled = true,
			ClientId = "client",
			ClientSecrets = "secret"
			AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
			AllowOfflineAccess = true,
			AccessTokenType = AccessTokenType.Reference,
			RequireConsent = false,
			RequirePkce = false,
			UpdateAccessTokenClaimsOnRefresh = true,
			RefreshTokenExpiration = TokenExpiration.Absolute,
			AbsoluteRefreshTokenLifetime = 123456,
			RefreshTokenUsage = TokenUsage.ReUse,
			AccessTokenLifetime = 600000,
			AllowedScopes = { "MyApi" }, // This previously worked, now it doesn't
		}
	};

Previously this would've worked. When you require the identity provider to issue an access token for the specific resource, the client would just supply the name of the ApiResource in the scope key-value pair of the request (openid profile MyApi)

Now, it doesn't. You need to wrap all of the Scopes present in the ApiResource in another ApiScope and then pass that ApiScope to the ApiResource in place of all those scopes.

For better clarity, here is a working variant in Identity Server 4.0.3:

public static IEnumerable<ApiScope> GetApiScopes() =>
	new List<ApiScope>
	{
	    new ApiScope(
			name: "Scope1",
			displayName: "scope1 description",
			userClaims: new[] { "claim1" }),
		new ApiScope(
			name: "Scope2",
			displayName: "scope2 description",
			userClaims: new[] { "claim2", "claim3", "claim4"}),
		new ApiScope(
			name: "Scope3",
			displayName: "scope3 description",
			userClaims: new[] { "claim5" }),
		new ApiScope(
			name: "Scope4",
			displayName: "scope4 description",
			userClaims: new[] { "claim6" }),
		// Wrapper
		new ApiScope(
			name: "MyApi",
			displayName: "",
			// Manually add all claims from above scopes. 
                        // If you end up in the future changing one of the above scopes's required claims,
                        // well, make sure you do the same here...
			userClaims: new[] { "claim1", "claim2", "claim3", "claim4", "claim5", "claim6"})		
	};
	
public static IEnumerable<ApiResource> GetApiResources() =>
	new List<ApiResource>
	{
		new ApiResource("MyApi", "MyApi description")
		{
			ApiSecrets = { new Secret("secret").Sha256() },
			Scopes =
			{
				"MyApi"
			}
		}
	};
	
public static IEnumerable<Client> GetClients() =>
	new List<Client>
	{
		new Client
		{
			Enabled = true,
			ClientId = "client",
			ClientSecrets = "secret"
			AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
			AllowOfflineAccess = true,
			AccessTokenType = AccessTokenType.Reference,
			RequireConsent = false,
			RequirePkce = false,
			UpdateAccessTokenClaimsOnRefresh = true,
			RefreshTokenExpiration = TokenExpiration.Absolute,
			AbsoluteRefreshTokenLifetime = 123456,
			RefreshTokenUsage = TokenUsage.ReUse,
			AccessTokenLifetime = 600000,
                        // now works because we have a fake "MyApi" scope,
                        // encapsulating our previously well-defined structure of scopes
			AllowedScopes = { "MyApi" }, 
		}
	};

@brockallen
Copy link
Contributor

In your first config above, MyApi is not a scope. This is some more context for the changes we made:

https://leastprivilege.com/2020/06/18/resource-access-in-identityserver4-v4-and-going-forward/

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 14, 2020

I've read it, but I still don't understand the correct way of doing what we previously could do in identity server 3 (first config). It definitely can't be like in my second configuration. Could you share an example of a config showing the right way to grant a client access to a specific api resource based on my first config?

That'd be super great!

EDIT: Basically the issue I'm experiencing is how to make it so that when a client requests access to an ApiScope for a given audience/api - the access token would be considered invalid in that API, unless all the scopes the ApiResource allows (by defining them in ApiResource.Scopes property) have been requested by the client. In other words, the description of ApiResource.Scopes becomes "Models the scopes this API resource allows requires."

@brockallen
Copy link
Contributor

You want to reject requests for scopes unless all scopes that are configured for that ApiResource are requested?

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 17, 2020

Yes, I think I figured it out:
Use JwtBearerEvents.OnTokenValidated and simply reject the token if they don't answer to the required scopes. Let me know if this was the correct way to go about it. (Doing the same for the introspection middleware)

EDIT: Seems that OnTokenValidated gets called after the token has been issued and is being used to access the resource. Ideally the token should only be validated "once", and if it fails validation - to not even issue it to the client. Am I using the incorrect event here?

@kikaragyozov
Copy link
Author

kikaragyozov commented Aug 21, 2020

Seems I had some misunderstanding. There's no way to avoid this.

Thank you for your help and time @brockallen! ❤️

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 26, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

2 participants