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");
}
}