oqtane / oqtane.framework

CMS & Application Framework for Blazor & .NET MAUI
http://www.oqtane.org
MIT License
1.87k stars 541 forks source link

The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions. #3325

Closed Rodien closed 12 months ago

Rodien commented 1 year ago

I am trying to use the 'External Login Settings,' and everything works except when I get redirected back to oqtane. Upon redirection, I receive the following message: 'The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions.

Calling the userinfo endpoint via Postman returns the following values: (userinfo-endpoint)

{ "name": "admin", "sub": "4rt0pmemefh6k1qpmvx7pbftwz", "email": "r.bas@dummy.nl", "email_verified": true, "roles": [ "Administrator", "F M" ] }

That's a valid email, so I don't get why it's failing. Or is there maybe something I am forgetting in the admin area?

image

sbwalker commented 1 year ago

@Rodien please look in your Event Log - there should be more detailed information related to why the login is failing.

Rodien commented 1 year ago

Error detail: Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was email And Actual Claim Types Are oc:entyp, sub, oi_au_id, azp, nonce, at_hash, oi_tkn_id, aud, exp, iss, iat. Login Denied.

Here is a dump of the token values.

{ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "4rt0pmemefh6k1qpmvx7pbftwz", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "admin", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "r.bas@dummy.nl", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Administrator", "Financieel Medewerker" ], "Permission": [ "ManageSettings", "AccessAdminPanel", "ManageAdminSettings", "PublishContent", "EditContent", "DeleteContent", "PreviewContent", "CloneContent", "AccessContentApi", "ListContent", "EditContentOwner", "ApiViewContent", "ViewContentTypes", "EditContentTypes", "ExecuteGraphQLMutations", "ManageUsers", "View Users", "ManageOwnUserInformation", "ListUsers", "EditUsers", "DeleteUsers", "AssignRoleToUsers", "Import", "Export", "ManageRemoteInstances", "ManageRemoteClients", "ExportRemoteInstances", "ManageFeatures", "ManageLayers", "ManageMediaFolder", "ManageMediaProfiles", "ViewMediaOptions", "ManageMenu", "ManageApplications", "ManageScopes", "ManageClientSettings", "ManageServerSettings", "ManageValidationSettings", "ManageQueries", "ManageRoles", "SiteOwner", "ManageSecurityHeadersSettings", "ApplyTheme", "ManageCultures" ], "email": "r.bas@dummy.nl", "email_verified": true, "oc:entyp": "user", "sub": "4rt0pmemefh6k1qpmvx7pbftwz", "name": "admin", "oi_prst": "CoolBlue_financiele_afdeling", "oi_au_id": "9ff252054e45468ab89042edf420c730", "client_id": "CoolBlue_financiele_afdeling", "oi_tkn_id": "b80a8e2b271241f1a9a88adaf43e48c6", "aud": [ "oct:CoolBlue", "oct:Default" ], "scope": "openid profile roles email", "jti": "9ee3d207-65d4-4821-bfca-356e7c9e6168", "exp": 1695727139, "iss": "https://localhost:7271/CoolBlue", "iat": 1695723539 }

image

sbwalker commented 1 year ago

The log error is telling you the full list of claims which Oqtane is receiving from the IDP based on the configuration specified. This list appears to be different than the list of claims you are receiving when calling the IDP with Postman (which obviously is using its own configuration to access the IDP). You will have to try to figure out what the difference is in configuration between the 2 methods.

sbwalker commented 1 year ago

I have done some more research and found a really helpful article related to this topic:

https://nestenius.se/2023/03/28/missing-openid-connect-claims-in-asp-net-core/

Rodien commented 1 year ago

Thanks for the article.

Quick Update:

Oqtane is currently looking inside the TokenId for an email claim, but it cannot find it. The next step should be for Oqtane to call the userinfoEndpoint, but it's not happening.

image

From the article:

If the expected claims are not found in the ID token, then enabling the GetClaimsFromUserInfoEndpoint flag will tell the OIDC handler to make an additional request to this endpoint.

.AddOpenIdConnect(options => { ... options.GetClaimsFromUserInfoEndpoint = true; ... }

I added that option inside Oqtane server but I still can not login.

image

I am gonna look futher at whats going on. This was just a quick update.

sbwalker commented 1 year ago

Oqtane is multi-tenant - so it allows you to define OIDC options for each site in your installation. So you need to look at Oqtane.Server\Extensions\OqtaneSiteAuthenticationBuilderExtensions.cs to see where the OIDC configuration is being specified. You will notice that it already sets GetClaimsFromUserInfoEndpoint (as well as other properties mentioned in the article):

            // site OpenId Connect options
            builder.AddSiteOptions<OpenIdConnectOptions>((options, alias, sitesettings) =>
            {
                if (sitesettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OpenIDConnect)
                {
                    // default options
                    options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
                    options.RequireHttpsMetadata = true;
                    options.SaveTokens = false;
                    options.GetClaimsFromUserInfoEndpoint = true;

I feel like the issue you are experiencing has to to with the mappings after the user token has been received. Oqtane already contains the following logic in Oqtane.Server\Extensions\OqtaneServiceCollectionExtensions.cs:

       public static IServiceCollection ConfigureOqtaneAuthenticationOptions(this IServiceCollection services, IConfigurationRoot Configuration)
        {
            // prevent remapping of claims
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

This is supposed to prevent .NET from performing its "default" mapping which changes the names of the claims. However I also see other other references online to these methods:

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

and the article linked above uses:

    options.MapInboundClaims = false;

So it seems that .NET is a bit of a mess in this area - with lots of conflicting guidance. However the last portion of the article where it talks about the .NET ClaimsMapper removing claims so that the cookie cannot become not too large also seems to be an area to explore.

sbwalker commented 1 year ago

Another good article here:

https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/

The interesting thing is that all of these articles say that the Email claim from the UserInfo endpoint is supposed to be included by default (ie. it is not one of the claims which is removed by the handler). So I don't know why this is happening in your environment.

sbwalker commented 1 year ago

One suggestion is that we could add an "Enable Logging?" option in User Settings which would instruct the framework to log information from additional events such as OnUserInformationReceived and OnSigningIn so that you can see what is actually happening under the covers in the .NET security infrastructure. By default the Enable Logging? option would be disabled as it would be a performance issue in a production environment. This could be helpful for troubleshooting OIDC integration issues.

Rodien commented 1 year ago

Adding those logging functionalities would definitely help.

I just changed the Provider Type to OAuth 2.0, and it now works.

OpenID Connect provider is not working.

OAuth 2.0 has a logic that calls the userInfo endpoint.

image

sbwalker commented 1 year ago

@Rodien I know its confusing but the OnCreatingTicket event is only called by OAuth2 - OnTokenValidated is called by OIDC. Please note that OIDC is essentially built on top of OAuth2 - it simply provides some standards for providers and consumers on how the 3 different endpoints which are part of the OAuth2 flow should interact with one another.

The difference between the 2 options in Oqtane is that the OIDC option uses the middleware provided by .NET Core - which internally contains handlers, etc... for implementing the OIDC process. OIDC does call the UserInfoEndpoint - you just can't see it because its embedded inside of a .NET Core handler. However, .NET Core also does a lot of other "magic" under the covers in terms of claims mapping, etc... which makes the process more difficult to follow and diagnose.

The OAuth2 option implements the process manually - in a very simplistic manner. This is why you can see the call to the UserInfoEndpoint in the Oqtane code. It is mainly intended for legacy systems which do not implement OIDC. But it can also be used with OIDC providers. It does not do any claims mapping - it only handles obtaining an auth token, and then calling the UserInfoEndpoint and looking for an Email claim - that is it. There is no mapping of any other claims.

In general, OIDC is the preferred approach for modern auth - however Microsoft has modified the implementation so many times to try and make it "smarter" that it is now sometimes unreliable and non-intuitive.

sbwalker commented 1 year ago

@Rodien I should probably also mention that I am using the OIDC approach in a number of my own production Oqtane sites and it is working fine. I also know of a many Oqtane users who are using this capability without any issues. So there is something specific about the IDP you are integrating with which seems to be causing issues with the OIDC support in the .NET Auth system. If you could provide some more details about the IDP it may help in identifying the problem.

Rodien commented 1 year ago

Oh yes, I find it puzzling because I can see that the ID Token does not contain an ‘email’ claim, but I can’t yet figure out why the userInfo is not being called.

We can probably close this issue.

I will keep looking into this issue for a few more days.

I am using Orchard Core as the IdP, and Orchard is using OpenIDDict.

https://github.com/openiddict/openiddict-core https://github.com/OrchardCMS/OrchardCore/tree/main/src/OrchardCore.Modules/OrchardCore.OpenId

But I saw videos of it working for other people.

So, yeah, it’s puzzling. I’m going to go back and check the configuration even more carefully

Rodien commented 1 year ago

@sbwalker I got it working.

I had to make some configuration changes inside Orchard Core, and I made a change in the 'AddSiteOptions' for the OpenID Connect options inside Oqtane.

So basically, I had to turn on 'Hybrid Flow' in Orchard Core (OpenID server configuration), and I had to turn on 'Hybrid Flow' again at the application level.

Server Config:

image

Application Config:

image

I changed 'OpenIdConnectResponseType.Code' to 'OpenIdConnectResponseType.CodeIdToken'

image

Rodien commented 1 year ago

I think that there needs to be a field added for 'ResponseType' in the 'External login settings'?

sbwalker commented 1 year ago

@Rodien as of OAuth 2.1 there are only two recommended flows:

  1. Authorization code flow, - user->server interaction
  2. Client credentials flow - server->server interaction

Hybrid Flow includes some aspects of Implicit Flow which was deprecated in OAuth 2.1 (because it is not considered to be secure). So this is why Oqtane sets the ResponseType to OpenIdConnectResponseType.Code (Authorization code flow). However for older IDP servers that are still using the Hybrid Flow it may be necessary to allow Oqtane users to specify the ResponseType.

sbwalker commented 12 months ago

@Rodien I believe this is resolved with the addition of the response types. I will close this issue. Thank you for your assistance.