Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using the Velusia.Client sample the id_token and access_token are null and the @User.Identity.Name is null as well in the index.cshtml #1962

Closed
1 task done
kdudley21 opened this issue Jan 23, 2024 · 7 comments
Labels

Comments

@kdudley21
Copy link

Confirm you've already contributed to this project or that you sponsor it

  • I confirm I'm a sponsor or a contributor

Version

5.1.0

Question

I am using the Velusia example to play around with setting up a test client and authorization server.
With the Velusia.Client it is coming back as authorized and there are some claims there but in the index.cshtml

@User.Identity.Name is null
and so is

@await Context.GetTokenAsync("access_token")
and
@await Context.GetTokenAsync("id_token")

Any idea why there are a handful of claims but these values are null ?

@kevinchalet
Copy link
Member

kevinchalet commented Jan 23, 2024

Hey,

@User.Identity.Name is null
and so is

That's because the OpenIddict client maps ClaimTypes.Name (the WS-Federation claim used by default by ClaimsIdentity's constructor to populate the IIdentity.Name property) from the OpenID Connect preferred_username claim. Yet, this claim is not currently added by the server samples. I opened openiddict/openiddict-samples#286 to improve that.

Since it's not a claim that is always added by identity providers, I also opened #1963 to update the client stack to fall back to the OIDC name claim when preferred_username is not present.

@await Context.GetTokenAsync("access_token")
and
@await Context.GetTokenAsync("id_token")

It's expected, OpenIddict doesn't use these constants.

To retrieve the access token, use either:

  • backchannel_access_token (represented by the OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken constant) for the access token returned by the token endpoint (only applies to flows where the token endpoint is used, e.g the code flow).
  • frontchannel_access_token (represented by the OpenIddictClientAspNetCoreConstants.Tokens.FrontchannelAccessToken constant) for the access token returned directly by the authorization endpoint (e.g implicit or hybrid flows where you specify response_type=token).

Same logic for the identity token.

Hope it'll help.

@kdudley21
Copy link
Author

Thank you so much for your response ! That clears up the confusion I had

@kdudley21
Copy link
Author

kdudley21 commented Jan 23, 2024

It looks like in Velusia the FrontchannelAccessToken and FrontchannelIdentityToken are null but the backchannel tokens are populated. Is that configured somewhere in the client to not send the FrontChannel tokens ?

Thanks again for all your help. I pulled down your changes this morning and the identity name is being populated perfectly now

@kevinchalet
Copy link
Member

Thank you so much for your response ! That clears up the confusion I had

Glad I could help 😄

It looks like in Velusia the FrontchannelAccessToken and FrontchannelIdentityToken are null but the backchannel tokens are populated. Is that configured somewhere in the client to not send the FrontChannel tokens ?

It's expected: this sample uses the authorization code flow, in which all the tokens are returned by the token endpoint. As I mentioned, the frontchannel tokens are only available when using the implicit or hybrid flows with response_type=token or response_type=id_token, which isn't something I recommend in most cases.

@kdudley21
Copy link
Author

kdudley21 commented Jan 23, 2024

Ohhhh ok I gotcha !

From what I understand the backchannel and authorization code flow is more popular anyways so this is perfect.

Thanks !

@kevinchalet
Copy link
Member

kevinchalet commented Jan 23, 2024

From what I understand the backchannel and authorization code flow is more popular anyways so this is perfect.

Yep! When negotiating the grant_type/response_type combination (based on the client configuration and the server metadata), the OpenIddict client will always prefer the authorization code flow, as it's the best option available.

If you're interested in the gnarly details, you can find the (complex) logic here, with a few comments indicating why some flows are preferred to others:

// In OAuth 2.0/OpenID Connect, the concept of "flow" is actually a quite complex combination
// of a grant type and a response type (that can include multiple, space-separated values).
//
// While the authorization code flow has a unique grant type/response type combination, more
// complex flows like the hybrid flow have many valid grant type/response types combinations.
//
// To evaluate whether a specific flow can be used, both the grant types and response types
// MUST be analyzed to find standard combinations that are supported by the both the client
// and the authorization server.
(context.GrantType, context.ResponseType) = (
Client: (
// Note: if grant types are explicitly listed in the client registration, only use
// the grant types that are both listed and enabled in the global client options.
// Otherwise, always default to the grant types that have been enabled globally.
GrantTypes: context.Registration.GrantTypes.Count switch
{
0 => context.Options.GrantTypes as ICollection<string>,
_ => context.Options.GrantTypes.Intersect(context.Registration.GrantTypes, StringComparer.Ordinal).ToList()
},
// Note: if response types are explicitly listed in the client registration, only use
// the response types that are both listed and enabled in the global client options.
// Otherwise, always default to the response types that have been enabled globally.
ResponseTypes: context.Registration.ResponseTypes.Count switch
{
0 => context.Options.ResponseTypes.Select(static types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.ToList(),
_ => context.Options.ResponseTypes.Select(static types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.Where(types => context.Registration.ResponseTypes.Any(value => value
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal)
.SetEquals(types)))
.ToList()
}),
Server: (
GrantTypes: context.Configuration.GrantTypesSupported,
ResponseTypes: context.Configuration.ResponseTypesSupported
.Select(static types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.ToList())) switch
{
// Note: if no grant type was explicitly returned as part of the server configuration,
// the identity provider is assumed to implicitly support both the authorization code
// and the implicit grants, as stated by the OAuth 2.0/OIDC discovery specifications.
//
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
// and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information.
// Note: response_type=code is always tested first as it doesn't require using
// response_mode=form_post or response_mode=fragment: fragment doesn't natively work with
// server-side clients and form_post is impacted by the same-site cookies restrictions
// that are now enforced by most browser vendors, which requires using SameSite=None for
// response_mode=form_post to work correctly. While it doesn't have native protection
// against mix-up attacks (due to the missing id_token in the authorization response),
// the code flow remains the best compromise and thus always comes first in the list.
// Authorization code flow with grant_type=authorization_code and response_type=code:
(var client, var server) when
// Ensure grant_type=authorization_code is - explicitly or implicitly - supported.
client.GrantTypes.Contains(GrantTypes.AuthorizationCode) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code grant is supported by the server.
server.GrantTypes.Contains(GrantTypes.AuthorizationCode)) &&
// Ensure response_type=code is supported.
client.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code)) &&
server.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code))
=> (GrantTypes.AuthorizationCode, ResponseTypes.Code),
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code id_token:
(var client, var server) when
// Ensure grant_type=authorization_code is - explicitly or implicitly - supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
// Ensure response_type=code id_token is supported.
client.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken)) &&
server.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken))
=> (GrantTypes.AuthorizationCode, ResponseTypes.Code + ' ' + ResponseTypes.IdToken),
// Implicit flow with grant_type=implicit and response_type=id_token:
(var client, var server) when
// Ensure grant_type=implicit is - explicitly or implicitly - supported.
client.GrantTypes.Contains(GrantTypes.Implicit) &&
(server.GrantTypes.Count is 0 || // If empty, assume the implicit grant is supported by the server.
server.GrantTypes.Contains(GrantTypes.Implicit)) &&
// Ensure response_type=id_token is supported.
client.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken)) &&
server.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken))
=> (GrantTypes.Implicit, ResponseTypes.IdToken),
// Note: response types combinations containing "token" are always tested last as some
// authorization servers - like OpenIddict - are known to block authorization requests
// asking for an access token if Proof Key for Code Exchange is used in the same request.
//
// Returning an identity token directly from the authorization endpoint also has privacy
// concerns that code-based flows - that require a backchannel request - typically don't
// have when the client application (confidential or public) is executed on a server.
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code id_token token.
(var client, var server) when
// Ensure grant_type=authorization_code is - explicitly or implicitly - supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
// Ensure response_type=code id_token token is supported.
client.ResponseTypes.Exists(static types => types.Count is 3 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token)) &&
server.ResponseTypes.Exists(static types => types.Count is 3 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> (GrantTypes.AuthorizationCode, ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token),
// Hybrid flow with grant_type=authorization_code/implicit and response_type=code token.
(var client, var server) when
// Ensure grant_type=authorization_code is - explicitly or implicitly - supported.
(client.GrantTypes.Contains(GrantTypes.AuthorizationCode) && client.GrantTypes.Contains(GrantTypes.Implicit)) &&
(server.GrantTypes.Count is 0 || // If empty, assume the code and implicit grants are supported by the server.
(server.GrantTypes.Contains(GrantTypes.AuthorizationCode) && server.GrantTypes.Contains(GrantTypes.Implicit))) &&
// Ensure response_type=code token is supported.
client.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.Token)) &&
server.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.Token))
=> (GrantTypes.AuthorizationCode, ResponseTypes.Code + ' ' + ResponseTypes.Token),
// Implicit flow with grant_type=implicit and response_type=id_token token.
(var client, var server) when
// Ensure grant_type=implicit is - explicitly or implicitly - supported.
client.GrantTypes.Contains(GrantTypes.Implicit) &&
(server.GrantTypes.Count is 0 || // If empty, assume the implicit grant is supported by the server.
server.GrantTypes.Contains(GrantTypes.Implicit)) &&
// Ensure response_type=code token is supported.
client.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token)) &&
server.ResponseTypes.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> (GrantTypes.Implicit, ResponseTypes.IdToken + ' ' + ResponseTypes.Token),
// Note: response_type=token is not considered secure enough as it allows malicious
// actors to inject access tokens that were initially issued to a different client.
// As such, while OpenIddict-based servers allow using response_type=token for backward
// compatibility with legacy clients, OpenIddict-based clients are deliberately not
// allowed to negotiate the unsafe and OAuth 2.0-only response_type=token flow.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc6749#section-10.16 and
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.1.2.
// None flow with response_type=none.
(var client, var server) when
// Ensure response_type=none is supported.
client.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.None)) &&
server.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.None))
=> (null, ResponseTypes.None),
// Note: this check is only enforced after the none flow was excluded as it doesn't use a grant type.
(var client, _) when client.GrantTypes.Count is 0
=> throw new InvalidOperationException(SR.GetResourceString(SR.ID0360)),
(var client, _) when client.ResponseTypes.Count is 0
=> throw new InvalidOperationException(SR.GetResourceString(SR.ID0361)),
(_, var server) when server.ResponseTypes.Count is 0
=> throw new InvalidOperationException(SR.GetResourceString(SR.ID0297)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0298))
};

@kevinchalet
Copy link
Member

Closing as I believe I addressed all your concerns, but feel free to reopen if you have additional questions 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants