Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/containers/Authentication/Authentication.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
justify-content: center;
}

&__button-sso {
margin-top: 8px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use variable

}

&__show-password-button {
margin-left: 4px;
}
Expand Down
49 changes: 45 additions & 4 deletions src/containers/Authentication/Authentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ import React from 'react';

import {Eye, EyeSlash, Xmark} from '@gravity-ui/icons';
import {ActionTooltip, Button, Link as ExternalLink, Icon, TextInput} from '@gravity-ui/uikit';
import {useHistory, useLocation} from 'react-router-dom';
import {useHistory, useLocation, useRouteMatch} from 'react-router-dom';

import {parseQuery} from '../../routes';
import routes, {getHomePagePath, parseQuery} from '../../routes';
import {basename} from '../../store';
import {authenticationApi} from '../../store/reducers/authentication/authentication';
import {useLoginWithDatabase} from '../../store/reducers/capabilities/hooks';
import {
useLoginWithDatabase,
useMetaCapabilitiesQuery,
useOidcAvailable,
} from '../../store/reducers/capabilities/hooks';
import {cn} from '../../utils/cn';
import {BRAND_BUTTON_CLASS} from '../../utils/constants';
import {prepareCommonErrorMessage} from '../../utils/errors';
import {useMetaAuth} from '../../utils/hooks/useMetaAuth';

import i18n from './i18n';
import {isDatabaseError, isPasswordError, isUserError} from './utils';
import {
createSsoAuthorizeUrl,
getSsoReturnTo,
isDatabaseError,
isPasswordError,
isUserError,
} from './utils';

import ydbLogoIcon from '../../assets/icons/ydb.svg';

Expand All @@ -29,12 +39,16 @@ interface AuthenticationProps {
function Authentication({closable = false}: AuthenticationProps) {
const history = useHistory();
const location = useLocation();
const isDirectAuthPage = Boolean(useRouteMatch({path: routes.auth, exact: true}));

const needDatabase = useLoginWithDatabase();
useMetaCapabilitiesQuery();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's better wrap Authentication with GetMetaCapabilities (see src/containers/App/Content)

const oidcAvailable = useOidcAvailable();

const [authenticate, {isLoading}] = authenticationApi.useAuthenticateMutation();

const {returnUrl, database: databaseFromQuery} = parseQuery(location);
const currentHref = window.location.href;

const path = React.useMemo(() => {
let path: string | undefined;
Expand Down Expand Up @@ -65,6 +79,22 @@ function Authentication({closable = false}: AuthenticationProps) {

const useMeta = useMetaAuth(path);

const ssoUrl = React.useMemo(() => {
if (!oidcAvailable) {
return undefined;
}

const homePath = getHomePagePath(undefined, undefined, {withBasename: true});
const currentUrl = new URL(currentHref);
const returnTo = getSsoReturnTo({
currentUrl,
fallbackPath: homePath,
isDirectAuthPage,
returnUrl,
});
return createSsoAuthorizeUrl(currentUrl.host, returnTo);
}, [currentHref, isDirectAuthPage, oidcAvailable, returnUrl]);

const [login, setLogin] = React.useState('');
const [database, setDatabase] = React.useState(databaseFromQuery?.toString() || undefined);
const [password, setPass] = React.useState('');
Expand Down Expand Up @@ -208,6 +238,17 @@ function Authentication({closable = false}: AuthenticationProps) {
>
Sign in
</Button>
{ssoUrl && (
<Button
view="outlined"
href={ssoUrl}
width="max"
size="l"
className={b('button-sso')}
>
{i18n('action_via-sso')}
</Button>
)}
{/* always preserve place for general error to prevent container height jumping */}
<div className={b('general-error')}>{generalError}</div>
</form>
Expand Down
3 changes: 2 additions & 1 deletion src/containers/Authentication/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"description_default-error": "Unknown error occurred",
"action_show-password": "Show password",
"action_hide-password": "Hide password",
"action_close": "Close"
"action_close": "Close",
"action_via-sso": "via SSO"
}
51 changes: 51 additions & 0 deletions src/containers/Authentication/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ interface AuthError {
};
}

export function createSsoAuthorizeUrl(host: string, returnTo: string) {
const url = new URL('/meta/oidc/authorize', `https://${host}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build the authorize URL from the configured meta backend

In the documented multi-cluster setup where REACT_APP_META_BACKEND can be a different URL such as http://your-meta-host:8765, BaseMetaAPI fetches capabilities from that configured backend, but this line redirects to https://<current UI host>/meta/oidc/authorize. The SSO button consequently targets the wrong host and protocol; derive the authorize endpoint from the configured meta-backend base while preserving its scheme and path prefix.

AGENTS.md reference: AGENTS.md:L489-L493

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current URL is intentional and follows #257: the UI and YDB Meta are expected to use the same host.
Using REACT_APP_META_BACKEND is not enough for a separate or prefixed backend. After SSO, the browser returns to /meta/oidc/callback, where the session cookie is created. If Meta uses another host, the cookie will belong to that host rather than the UI host.
Supporting a separate Meta host or /api/meta3 requires additional proxy/backend integration, so it is outside the scope of this change.

url.searchParams.set('return_to', returnTo);

return url.href;
}

interface GetSsoReturnToParams {
currentUrl: URL;
fallbackPath: string;
isDirectAuthPage: boolean;
returnUrl: unknown;
}

function isSafeLocalReturnTo(path: string) {
return (
path.startsWith('/') &&
!path.startsWith('//') &&
!path.includes('\\') &&
!path.includes('\r') &&
!path.includes('\n')
);
}

export function getSsoReturnTo({
currentUrl,
fallbackPath,
isDirectAuthPage,
returnUrl,
}: GetSsoReturnToParams) {
if (!isDirectAuthPage) {
return `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
Comment thread
kkdras marked this conversation as resolved.
Outdated
}

if (typeof returnUrl !== 'string') {
return fallbackPath;
}

try {
const savedReturnUrl = new URL(decodeURIComponent(returnUrl));
if (savedReturnUrl.origin !== currentUrl.origin) {
return fallbackPath;
}

const path = `${savedReturnUrl.pathname}${savedReturnUrl.search}${savedReturnUrl.hash}`;
return isSafeLocalReturnTo(path) ? path : fallbackPath;
} catch {
return fallbackPath;
}
}

function isAuthError(error: unknown): error is AuthError {
return Boolean(
error &&
Expand Down
7 changes: 7 additions & 0 deletions src/store/reducers/capabilities/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export const useSchemaTopicDataAvailable = () => {
return useGetMetaFeatureVersion('/meta/schema_topic_data') >= 1;
};

export const useOidcAvailable = () => {
const authorizeAvailable = useGetMetaFeatureVersion('/meta/oidc/authorize') >= 1;
const callbackAvailable = useGetMetaFeatureVersion('/meta/oidc/callback') >= 1;

return authorizeAvailable && callbackAvailable;
};

export const useCreateDirectoryFeatureAvailable = () => {
return useGetFeatureVersion('/scheme/directory') > 0;
};
Expand Down
2 changes: 2 additions & 0 deletions src/types/api/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export type MetaCapability =
| '/meta/delete_cluster'
| '/meta/events'
| '/meta/login'
| '/meta/oidc/authorize'
| '/meta/oidc/callback'
| '/meta/whoami'
| '/meta/databases'
| '/meta/environments'
Expand Down
Loading