【发布时间】:2021-06-07 08:50:25
【问题描述】:
我们有用户抱怨,因为他们在工作期间被重定向到 Identity Server 的登录页面(因此丢失了他们当前的工作)。我们已努力配置滑动到期,所以我不确定为什么会发生这种情况。
我意识到这篇文章中有很多代码。但是有很多活动部件,我想提供尽可能多的信息。
这种行为是荒谬的,很难报告确切的可重现事件。在我的测试中,我被随机弹出,很难理解它是否与我设置的任何配置有任何关系。在我看来,我根本不应该被驱逐,因为在addAccessTokenExpiring 事件期间总是会发送静默登录。
我们的设置是:
- 一个 Idp(使用 IdentityServer 4)
- 客户端应用,使用 Vue.js(使用 Typescript)实现
- 用 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 Core Identity:
services.AddIdentity<IdpUser, IdentityRole<int>>()
.AddEntityFrameworkStores<IdpDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = cookieDuration; // set to 1 hour
options.SlidingExpiration = true;
});
我的期望是滑动窗口应该每 5 分钟延长一次,因为用户应该在令牌过期之前再次静默登录。
在我的开发环境中监视 IDP 时,我确实注意到的一件事是 checksession 调用仅在用户登录时进行一次。wiki 说 checksession 调用应该每 2 秒发生一次(默认)。我没有改变这个默认值(不知情)。我什至明确将checkSessionInterval 属性设置为2000,以确保它设置为2s。
我要说明的另一件事是静默刷新 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>
如果有人能对此有所了解,将不胜感激。
一些进一步的信息。作为测试,我将令牌的刷新时间和身份 cookie 的 cookie 生命周期都设置为 10 小时(36,000 秒)。
我仍然收到用户在 45 分钟后被踢出的报告。
【问题讨论】:
-
由于重新部署了 IdentityServer 或客户端,您没有退出?
-
@ToreNestenius 否。这发生在正常使用过程中。这不是由于部署了客户端或 IDP。
-
我只是有一个想法。我是否应该在拨打
AddIdentityServer之后再拨打ConfigureApplicationCookie(这反过来又拨打AddAspNetIdentity)。也许最后一次调用覆盖了我的自定义 cookie 设置。虽然我注意到AddAspNetIdentity并没有覆盖cookie 的名称,但是当我给它一个不同的名称时。在ConfigureApplicationCookie. -
当你涉及到许多不同的服务都想拥有 cookie 时,总是一团糟。理想情况下,我会将 ASP.NET 标识放在它自己的服务中。
-
IdentityServer 不是用户存储。它与用户存储交互,并为这些用户存储实现 OAuth2/OpenIdConnect。 ASP.NET 标识就是这样一种用户存储。我需要 IdentityServer 来处理 OAuth 的东西。
标签: identityserver4 oidc-client-js