使用身份服务器4注销时不稳定 [英] Erratic Signing Out with IdentityServer 4
本文介绍了使用身份服务器4注销时不稳定的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!
问题描述
我意识到这篇文章中有相当多的代码。但有很多活动部件,我想给出尽可能多的信息。
这种行为是随机的,很难报告准确的可重现事件。在我的测试中,我被随机弹出,很难理解这是否与我设置的任何配置有任何关系。在我看来,我根本不应该被驱逐,因为在addAccessTokenExpiring
事件期间总是会发送静默登录。
我们拥有的设置是:
- IdP(使用身份服务器4)
- 客户端APP,使用Vue.js(使用TypeScrip)实现
- 用ASP.NET Core 5编写的API
我们编写的配置和身份验证服务是:
auth.config.ts
import { Log, UserManagerSettings, WebStorageStateStore } from "oidc-client";
import AppConfig from "./invariable/app.config";
/* eslint-disable */
class AuthConfig {
public settings: UserManagerSettings;
private baseUrl: string;
constructor() {
this.baseUrl = AppConfig.RunTimeConfig.VUE_APP_APPURL || process.env.VUE_APP_APPURL;
this.settings = {
userStore: new WebStorageStateStore({ store: window.localStorage }),
authority: AppConfig.RunTimeConfig.VUE_APP_IDPURL || process.env.VUE_APP_IDPURL,
client_id: AppConfig.RunTimeConfig.VUE_APP_CLIENTID || process.env.VUE_APP_CLIENTID,
client_secret: AppConfig.RunTimeConfig.VUE_APP_CLIENTSECRET || process.env.VUE_APP_CLIENTSECRET,
redirect_uri: this.baseUrl + process.env.VUE_APP_AUTHCALLBACK,
automaticSilentRenew: false,
silent_redirect_uri: this.baseUrl + process.env.VUE_APP_SILENTREFRESH,
response_type: "code",
response_mode: "query",
scope: "our_scopes",
post_logout_redirect_uri: this.baseUrl + process.env.VUE_APP_SIGNOUT_CALLBACK,
filterProtocolClaims: true,
loadUserInfo: true,
revokeAccessTokenOnSignout: true,
staleStateAge: 300, // should match access_token lifetime.
};
}
}
/* eslint-enable */
const authConfig = new AuthConfig();
export default authConfig;
auth.service.ts
import { UserManagerSettings, User, UserManager } from "oidc-client";
import authConfig from "@/config/auth.config";
import axios, { AxiosResponse } from "axios";
import { Ajax } from "@/config/invariable/ajax";
import AccessClaim from "@/domain/general/accessclaim";
import _ from "lodash";
import store from "@/store";
import StoreNamespaces from "@/config/invariable/store.namespaces";
import Token from "@/store/token/token";
export class AuthService {
private userManager: UserManager;
private tokenStore: string;
constructor(private settings: UserManagerSettings) {
this.settings = settings;
this.userManager = new UserManager(this.settings);
this.tokenStore = StoreNamespaces.tokenModule;
}
public addEvents(): void {
this.userManager.events.addUserSignedOut(() => {
this.signInAgain();
});
this.userManager.events.addAccessTokenExpired(() => {
console.log("Token expired");
this.clearLocalState();
console.log("Stale state cleaned up");
});
this.userManager.events.addAccessTokenExpiring(() => {
console.log("Access token about to expire.");
this.signInAgain();
});
this.userManager.events.addSilentRenewError(() => {
// custom logic here
console.log("An error happened whilst silently renewing the token.");
});
}
public clearLocalState(): Promise<void> {
return this.userManager.clearStaleState();
}
public getUserOnLoad(): Promise<User> {
return this.userManager.getUser().then((user) => {
if (!_.isNil(user) && !user.expired) {
console.log("first load sign-in");
const decodedIdToken = user.profile;
if (!_.isNil(decodedIdToken.store) && !_.isArray(decodedIdToken.store)) decodedIdToken.store = [decodedIdToken.store];
if (!_.isNil(decodedIdToken.classification) && !_.isArray(decodedIdToken.classification)) decodedIdToken.classification = [decodedIdToken.classification];
if (!_.isNil(decodedIdToken.location) && !_.isArray(decodedIdToken.location)) decodedIdToken.location = [decodedIdToken.location];
if (!_.isArray(decodedIdToken.app)) decodedIdToken.app = [decodedIdToken.app];
const token = new Token();
token.accessToken = user.access_token;
token.idToken = user.id_token;
token.storeClaims = decodedIdToken.store || [];
token.userType = decodedIdToken.usertype;
token.isLoggedIn = user && !user.expired;
token.app = decodedIdToken.app;
token.userName = decodedIdToken.name ?? "Unknown User";
store.dispatch(`${this.tokenStore}/setToken`, token);
return user;
} else {
return this.signInAgain();
}
});
}
public async getUserIfLoggedIn(): Promise<User | null> {
const currentUser: User | null = await this.userManager.getUser();
const loggedIn = currentUser !== null && !currentUser.expired;
return loggedIn ? currentUser : null;
}
public async isLoggedIn(): Promise<boolean> {
const currentUser: User | null = await this.userManager.getUser();
return currentUser !== null && !currentUser.expired;
}
public login(): Promise<void> {
return this.userManager.signinRedirect();
}
public logout(): Promise<void> {
return this.userManager.signoutRedirect();
}
public getAccessToken(): Promise<string> {
return this.userManager.getUser().then((data: any) => {
return data.access_token;
});
}
public signInAgain(): Promise<User> {
return this.userManager
.signinSilent()
.then((user) => {
console.log("silent sign-in");
const decodedIdToken = user.profile;
if (!_.isNil(decodedIdToken.store) && !_.isArray(decodedIdToken.store)) decodedIdToken.store = [decodedIdToken.store];
if (!_.isNil(decodedIdToken.classification) && !_.isArray(decodedIdToken.classification)) decodedIdToken.classification = [decodedIdToken.classification];
if (!_.isNil(decodedIdToken.location) && !_.isArray(decodedIdToken.location)) decodedIdToken.location = [decodedIdToken.location];
if (!_.isArray(decodedIdToken.app)) decodedIdToken.app = [decodedIdToken.app];
const token = new Token();
token.accessToken = user.access_token;
token.idToken = user.id_token;
token.storeClaims = decodedIdToken.store || [];
token.userType = decodedIdToken.usertype;
token.isLoggedIn = user && !user.expired;
token.app = decodedIdToken.app;
token.userName = decodedIdToken.name ?? "Unknown User";
store.dispatch(`${this.tokenStore}/setToken`, token);
return user;
})
.catch((err) => {
console.log("silent error");
console.log(err);
this.login();
return err;
});
}
public getAccessClaims(userDetails: any): Promise<AxiosResponse<any>> {
return axios.post(`${Ajax.appApiBase}/PermittedUse/GetAccessesForUser`, userDetails).then((resp: AxiosResponse<any>) => {
return resp.data;
});
}
public getPermissions(userDetails: any, siteId: number | null): Promise<AxiosResponse<any>> {
return axios.get(`${Ajax.appApiBase}/PermittedUse/GetPermissions/${siteId ?? 0}`).then((resp: AxiosResponse<any>) => {
return resp.data;
});
}
public constructAccess(userType: string, claims: Array<AccessClaim>): Array<AccessClaim> {
switch (userType) {
case "storeadmin":
case "storeuser":
return _.filter(claims, (claim) => {
return claim.claim === "store";
});
case "warduser":
return _.filter(claims, (claim) => {
return claim.claim === "classification";
});
}
return Array<AccessClaim>();
}
public getBookableLocations(userType: string, claims: Array<AccessClaim>): Array<AccessClaim> {
switch (userType) {
case "storeadmin":
case "storeuser":
return _.filter(claims, (claim) => {
return claim.claim === "store";
});
case "warduser":
return _.filter(claims, (claim) => {
return claim.claim === "location";
});
}
return Array<AccessClaim>();
}
}
export const authService = new AuthService(authConfig.settings);
在IdP上,我们的客户端配置为:
ClientName = IcClients.Names.ConsumablesApp,
ClientId = IcClients.ConsumablesApp,
RequireConsent = false,
AccessTokenLifetime = TokenConfig.AccessTokenLifetime, // 300 for test purposes
IdentityTokenLifetime = TokenConfig.IdentityTokenLifetime, // 300
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding,
UpdateAccessTokenClaimsOnRefresh = true,
RequireClientSecret = true,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowAccessTokensViaBrowser = true,
AlwaysIncludeUserClaimsInIdToken = true,
RedirectUris = new List<string>
{
"https://localhost:44336/authcallback.html",
"https://localhost:8090/authcallback.html",
"https://localhost:44336/silent-refresh.html",
"https://localhost:8090/silent-refresh.html"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44336/signout-callback-oidc.html",
"https://localhost:8090/signout-callback-oidc.html"
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.LocalApi.ScopeName,
IcAccessScopes.IcAccessClaimsScope,
IdentityResources.UserDetails,
IcAccessScopes.ConsumablesScope
},
ClientSecrets = { new Secret("oursecret".Sha256())}
在IdP,我们使用的是ASP.NET核心标识:
services.AddIdentity<IdpUser, IdentityRole<int>>()
.AddEntityFrameworkStores<IdpDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = cookieDuration; // set to 1 hour
options.SlidingExpiration = true;
});
我的预期是滑动窗口应该每隔5分钟延长一次,因为用户应该在令牌过期之前以静默方式再次登录。
在我的开发环境中监视IdP时,我注意到checksession
调用仅在用户登录时进行一次。维基说checksession
调用应该每两秒发生一次(默认情况下)。我没有更改这个默认设置(不是故意的)。我甚至明确地将checkSessionInterval
属性设置为2000,以确保将其设置为2。
我想要介绍的另一件事是静默刷新html文件,因为我意识到CSP的东西可以发挥作用:
<head>
<title></title>
<meta http-equiv="Content-Security-Policy" content="frame-src 'self' <%= VUE_APP_IDPURL %>; script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
</head>
<body>
<script src="./oidc-client.min.js"></script>
<script>
(function refresh() {
window.location.hash = decodeURIComponent(window.location.hash);
new Oidc.UserManager({
// eslint-disable-next-line @typescript-eslint/camelcase
response_mode: "query",
userStore: new Oidc.WebStorageStateStore({
store: window.localStorage,
}),
})
.signinSilentCallback()
.then(function() {
console.log("****************************************signinSilentCallback****************************************");
})
.catch(function(err) {
debug;
console.log(err);
});
})();
</script>
</body>
如果有人能说明这一点,我们将不胜感激。
一些进一步的信息。作为测试,我将令牌的refrsh时间和身份cookie的cookie生存期都设置为10小时(36,000秒)。
我仍然收到用户在45分钟后被踢出的报告。
推荐答案
我在解决此问题时得出了两个结论,我不敢说这是我们问题的适当解决方案。
- 我的登录代码有误。我使用了IdentityServer4扩展
HttpContext.SignInAsync(user, authProperties)
来创建会话cookie。如果使用ASP.NET标识,这不是做事情的方法。出于1个原因,它不在Cookie中包括SecurityStamp声明。在他们自己的快速入门中,他们使用SignInManager
登录并发布Cookie_signInManager.PasswordSignInAsync(idpUser, model.Password, model.RememberLogin, true)
。 - 我需要关闭ASP.NET标识中的
SecurityStamp
验证功能。我们已经断定,没有它我们也能活下去。您可能认为这会有一个配置设置,但我找不到它。因此,在这个阶段,我已经将UserManager
子类化,并覆盖了SupportsUserSecurityStamp
属性,如下所示:public override bool SupportsUserSecurityStamp => false;
。理论上,这一功能现在将被关闭。如果不是这样,或者如果有更好的方法来做,我很高兴被纠正。(希望听取ASP.NET团队成员对此的看法)。
就是这样。
这篇关于使用身份服务器4注销时不稳定的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
查看全文