mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-4039 logout with context (#2296)
* CB-4039 build logout redirect link on backend side * CB-4039 fix return value * CB-4039 adds logout with context for Okta Identity Provider * CB-4039 backward compatibility fix * CB-4039 add since annotation * CB-4039 api return type fix * CB-4039 logout extended * CB-4039 okta logout redirect moved to AuthenticationService * CB-4039 remove windowService dep * CB-4039 removes old logic of identity provider logout links * CB-4039 okta review changes --------- Co-authored-by: s.teleshev <s.teleshev@mu.se> Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
co-authored by
s.teleshev
Daria Marutkina
parent
5db30be61b
commit
0544547f27
+12
-5
@@ -19,6 +19,7 @@ package io.cloudbeaver.auth;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -28,15 +29,21 @@ import java.util.Map;
|
||||
*/
|
||||
public interface SMAuthProviderFederated {
|
||||
|
||||
/**
|
||||
* Returns new identifying credentials which can be used to find/create user in database
|
||||
*/
|
||||
@NotNull
|
||||
String getSignInLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
|
||||
|
||||
|
||||
/**
|
||||
* @return a common link for logout, not related with the user context
|
||||
*/
|
||||
@NotNull
|
||||
String getSignOutLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
|
||||
String getCommonSignOutLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
|
||||
|
||||
default String getUserSignOutLink(
|
||||
@NotNull SMAuthProviderCustomConfiguration providerConfig,
|
||||
@NotNull Map<String, Object> userCredentials
|
||||
) throws DBException {
|
||||
return getCommonSignOutLink(providerConfig.getId(), providerConfig.getParameters());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getMetadataLink(String id, @NotNull Map<String, Object> providerConfig) throws DBException;
|
||||
|
||||
@@ -131,8 +131,6 @@ public class WebAuthInfo implements SMSessionPrincipal {
|
||||
authProviderInstance.closeSession(session, authSession);
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
} finally {
|
||||
authSession = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -614,7 +614,7 @@ public class WebSession extends BaseWebSession
|
||||
super.close();
|
||||
}
|
||||
|
||||
private void clearAuthTokens() throws DBException {
|
||||
private List<WebAuthInfo> clearAuthTokens() throws DBException {
|
||||
ArrayList<WebAuthInfo> tokensCopy;
|
||||
synchronized (authTokens) {
|
||||
tokensCopy = new ArrayList<>(this.authTokens);
|
||||
@@ -623,6 +623,7 @@ public class WebSession extends BaseWebSession
|
||||
removeAuthInfo(ai);
|
||||
}
|
||||
resetAuthToken();
|
||||
return tokensCopy;
|
||||
}
|
||||
|
||||
public DBRProgressMonitor getProgressMonitor() {
|
||||
@@ -873,18 +874,23 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAuthInfo(String providerId) throws DBException {
|
||||
public List<WebAuthInfo> removeAuthInfo(String providerId) throws DBException {
|
||||
List<WebAuthInfo> oldInfo;
|
||||
if (providerId == null) {
|
||||
clearAuthTokens();
|
||||
oldInfo = clearAuthTokens();
|
||||
} else {
|
||||
WebAuthInfo authInfo = getAuthInfo(providerId);
|
||||
if (authInfo != null) {
|
||||
removeAuthInfo(authInfo);
|
||||
oldInfo = List.of(authInfo);
|
||||
} else {
|
||||
oldInfo = List.of();
|
||||
}
|
||||
}
|
||||
if (authTokens.isEmpty()) {
|
||||
resetUserState();
|
||||
}
|
||||
return oldInfo;
|
||||
}
|
||||
|
||||
public List<DBACredentialsProvider> getContextCredentialsProviders() {
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ public class WebAuthProviderConfiguration {
|
||||
public String getSignOutLink() throws DBException {
|
||||
SMAuthProvider<?> instance = providerDescriptor.getInstance();
|
||||
return instance instanceof SMAuthProviderFederated
|
||||
? ((SMAuthProviderFederated) instance).getSignOutLink(getId(), config.getParameters())
|
||||
? ((SMAuthProviderFederated) instance).getCommonSignOutLink(getId(), config.getParameters())
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ type AuthInfo {
|
||||
userTokens: [UserAuthToken!]
|
||||
}
|
||||
|
||||
|
||||
type LogoutInfo @since(version: "23.3.3") {
|
||||
redirectLinks: [String!]!
|
||||
}
|
||||
|
||||
type UserAuthToken {
|
||||
# Auth provider used for authorization
|
||||
authProvider: ID!
|
||||
@@ -139,8 +144,13 @@ extend type Query {
|
||||
authUpdateStatus(authId: ID!, linkUser: Boolean): AuthInfo!
|
||||
|
||||
# Logouts user. If provider not specified then all authorizations are revoked from session.
|
||||
@deprecated
|
||||
authLogout(provider: ID, configuration: ID): Boolean
|
||||
|
||||
# Same as #authLogout, but returns additional information
|
||||
@since(version: "23.3.3")
|
||||
authLogoutExtended(provider: ID, configuration: ID): LogoutInfo!
|
||||
|
||||
# Active user information. null is no user was authorized within session
|
||||
activeUser: UserInfo
|
||||
|
||||
|
||||
+5
-1
@@ -45,7 +45,11 @@ public interface DBWServiceAuth extends DBWService {
|
||||
WebAuthStatus authUpdateStatus(@NotNull WebSession webSession, @NotNull String authId, boolean linkWithActiveUser) throws DBWebException;
|
||||
|
||||
@WebAction(authRequired = false)
|
||||
void authLogout(@NotNull WebSession webSession, @Nullable String providerId, @Nullable String configurationId) throws DBWebException;
|
||||
WebLogoutInfo authLogout(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String providerId,
|
||||
@Nullable String configurationId
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction(authRequired = false)
|
||||
WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException;
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.auth;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record WebLogoutInfo(@NotNull List<String> redirectLinks) {
|
||||
}
|
||||
+7
-4
@@ -42,12 +42,15 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
|
||||
env.getArgument("configuration"),
|
||||
env.getArgument("credentials"),
|
||||
CommonUtils.toBoolean(env.getArgument("linkUser"))))
|
||||
.dataFetcher("authLogoutExtended", env -> getService(env).authLogout(
|
||||
getWebSession(env, false),
|
||||
env.getArgument("provider"),
|
||||
env.getArgument("configuration")
|
||||
))
|
||||
.dataFetcher("authLogout", env -> {
|
||||
getService(env).authLogout(
|
||||
getWebSession(env, false),
|
||||
getService(env).authLogout(getWebSession(env, false),
|
||||
env.getArgument("provider"),
|
||||
env.getArgument("configuration")
|
||||
);
|
||||
env.getArgument("configuration"));
|
||||
return true;
|
||||
})
|
||||
.dataFetcher("authUpdateStatus", env -> getService(env).authUpdateStatus(
|
||||
|
||||
+24
-2
@@ -18,6 +18,7 @@ package io.cloudbeaver.service.auth.impl;
|
||||
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.WebServiceUtils;
|
||||
import io.cloudbeaver.auth.SMAuthProviderFederated;
|
||||
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
|
||||
import io.cloudbeaver.model.WebPropertyInfo;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
@@ -31,6 +32,7 @@ import io.cloudbeaver.registry.WebMetaParametersRegistry;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.service.auth.DBWServiceAuth;
|
||||
import io.cloudbeaver.service.auth.WebAuthStatus;
|
||||
import io.cloudbeaver.service.auth.WebLogoutInfo;
|
||||
import io.cloudbeaver.service.auth.WebUserInfo;
|
||||
import io.cloudbeaver.service.security.SMUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
@@ -39,6 +41,7 @@ import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
|
||||
import org.jkiss.dbeaver.model.auth.SMSessionExternal;
|
||||
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
|
||||
import org.jkiss.dbeaver.model.security.SMController;
|
||||
import org.jkiss.dbeaver.model.security.SMSubjectType;
|
||||
@@ -131,7 +134,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authLogout(
|
||||
public WebLogoutInfo authLogout(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String providerId,
|
||||
@Nullable String configurationId
|
||||
@@ -140,7 +143,26 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
|
||||
throw new DBWebException("Not logged in");
|
||||
}
|
||||
try {
|
||||
webSession.removeAuthInfo(providerId);
|
||||
List<WebAuthInfo> removedInfos = webSession.removeAuthInfo(providerId);
|
||||
List<String> logoutUrls = new ArrayList<>();
|
||||
var cbApp = CBApplication.getInstance();
|
||||
for (WebAuthInfo removedInfo : removedInfos) {
|
||||
if (removedInfo.getAuthProviderDescriptor()
|
||||
.getInstance() instanceof SMAuthProviderFederated federatedProvider
|
||||
&& removedInfo.getAuthSession() instanceof SMSessionExternal externalSession
|
||||
) {
|
||||
var providerConfig =
|
||||
cbApp.getAuthConfiguration().getAuthProviderConfiguration(removedInfo.getAuthConfiguration());
|
||||
if (providerConfig == null) {
|
||||
log.warn(removedInfo.getAuthConfiguration() + " provider configuration wasn't found");
|
||||
continue;
|
||||
}
|
||||
String logoutUrl = federatedProvider.getUserSignOutLink(providerConfig,
|
||||
externalSession.getAuthParameters());
|
||||
logoutUrls.add(logoutUrl);
|
||||
}
|
||||
}
|
||||
return new WebLogoutInfo(logoutUrls);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("User logout failed", e);
|
||||
}
|
||||
|
||||
+8
-4
@@ -1348,7 +1348,8 @@ public class CBEmbeddedSecurityController<T extends WebAuthApplication>
|
||||
var authProviderFederated = (SMAuthProviderFederated) authProviderInstance;
|
||||
String signInLink = buildRedirectLink(authProviderFederated.getSignInLink(authProviderConfigurationId, Map.of()),
|
||||
authAttemptId);
|
||||
String signOutLink = authProviderFederated.getSignOutLink(authProviderConfigurationId, Map.of());
|
||||
String signOutLink = authProviderFederated.getCommonSignOutLink(authProviderConfigurationId,
|
||||
Map.of());
|
||||
Map<SMAuthConfigurationReference, Object> authData = Map.of(new SMAuthConfigurationReference(authProviderId,
|
||||
authProviderConfigurationId), filteredUserCreds);
|
||||
return SMAuthInfo.inProgress(authAttemptId, signInLink, signOutLink, authData, isMainSession);
|
||||
@@ -1621,9 +1622,12 @@ public class CBEmbeddedSecurityController<T extends WebAuthApplication>
|
||||
signInLink = buildRedirectLink(((SMAuthProviderFederated) authProviderInstance).getRedirectLink(
|
||||
authProviderConfiguration,
|
||||
Map.of()), authId);
|
||||
signOutLink = buildRedirectLink(((SMAuthProviderFederated) authProviderInstance).getSignOutLink(
|
||||
authProviderConfiguration,
|
||||
Map.of()), authId);
|
||||
var userCustomSignOutLink =
|
||||
((SMAuthProviderFederated) authProviderInstance).getUserSignOutLink(
|
||||
application.getAuthConfiguration()
|
||||
.getAuthProviderConfiguration(authProviderConfiguration),
|
||||
authProviderData);
|
||||
signOutLink = userCustomSignOutLink;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,31 +25,6 @@ export class AuthInfoService {
|
||||
return this.userInfoResource.data;
|
||||
}
|
||||
|
||||
get userAuthConfigurations(): IUserAuthConfiguration[] {
|
||||
const tokens = this.userInfo?.authTokens;
|
||||
const result: IUserAuthConfiguration[] = [];
|
||||
|
||||
if (!tokens) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.authConfiguration) {
|
||||
const provider = this.authProvidersResource.values.find(provider => provider.id === token.authProvider);
|
||||
|
||||
if (provider) {
|
||||
const configuration = provider.configurations?.find(configuration => configuration.id === token.authConfiguration);
|
||||
|
||||
if (configuration) {
|
||||
result.push({ providerId: provider.id, configuration });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly userInfoResource: UserInfoResource,
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { injectable } from '@cloudbeaver/core-di';
|
||||
import { AutoRunningTask, ISyncExecutor, ITask, SyncExecutor, whileTask } from '@cloudbeaver/core-executor';
|
||||
import { CachedDataResource, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource';
|
||||
import { SessionDataResource, SessionResource } from '@cloudbeaver/core-root';
|
||||
import { AuthInfo, AuthStatus, GetActiveUserQueryVariables, GraphQLService, UserInfo } from '@cloudbeaver/core-sdk';
|
||||
import { AuthInfo, AuthLogoutQuery, AuthStatus, GetActiveUserQueryVariables, GraphQLService, UserInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID';
|
||||
import { AuthProviderService } from './AuthProviderService';
|
||||
@@ -20,6 +20,8 @@ import type { IAuthCredentials } from './IAuthCredentials';
|
||||
|
||||
export type UserInfoIncludes = GetActiveUserQueryVariables;
|
||||
|
||||
export type UserLogoutInfo = AuthLogoutQuery['result'];
|
||||
|
||||
export interface ILoginOptions {
|
||||
credentials?: IAuthCredentials;
|
||||
configurationId?: string;
|
||||
@@ -151,8 +153,8 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
|
||||
);
|
||||
}
|
||||
|
||||
async logout(provider?: string, configuration?: string): Promise<void> {
|
||||
await this.graphQLService.sdk.authLogout({
|
||||
async logout(provider?: string, configuration?: string): Promise<AuthLogoutQuery> {
|
||||
const result = await this.graphQLService.sdk.authLogout({
|
||||
provider,
|
||||
configuration,
|
||||
});
|
||||
@@ -160,6 +162,8 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
|
||||
this.resetIncludes();
|
||||
this.setData(await this.loader());
|
||||
this.sessionDataResource.markOutdated();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async setConfigurationParameter(key: string, value: any): Promise<UserInfo | null> {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
query authLogout(
|
||||
$provider: ID
|
||||
$configuration: ID
|
||||
) {
|
||||
authLogout(
|
||||
provider: $provider
|
||||
configuration: $configuration
|
||||
)
|
||||
}
|
||||
query authLogout($provider: ID, $configuration: ID) {
|
||||
result: authLogoutExtended(provider: $provider, configuration: $configuration) {
|
||||
redirectLinks
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import {
|
||||
AppAuthService,
|
||||
AUTH_PROVIDER_LOCAL_ID,
|
||||
AuthInfoService,
|
||||
AuthProviderContext,
|
||||
AuthProviderService,
|
||||
AuthProvidersResource,
|
||||
IUserAuthConfiguration,
|
||||
RequestedProvider,
|
||||
UserInfoResource,
|
||||
UserLogoutInfo,
|
||||
} from '@cloudbeaver/core-authentication';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import type { DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
@@ -27,6 +26,7 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { ISessionAction, ServerConfigResource, sessionActionContext, SessionActionService, SessionDataResource } from '@cloudbeaver/core-root';
|
||||
import { ScreenService, WindowsService } from '@cloudbeaver/core-routing';
|
||||
import { NavigationService } from '@cloudbeaver/core-ui';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { AuthDialogService } from './Dialog/AuthDialogService';
|
||||
import type { IAuthOptions } from './IAuthOptions';
|
||||
@@ -54,7 +54,6 @@ export class AuthenticationService extends Bootstrap {
|
||||
private readonly authProviderService: AuthProviderService,
|
||||
private readonly authProvidersResource: AuthProvidersResource,
|
||||
private readonly sessionDataResource: SessionDataResource,
|
||||
private readonly authInfoService: AuthInfoService,
|
||||
private readonly serverConfigResource: ServerConfigResource,
|
||||
private readonly windowsService: WindowsService,
|
||||
private readonly sessionActionService: SessionActionService,
|
||||
@@ -91,22 +90,10 @@ export class AuthenticationService extends Bootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
let userAuthConfiguration: IUserAuthConfiguration | undefined = undefined;
|
||||
|
||||
if (providerId) {
|
||||
userAuthConfiguration = this.authInfoService.userAuthConfigurations.find(
|
||||
c => c.providerId === providerId && c.configuration.id === configurationId,
|
||||
);
|
||||
} else if (this.authInfoService.userAuthConfigurations.length > 0) {
|
||||
userAuthConfiguration = this.authInfoService.userAuthConfigurations[0];
|
||||
}
|
||||
|
||||
if (userAuthConfiguration?.configuration.signOutLink) {
|
||||
this.logoutConfiguration(userAuthConfiguration);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userInfoResource.logout(providerId, configurationId);
|
||||
const logoutResult = await this.userInfoResource.logout(providerId, configurationId);
|
||||
|
||||
this.handleRedirectLinks(logoutResult.result);
|
||||
|
||||
if (!this.administrationScreenService.isConfigurationMode && !providerId) {
|
||||
this.screenService.navigateToRoot();
|
||||
@@ -114,15 +101,20 @@ export class AuthenticationService extends Bootstrap {
|
||||
|
||||
await this.onLogout.execute('after');
|
||||
} catch (exception: any) {
|
||||
this.notificationService.logException(exception, "Can't logout");
|
||||
this.notificationService.logException(exception, 'authentication_logout_error');
|
||||
}
|
||||
}
|
||||
|
||||
private async logoutConfiguration(configuration: IUserAuthConfiguration): Promise<void> {
|
||||
if (configuration.configuration.signOutLink) {
|
||||
const id = `${configuration.configuration.id}-sign-out`;
|
||||
// TODO handle all redirect links once we know what to do with multiple popups issue
|
||||
private handleRedirectLinks(userLogoutInfo: UserLogoutInfo) {
|
||||
const redirectLinks = userLogoutInfo.redirectLinks;
|
||||
|
||||
if (redirectLinks.length) {
|
||||
const url = redirectLinks[0];
|
||||
const id = `okta-logout-id-${uuid()}`;
|
||||
|
||||
const popup = this.windowsService.open(id, {
|
||||
url: configuration.configuration.signOutLink,
|
||||
url,
|
||||
target: id,
|
||||
width: 600,
|
||||
height: 700,
|
||||
|
||||
@@ -2,6 +2,7 @@ export default [
|
||||
['authentication_login_dialog_title', 'Authentication'],
|
||||
['authentication_login', 'Login'],
|
||||
['authentication_logout', 'Logout'],
|
||||
['authentication_logout_error', "Can't logout"],
|
||||
['authentication_authenticate', 'Authenticate'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', 'Federated'],
|
||||
|
||||
@@ -2,6 +2,7 @@ export default [
|
||||
['authentication_login_dialog_title', 'Autenticazione'],
|
||||
['authentication_login', 'Login'],
|
||||
['authentication_logout', 'Logout'],
|
||||
['authentication_logout_error', "Can't logout"],
|
||||
['authentication_authenticate', 'Autentica'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', 'Federated'],
|
||||
|
||||
@@ -2,6 +2,7 @@ export default [
|
||||
['authentication_login_dialog_title', 'Аутентификация'],
|
||||
['authentication_login', 'Войти'],
|
||||
['authentication_logout', 'Выйти'],
|
||||
['authentication_logout_error', 'Не удалось выйти'],
|
||||
['authentication_authenticate', 'Аутентифицироваться'],
|
||||
['authentication_authorizing', 'Авторизация...'],
|
||||
['authentication_auth_federated', 'Федеративная'],
|
||||
|
||||
@@ -2,6 +2,7 @@ export default [
|
||||
['authentication_login_dialog_title', '认证'],
|
||||
['authentication_login', '登录'],
|
||||
['authentication_logout', '登出'],
|
||||
['authentication_logout_error', "Can't logout"],
|
||||
['authentication_authenticate', '认证'],
|
||||
['authentication_authorizing', 'Authorizing...'],
|
||||
['authentication_auth_federated', '联合认证'],
|
||||
|
||||
Reference in New Issue
Block a user