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:
Alexander Skoblikov
2024-01-17 20:29:31 +03:00
committed by GitHub
co-authored by s.teleshev Daria Marutkina
parent 5db30be61b
commit 0544547f27
18 changed files with 131 additions and 82 deletions
@@ -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;
}
}
}
@@ -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() {
@@ -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
@@ -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;
@@ -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) {
}
@@ -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(
@@ -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);
}
@@ -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', '联合认证'],