Je me demande comment actualiser un jeton d'accès dans un client IdentityServer4 à l'aide du flux hybride et construit à l'aide d'ASP.NET Core MVC.
Si j'ai bien compris le concept dans son ensemble, le client doit tout d'abord disposer de la portée "offline_access" pour pouvoir utiliser les jetons d'actualisation, ce qui est la meilleure pratique pour autoriser les jetons d'accès de courte durée et la capacité de révoquer des jetons d'actualisation empêchant toute nouvelle création d'accès être délivré au client.
J'ai réussi à obtenir un jeton d'accès et un jeton d'actualisation, mais comment dois-je gérer la procédure de mise à jour réelle du jeton d'accès dans le client MVC?
Le middleware OpenID Connect (OIDC) peut-il gérer cela automatiquement? Ou devrais-je plutôt vérifier l'heure d'expiration du jeton d'accès partout où j'appelle les api WEB en vérifiant si le jeton d'accès a expiré ou expirera très bientôt (30 secondes à venir), puis actualisez le jeton d'accès en appelant le noeud final à l'aide du jeton d'actualisation ?
Est-il recommandé d'utiliser la méthode d'extension IdentityModel2 library TokenClient
RequestRefreshTokenAsync
dans les méthodes d'action de mon contrôleur pour appeler le point de terminaison du jeton?
J'ai vu du code qui, dans l'événement intermédiaire OIDC, demande le jeton d'accès et utilise, dans la réponse, une revendication contenant une date d'expiration. Le problème est que mon OIDC demande déjà d’une manière ou d’une autre un jeton d’accès automatiquement. Il n’est donc pas bon de demander un nouveau jeton d’accès directement après avoir reçu le premier.
Exemple de méthode d'action de contrôleur without logique de rafraîchissement de jeton d'accès:
public async Task<IActionResult> GetInvoices()
{
var token = await HttpContext.Authentication.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(token);
var response = await client.GetStringAsync("http://localhost:5001/api/getInvoices");
ViewBag.Json = JArray.Parse(response).ToString();
return View();
}
Le middleware OIDC not s'en chargera pour vous. Il est en cours d'exécution lorsqu'il détecte une réponse HTTP 401
, puis redirige l'utilisateur vers la page de connexion d'IdentityServer. Une fois la redirection vers votre application MVC effectuée, les revendications sont transformées en ClaimsIdentity
et sont transmises au middleware des cookies qui les matérialise sous la forme d’un cookie de session.
Toute autre demande n'impliquera pas le middleware OIDC tant que le cookie est toujours valide.
Donc, vous devez vous en occuper vous-même. Vous devez également prendre en compte le fait que chaque fois que vous allez actualiser le jeton d'accès, vous devez mettre à jour le jeton existant afin de ne pas le perdre. Si vous ne le faites pas, le cookie de session contiendra toujours le même jeton - l'original - et vous l'actualiserez à chaque fois.
Une solution que j'ai trouvée consiste à intégrer cela dans le middleware Cookies. Voici le flux général:
ClaimsIdentity
Ce qui me plaît dans cette approche, c’est que dans votre code MVC, vous avez presque toujours la garantie de toujours avoir un jeton d’accès valide, à moins que le renvoi du jeton échoue plusieurs fois de suite.
Ce que je n’aime pas, c’est que cela est très lié à MVC - plus particulièrement au middleware Cookies - et qu’il n’est donc pas vraiment portable.
Vous pouvez jeter un oeil à ce rapport GitHub j'ai mis en place. Il utilise en effet IdentityModel
car cela s’occupe de tout et cache la plus grande partie de la complexité des appels HTTP que vous auriez à faire à IdentityServer.
J'ai créé une solution basée sur un filtre d'action togheter avec le middleware OIDC dans ASP.NET Core 2.0.
Les requêtes AJAX seront également acheminées via le filtre d'action, d'où la mise à jour du jeton d'accès/d'actualisation.
https://Gist.github.com/devJ0n/43c6888161169e09fec542d2dc12af09
J'ai trouvé deux solutions possibles, les deux sont égales mais se produisent à des moments différents dans le middleware OIDC. Dans les événements, j'extrais la valeur de temps d'expiration du jeton d'accès et l'enregistre en tant que revendication, qui peut ensuite être utilisée pour vérifier s'il est correct d'appeler une API Web avec le jeton d'accès actuel ou si je préfère demander un nouveau jeton d'accès à l'aide de l'actualisation jeton.
J'apprécierais que quelqu'un puisse donner des informations sur le choix de ces événements qu'il est préférable d'utiliser.
var oidcOptions = new OpenIdConnectOptions
{
AuthenticationScheme = appSettings.OpenIdConnect.AuthenticationScheme,
SignInScheme = appSettings.OpenIdConnect.SignInScheme,
Authority = appSettings.OpenIdConnect.Authority,
RequireHttpsMetadata = _hostingEnvironment.IsDevelopment() ? false : true,
PostLogoutRedirectUri = appSettings.OpenIdConnect.PostLogoutRedirectUri,
ClientId = appSettings.OpenIdConnect.ClientId,
ClientSecret = appSettings.OpenIdConnect.ClientSecret,
ResponseType = appSettings.OpenIdConnect.ResponseType,
UseTokenLifetime = appSettings.OpenIdConnect.UseTokenLifetime,
SaveTokens = appSettings.OpenIdConnect.SaveTokens,
GetClaimsFromUserInfoEndpoint = appSettings.OpenIdConnect.GetClaimsFromUserInfoEndpoint,
Events = new OpenIdConnectEvents
{
OnTicketReceived = TicketReceived,
OnUserInformationReceived = UserInformationReceived
},
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = appSettings.OpenIdConnect.NameClaimType,
RoleClaimType = appSettings.OpenIdConnect.RoleClaimType
}
};
oidcOptions.Scope.Clear();
foreach (var scope in appSettings.OpenIdConnect.Scopes)
{
oidcOptions.Scope.Add(scope);
}
app.UseOpenIdConnectAuthentication(oidcOptions);
Et voici quelques exemples d'événements parmi lesquels je peux choisir:
public async Task TicketReceived(TicketReceivedContext trc)
{
await Task.Run(() =>
{
Debug.WriteLine("TicketReceived");
//Alternatives to get the expires_at value
//var expiresAt1 = trc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
//var expiresAt2 = trc.Ticket.Properties.GetTokenValue("expires_at");
//var expiresAt3 = trc.Ticket.Properties.Items[".Token.expires_at"];
//Outputs:
//expiresAt1 = "2016-12-19T11:58:24.0006542+00:00"
//expiresAt2 = "2016-12-19T11:58:24.0006542+00:00"
//expiresAt3 = "2016-12-19T11:58:24.0006542+00:00"
//Remove OIDC protocol claims ("iss","aud","exp","iat","auth_time","nonce","acr","amr","azp","nbf","c_hash","sid","idp")
ClaimsPrincipal p = TransformClaims(trc.Ticket.Principal);
//var identity = p.Identity as ClaimsIdentity;
// keep track of access token expiration
//identity.AddClaim(new Claim("expires_at1", expiresAt1.ToString()));
//identity.AddClaim(new Claim("expires_at2", expiresAt2.ToString()));
//identity.AddClaim(new Claim("expires_at3", expiresAt3.ToString()));
//Todo: Check if it's OK to replace principal instead of the ticket, currently I can't make it work when replacing the whole ticket.
//trc.Ticket = new AuthenticationTicket(p, trc.Ticket.Properties, trc.Ticket.AuthenticationScheme);
trc.Principal = p;
});
}
J'ai également l'événement UserInformationReceived, je ne sais pas si je devrais l'utiliser au lieu de l'événement TicketReceived.
public async Task UserInformationReceived(UserInformationReceivedContext uirc)
{
await Task.Run(() =>
{
Debug.WriteLine("UserInformationReceived");
////Alternatives to get the expires_at value
//var expiresAt4 = uirc.Ticket.Properties.GetTokens().SingleOrDefault(t => t.Name == "expires_at").Value;
//var expiresAt5 = uirc.Ticket.Properties.GetTokenValue("expires_at");
//var expiresAt6 = uirc.Ticket.Properties.Items[".Token.expires_at"];
//var expiresIn1 = uirc.ProtocolMessage.ExpiresIn;
//Outputs:
//expiresAt4 = "2016-12-19T11:58:24.0006542+00:00"
//expiresAt5 = "2016-12-19T11:58:24.0006542+00:00"
//expiresAt6 = "2016-12-19T11:58:24.0006542+00:00"
//expiresIn = "60" <-- The 60 seconds test interval for the access token lifetime is configured in the IdentityServer client configuration settings
var identity = uirc.Ticket.Principal.Identity as ClaimsIdentity;
//Keep track of access token expiration
//Add a claim with information about when the access token is expired, it's possible that I instead should use expiresAt4, expiresAt5 or expiresAt6
//instead of manually calculating the expire time.
//This claim will later be checked before calling Web API's and if needed a new access token will be requested via the IdentityModel2 library.
//identity.AddClaim(new Claim("expires_at4", expiresAt4.ToString()));
//identity.AddClaim(new Claim("expires_at5", expiresAt5.ToString()));
//identity.AddClaim(new Claim("expires_at6", expiresAt6.ToString()));
//identity.AddClaim(new Claim("expires_in1", expiresIn1.ToString()));
identity.AddClaim(new Claim("expires_in", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToLocalTime().ToString()));
//identity.AddClaim(new Claim("expires_in3", DateTime.Now.AddSeconds(Convert.ToDouble(uirc.ProtocolMessage.ExpiresIn)).ToString()));
//The following is not needed when to OIDC middleware CookieAuthenticationOptions.SaveTokens = true
//identity.AddClaim(new Claim("access_token", uirc.ProtocolMessage.AccessToken));
//identity.Claims.Append(new Claim("refresh_token", uirc.ProtocolMessage.RefreshToken));
//identity.AddClaim(new Claim("id_token", uirc.ProtocolMessage.IdToken));
});
}