Cannot get groups claim in .NET Framework MVC App

For the life of me I cannot figure out why in .NET Core/5 apps I can get groups scope to work, but I cannot in a .NET Framework 4.6.1 application. I’ve used the exact same application info (client id, client secret, domain) from our Okta tenant.

Here is the code from a .NET 5 app Startup.cs file that works fine:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect(options =>
            {
                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.Authority = Configuration["Okta:Domain"];
                options.RequireHttpsMetadata = true;
                options.ClientId = Configuration["Okta:ClientId"];
                options.ClientSecret = Configuration["Okta:ClientSecret"];
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("groups");
                options.SaveTokens = true;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "groups",
                    ValidateIssuer = true
                };

                options.ClaimActions.Add(new JsonKeyClaimAction(ClaimTypes.Role, "string", "groups"));
            });
            services.AddControllersWithViews();
        }

Here is one version of code from a .NET Framework app Startup.cs file that does not work:

public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOktaMvc(new OktaMvcOptions
            {
                OktaDomain = authority,
                ClientId = clientId,
                ClientSecret = clientSecret,
                RedirectUri = redirectUri,
                PostLogoutRedirectUri = postLogoutRedirectUri,
                Scope = new List<string> {
                    "openid",
                    "profile",
                    "groups"
                },
                GetClaimsFromUserInfoEndpoint = true
            });
        }

It gives the following error:

OpenIdConnectMessage.Error was not null, indicating an error. Error: ‘invalid_scope’. Error_Description (may be empty): ‘One or more scopes are not configured for the authorization server resource.’. Error_Uri (may be empty): ‘error_uri is null’."

Here is another attempt I made using a different library, on the instructions of these pages

:

 public void Configuration(IAppBuilder app)
{
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = clientId,
                RequireHttpsMetadata = true,
                ClientSecret = clientSecret,
                Authority = authority,
                RedirectUri = redirectUri,
                SaveTokens = true,
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                Scope = OpenIdConnectScope.OpenIdProfile + " email phone address groups",
                PostLogoutRedirectUri = postLogoutRedirectUri,
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "groups",
                    ValidateIssuer = true
                },
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n =>
                    {
                        // Exchange code for access and ID tokens
                        var tokenClient = new TokenClient(authority + "/v1/token", clientId, clientSecret);
                        var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri);

                        if (tokenResponse.IsError)
                        {
                            throw new Exception(tokenResponse.Error);
                        }

                        var userInfoClient = new UserInfoClient(authority + "/v1/userinfo");
                        var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
                        var claims = new List<Claim>();
                        claims.AddRange(userInfoResponse.Claims);
                        claims.Add(new Claim("id_token", tokenResponse.IdentityToken));
                        claims.Add(new Claim("access_token", tokenResponse.AccessToken));

                        if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
                        {
                            claims.Add(new Claim("refresh_token", tokenResponse.RefreshToken));
                        }

                        n.AuthenticationTicket.Identity.AddClaims(claims);

                        return;
                    },

                    RedirectToIdentityProvider = n =>
                    {
                        // If signing out, add the id_token_hint
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                        {
                            var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");

                            if (idTokenClaim != null)
                            {
                                n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
                            }

                        }

                        return Task.CompletedTask;
                    }
                },
            });
        }
    }

When using the same domain value as done in the .NET 5 application (https://{our Okta domain}), this gives a simple Not Found error. When using the domain given in the above linked tutorial (https://our Okta domain}/oauth2/default) it gives the same error as the other set of code above did:

OpenIdConnectMessage.Error was not null, indicating an error. Error: ‘invalid_scope’. Error_Description (may be empty): ‘One or more scopes are not configured for the authorization server resource.’. Error_Uri (may be empty): ‘error_uri is null’."

If the authorization server does not have the configuration for that scope, why am I able to get the groups scope to work using the same client id and client secret in the .NET Core/5 apps? Is there some magic that the .NET Core libraries have that isn’t possessed by the .NET Framework equivalents?

Hi @david.pimentel! Can you try our samples from here instead GitHub - okta/samples-aspnet: samples-aspnet (from - Add User Authentication and Okta Resource Management to Your ASP.NET App | Okta Developer) - perhaps the sample you are using is outdated.

Hi @sigama ,
Thank you for the response and the information about more updated GitHub examples. Unfortunately, I tried the one that best fit my scenario (okta-hosted-login) and ran into the same problem. It worked fine until I added the groups scope in the Startup.cs file, like so:

public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOktaMvc(new OktaMvcOptions()
            {
                OktaDomain = ConfigurationManager.AppSettings["okta:OktaDomain"],
                ClientId = ConfigurationManager.AppSettings["okta:ClientId"],
                ClientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"],
                AuthorizationServerId = ConfigurationManager.AppSettings["okta:AuthorizationServerId"],
                RedirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"],
                PostLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"],
                GetClaimsFromUserInfoEndpoint = true,
                Scope = new List<string> {"openid", "profile", "email", "groups"},
            });
        }

The error I get is yet again

“OpenIdConnectMessage.Error was not null, indicating an error. Error: ‘invalid_scope’. Error_Description (may be empty): ‘One or more scopes are not configured for the authorization server resource.’. Error_Uri (may be empty): ‘error_uri is null’.”

Is there some alternate way I can get AD groups to map to Roles for the Authorize attribute in a .NET Framework app? That is my end goal.

If you’re using a custom authorization server, the ‘groups’ scope will not be created for you, like it is when using the built-in (un-customizable) Org authorization server. Details about the different authorization servers found here: Authorization Servers | Okta Developer

When using a custom auth server, you can instead simply create your own groups claim on the authorization server in question (by default, this tends to be the one we unhelpfully call “Default,” which you can find in the admin console under Security → API → Authorization Servers). Whether or not this is a scope-dependent claim is up to you, so you don’t necessarily even need to request a ‘groups’ scope (that you would also have to create) to get the group membership information in your token if you don’t wish to.

Full details about setting up this claim found here (this guide does not make the claim scope-dependent): Add a Groups claim for a Custom Authorization Server | Okta Developer