如何使用带有 OpenId Connect 的刷新令牌处理 asp.net 核心中的过期访问令牌
How to handle expired access token in asp.net core using refresh token with OpenId Connect
我已经使用 asp.net 配置了一个 ASOS OpenIdConnect 服务器和一个使用 "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0 和 "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0" 的核心 mvc 应用程序。我已经测试了 "Authorization Code" 工作流程并且一切正常。
客户端 Web 应用按预期处理身份验证并创建存储 id_token、access_token 和 refresh_token 的 cookie。
如何强制Microsoft.AspNetCore.Authentication.OpenIdConnect在到期时请求新的access_token?
asp.net 核心 mvc 应用忽略过期的 access_token。
我想让 openidconnect 查看过期的 access_token,然后使用刷新令牌进行调用以获取新的 access_token。它还应该更新 cookie 值。如果刷新令牌请求失败,我希望 openidconnect 到 "sign out" cookie(删除它或其他东西)。
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = "myClient",
ClientSecret = "secret_secret_secret",
PostLogoutRedirectUri = "http://localhost:27933/",
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
Authority = http://localhost:27933,
MetadataAddress = "http://localhost:27933/connect/config",
Scope = { "email", "roles", "offline_access" },
});
您必须通过在 startup.cs:
中设置来启用 refresh_token 的生成
- 将值设置为 AuthorizationEndpointPath =“/connect/authorize”; // refreshtoken
需要
- 将值设置为 TokenEndpointPath = "/connect/token"; // 标准令牌端点名称
在您的令牌提供程序中,在 HandleTokenrequest 方法末尾验证令牌请求之前,请确保您已设置离线范围:
// 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);
如果设置正确,当您使用密码 grant_type 登录时,您应该会收到 refresh_token 回复。
然后您必须从您的客户端发出以下请求(我使用的是 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'
}
});
}
就是这样,请确保您在 HandleRequestToken 中的身份验证提供程序没有试图操纵 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;
}
refresh_token 只能通过这个方法,由另一个处理 refresh_token.
的中间件处理
如果您想更深入地了解身份验证服务器正在做什么,可以查看 OpenIdConnectServerHandler 的代码:
在客户端,您还必须能够处理令牌的自动刷新,这里是 Angular 1.X 的 http 拦截器示例,其中一个处理 401 响应,刷新令牌,然后重试请求:
'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;
}]);
这是我刚刚为 Aurelia 做的一个例子,这次我将我的 http 客户端包装到一个 http 处理程序中,该处理程序检查令牌是否过期。如果它已过期,它将首先刷新令牌,然后执行请求。它使用承诺来保持与客户端数据服务的接口一致。此处理程序公开与 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);
}
}
}
对于 jquery 你可以查看 jquery oAuth:
https://github.com/esbenp/jquery-oauth
希望这对您有所帮助。
asp.net核心的openidconnect认证中似乎没有编程来管理接收后在服务器上的access_token。
我发现我可以拦截 cookie 验证事件并检查访问令牌是否已过期。如果是这样,请使用 grant_type=refresh_token.
对令牌端点进行手动 HTTP 调用
通过调用 context.ShouldRenew = true;这将导致 cookie 更新并在响应中发送回客户端。
我已经提供了我所做工作的基础,并会在所有工作解决后更新此答案。
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);
}
}
});
根据@longday 的回答,我已经成功地使用此代码强制客户端刷新,而无需手动查询开放 ID 端点:
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);
}
我已经使用 asp.net 配置了一个 ASOS OpenIdConnect 服务器和一个使用 "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0 和 "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0" 的核心 mvc 应用程序。我已经测试了 "Authorization Code" 工作流程并且一切正常。
客户端 Web 应用按预期处理身份验证并创建存储 id_token、access_token 和 refresh_token 的 cookie。
如何强制Microsoft.AspNetCore.Authentication.OpenIdConnect在到期时请求新的access_token?
asp.net 核心 mvc 应用忽略过期的 access_token。
我想让 openidconnect 查看过期的 access_token,然后使用刷新令牌进行调用以获取新的 access_token。它还应该更新 cookie 值。如果刷新令牌请求失败,我希望 openidconnect 到 "sign out" cookie(删除它或其他东西)。
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = "myClient",
ClientSecret = "secret_secret_secret",
PostLogoutRedirectUri = "http://localhost:27933/",
RequireHttpsMetadata = false,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet,
Authority = http://localhost:27933,
MetadataAddress = "http://localhost:27933/connect/config",
Scope = { "email", "roles", "offline_access" },
});
您必须通过在 startup.cs:
中设置来启用 refresh_token 的生成- 将值设置为 AuthorizationEndpointPath =“/connect/authorize”; // refreshtoken 需要
- 将值设置为 TokenEndpointPath = "/connect/token"; // 标准令牌端点名称
在您的令牌提供程序中,在 HandleTokenrequest 方法末尾验证令牌请求之前,请确保您已设置离线范围:
// 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);
如果设置正确,当您使用密码 grant_type 登录时,您应该会收到 refresh_token 回复。
然后您必须从您的客户端发出以下请求(我使用的是 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'
}
});
}
就是这样,请确保您在 HandleRequestToken 中的身份验证提供程序没有试图操纵 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;
}
refresh_token 只能通过这个方法,由另一个处理 refresh_token.
的中间件处理如果您想更深入地了解身份验证服务器正在做什么,可以查看 OpenIdConnectServerHandler 的代码:
在客户端,您还必须能够处理令牌的自动刷新,这里是 Angular 1.X 的 http 拦截器示例,其中一个处理 401 响应,刷新令牌,然后重试请求:
'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;
}]);
这是我刚刚为 Aurelia 做的一个例子,这次我将我的 http 客户端包装到一个 http 处理程序中,该处理程序检查令牌是否过期。如果它已过期,它将首先刷新令牌,然后执行请求。它使用承诺来保持与客户端数据服务的接口一致。此处理程序公开与 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);
}
}
}
对于 jquery 你可以查看 jquery oAuth:
https://github.com/esbenp/jquery-oauth
希望这对您有所帮助。
asp.net核心的openidconnect认证中似乎没有编程来管理接收后在服务器上的access_token。
我发现我可以拦截 cookie 验证事件并检查访问令牌是否已过期。如果是这样,请使用 grant_type=refresh_token.
对令牌端点进行手动 HTTP 调用通过调用 context.ShouldRenew = true;这将导致 cookie 更新并在响应中发送回客户端。
我已经提供了我所做工作的基础,并会在所有工作解决后更新此答案。
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);
}
}
});
根据@longday 的回答,我已经成功地使用此代码强制客户端刷新,而无需手动查询开放 ID 端点:
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);
}