2

I have an identity server 4 project as server, and asp.net mvc 5 as a client. everything is working fine except one occasional issue : Response type not supported: code+id_token

During my investigation from production log, I found the authorization url was encoded twice occasionally, then I can't find the root cause and reproduce. I guess this is the inner behavior of Microsoft.Owin.Security.OpenIdConnect,

The authorized url is generated automatically when the protected resource is visited by the user who doesn't log in.

From my mvc client side, "code id_token" is encoded to "code+id_token" firstly, then "code+id_token" is encoded to "code%2Bid_token".

From my identity server side, "code%2Bid_token" is decoded to "code+id_token" so that the validation error happens.

The following is my log:

INFO  11:54:35 Request starting HTTP/1.1 GET http://login.example.com/connect/authorize?client_id=gjcf_mvc&nonce=636709820590066816.ZWNlNzJmOGQtMGFhNC00NzVkLTllNzktNmE5NTIzN2EzNDE3NThhMmI2OGYtODI5Mi00OTE2LTgzN2MtNGFkZWUzODQ4Nzlk&redirect_uri=https%3A%2F%2Fwww.example.com%2Fsignin-oidc&response_mode=form_post&response_type=code%2Bid_token&scope=openid%2Bprofile%2Bapi1%2BGjcfApi%2Boffline_access&state=OpenIdConnect.AuthenticationProperties 

INFO  11:54:35 Invoking IdentityServer endpoint: IdentityServer4.Endpoints.AuthorizeEndpoint for /connect/authorize

ERROR 11:54:35 Response type not supported: code+id_token
{
  "ClientId": "gjcf_mvc",
  "ClientName": "gjcf_mvc_name",
  "RedirectUri": "https://www.example.com/signin-oidc",
  "AllowedRedirectUris": [
    "https://www.example.com/signin-oidc"
  ],
  "SubjectId": "anonymous",
  "RequestedScopes": "",
  "State": "OpenIdConnect.AuthenticationProperties=5USuW-uf3wCad1ap9VCDDCE6bTKr1mUMZob-yI_vBUsAFqx_7oLv-0f3rTApD5_6NjVf3siQsJKg9cH4T7YA6ra2B_6_Yooq_S0rJW2L3I4a13Gg5DpcESjg8gb4MQSysOm_xLjgXa96gpGN0tTwNmnb6dB6S3c3ttIDPt_JWCI0qHclfprE_RlO4RlY3LqsI3YhGznHUXM9UW-x38KB9vUtdfulXYrWRko35cQmezI3QAIXqOCt_d7qLgL5WBeNRRk8I0QrbfrmhTwwtS1fTBi5vUPujBPi9L14mCeKPbNZIm5w4oqZOznjBhw0k5v2",
  "Raw": {
    "client_id": "gjcf_mvc",
    "nonce": "636709820590066816.ZWNlNzJmOGQtMGFhNC00NzVkLTllNzktNmE5NTIzN2EzNDE3NThhMmI2OGYtODI5Mi00OTE2LTgzN2MtNGFkZWUzODQ4Nzlk",
    "redirect_uri": "https://www.example.com/signin-oidc",
    "response_mode": "form_post",
    "response_type": "code+id_token",
    "scope": "openid+profile+api1+GjcfApi+offline_access",
    "state": "OpenIdConnect.AuthenticationProperties"
  }
}

The following code is in asp.net mvc startup:

app.UseCookieAuthentication(new CookieAuthenticationOptions
                {
                    AuthenticationType = "Cookies",
                    Provider = new CookieAuthenticationProvider
                    {
                        OnResponseSignIn = context =>
                        {
                            context.Properties.AllowRefresh = true;
                            context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(40);
                        }
                    }
                });

                GjcfOpenIdConnectConfiguration conf = GjcfOpenIdConnectConfiguration.Instance;

                app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
                {
                    ClientId = conf.ClientId,
                    ClientSecret = conf.ClientSecret,
                    Authority = conf.Authority,
                    RedirectUri = conf.RedirectUri,
                    PostLogoutRedirectUri = conf.PostLogoutRedirectUri,

                    ResponseType = conf.ResponseType,
                    Scope = conf.Scope,
                    SignInAsAuthenticationType = "Cookies",


                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthorizationCodeReceived = async n =>
                        {
                            // use the code to get the access and refresh token
                            var tokenClient = new TokenClient(
                                conf.TokenAddress,
                                conf.ClientId,
                                conf.ClientSecret);

                            var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
                                n.Code, n.RedirectUri);

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

                            // use the access token to retrieve claims from userinfo
                            var userInfoClient = new UserInfoClient(
                                new Uri(conf.UserInfoAddress),
                                tokenResponse.AccessToken);

                            var userInfoResponse = await userInfoClient.GetAsync();

                            // create new identity
                            var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                            id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);

                            id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                            id.AddClaim(new Claim("expires_at",
                                DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
                            id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                            id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                            id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
                            id.AddClaim(new Claim(ClaimTypes.NameIdentifier,
                                n.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value));
                            id.AddClaim(new Claim(
                                "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
                                n.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value));

                            n.AuthenticationTicket = new AuthenticationTicket(
                                new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType,
                                    "name", "role"),
                                n.AuthenticationTicket.Properties);
                        },

                        AuthenticationFailed = (context) =>
                        {
                            if (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311"))
                            {
                                context.SkipToNextMiddleware();
                                return Task.FromResult(0);
                            }
                            return Task.FromResult(0);
                        },

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

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

                            }

                            return Task.FromResult(0);
                        }
                    }
                });

When the user access to the action marked as Authorize attribute, the authorize url is generated.

[Authorize]
        public ActionResult IdentityServerJump(string returnUrl)
        {
            if (!string.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
Michael Zhang
  • 21
  • 1
  • 3
  • 1
    Can you please provide some related code. – Dimitar Sep 12 '18 at 03:14
  • Welcome to SO and you have done a good job at putting your first question. As @Dimitar mentioned, a code would be helpful to identify double encoding which you are experiencing. – Kavindu Dodanduwa Sep 12 '18 at 03:26
  • @Michael Zhang did you find a solution? I'm having the same problem. I think it is based on which browser the user is using. But I'm not sure how to fix it. – Ken Hadden May 20 '20 at 20:37

1 Answers1

0

Though you haven't provided code, problem exist with following section

From my mvc client side, "code id_token" is encoded to "code+id_token" firstly, then "code+id_token" is encoded to "code%2Bid_token".

Not sure why you do a double encoding. From IS4 (identity server) end, it will do a URL decode not knowing that query segment is double encoded.

The correction for this is to use a single encoding. As per OpenID Connect specifications, stick with %20 encoding for spaces and do not double encode as you are doing now.!!. Also I welcome you to read about + vs %20 at this answer

Kavindu Dodanduwa
  • 12,193
  • 3
  • 33
  • 46