Solución:
Parece que no hay programación en la autenticación openidconnect para que asp.net core administre el access_token en el servidor después de recibirlo.
Descubrí que puedo interceptar el evento de validación de cookies y verificar si el token de acceso ha expirado. Si es así, realice una llamada HTTP manual al punto final del token con grant_type = refresh_token.
Al llamar a context.ShouldRenew = true; esto hará que la cookie se actualice y se envíe al cliente en la respuesta.
Proporcioné la base de lo que hice y trabajaré para actualizar esta respuesta una vez que todo funcione como se haya resuelto.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies",
ExpireTimeSpan = new TimeSpan(0, 0, 20),
SlidingExpiration = false,
CookieName = "WebAuth",
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
logger.Warn($"Access token has expired, user: {context.HttpContext.User.Identity.Name}");
//TODO: send refresh token to ASOS. Update tokens in context.Properties.Items
//context.Properties.Items["Token.access_token"] = newToken;
context.ShouldRenew = true;
}
}
return Task.FromResult(0);
}
}
});
Debe habilitar la generación de refresh_token estableciendo en startup.cs:
- Establecer valores en AuthorizationEndpointPath = “/ connect / authorize”; // necesario para actualizar el token
- Establecer valores en TokenEndpointPath = “/ connect / token”; // nombre del extremo del token estándar
En su proveedor de tokens, antes de validar la solicitud de token al final del método HandleTokenrequest, asegúrese de haber configurado el alcance sin conexión:
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
Si está configurado correctamente, debería recibir un refresh_token cuando inicie sesión con una contraseña grant_type.
Luego de tu cliente debes emitir la siguiente solicitud (estoy usando Aurelia):
refreshToken() {
let baseUrl = yourbaseUrl;
let data = "client_id=" + this.appState.clientId
+ "&grant_type=refresh_token"
+ "&refresh_token=myRefreshToken";
return this.http.fetch(baseUrl + 'connect/token', {
method: 'post',
body : data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
}
});
}
y eso es todo, asegúrese de que su proveedor de autenticación en HandleRequestToken no esté intentando manipular la solicitud que es de tipo refresh_token:
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
// Password type request processing only
// code that shall not touch any refresh_token request
}
else if(!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid grant type.");
return;
}
return;
}
El refresh_token solo podrá pasar por este método y es manejado por otra pieza de middleware que maneja refresh_token.
Si desea un conocimiento más profundo sobre lo que está haciendo el servidor de autenticación, puede echar un vistazo al código de OpenIdConnectServerHandler:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/master/src/AspNet.Security.OpenIdConnect.Server/OpenIdConnectServerHandler.Exchange.cs
En el lado del cliente, también debe poder manejar la actualización automática del token, aquí hay un ejemplo de un interceptor http para Angular 1.X, donde uno maneja respuestas 401, actualiza el token y luego vuelve a intentar la solicitud:
'use strict';
app.factory('authInterceptorService',
['$q', '$injector', '$location', 'localStorageService',
function ($q, $injector, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var $http;
var _request = function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
return config;
};
var _responseError = function (rejection) {
var deferred = $q.defer();
if (rejection.status === 401) {
var authService = $injector.get('authService');
console.log("calling authService.refreshToken()");
authService.refreshToken().then(function (response) {
console.log("token refreshed, retrying to connect");
_retryHttpRequest(rejection.config, deferred);
}, function () {
console.log("that didn't work, logging out.");
authService.logOut();
$location.path('/login');
deferred.reject(rejection);
});
} else {
deferred.reject(rejection);
}
return deferred.promise;
};
var _retryHttpRequest = function (config, deferred) {
console.log('autorefresh');
$http = $http || $injector.get('$http');
$http(config).then(function (response) {
deferred.resolve(response);
},
function (response) {
deferred.reject(response);
});
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
authInterceptorServiceFactory.retryHttpRequest = _retryHttpRequest;
return authInterceptorServiceFactory;
}]);
Y aquí hay un ejemplo que acabo de hacer para Aurelia, esta vez envolví mi cliente http en un controlador http que verifica si el token está vencido o no. Si está vencido, primero actualizará el token y luego realizará la solicitud. Utiliza la promesa de mantener coherente la interfaz con los servicios de datos del lado del cliente. Este controlador expone la misma interfaz que el cliente aurelia-fetch.
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {AuthService} from './authService';
@inject(HttpClient, AuthService)
export class HttpHandler {
constructor(httpClient, authService) {
this.http = httpClient;
this.authService = authService;
}
fetch(url, options){
let _this = this;
if(this.authService.tokenExpired()){
console.log("token expired");
return new Promise(
function(resolve, reject) {
console.log("refreshing");
_this.authService.refreshToken()
.then(
function (response) {
console.log("token refreshed");
_this.http.fetch(url, options).then(
function (success) {
console.log("call success", url);
resolve(success);
},
function (error) {
console.log("call failed", url);
reject(error);
});
}, function (error) {
console.log("token refresh failed");
reject(error);
});
}
);
}
else {
// token is not expired, we return the promise from the fetch client
return this.http.fetch(url, options);
}
}
}
Para jquery puede buscar un jquery oAuth:
https://github.com/esbenp/jquery-oauth
Espero que esto ayude.
A raíz de la respuesta de @ longday, he tenido éxito al usar este código para forzar la actualización de un cliente sin tener que consultar manualmente un punto final de ID abierto:
OnValidatePrincipal = context =>
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire > DateTime.Now) //TODO:change to check expires in next 5 mintues.
{
context.ShouldRenew = true;
context.RejectPrincipal();
}
}
return Task.FromResult(0);
}