Skip to content

Commit 51a2d66

Browse files
authored
feat: implement button "login with SSO" (#4242)
1 parent de285dc commit 51a2d66

8 files changed

Lines changed: 131 additions & 7 deletions

File tree

‎src/containers/App/Content.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,20 @@ function ContentWrapper(props: ContentWrapperProps) {
278278
if (authUnavailable || metaAuthUnavailable) {
279279
return <AccessDenied />;
280280
}
281-
return <Authentication />;
281+
return (
282+
<GetMetaCapabilities>
283+
<Authentication />
284+
</GetMetaCapabilities>
285+
);
282286
};
283287

284288
return (
285289
<Switch>
286290
{!authUnavailable && !metaAuthUnavailable && (
287291
<Route path={routes.auth}>
288-
<Authentication closable />
292+
<GetMetaCapabilities>
293+
<Authentication closable />
294+
</GetMetaCapabilities>
289295
</Route>
290296
)}
291297
<Route>

‎src/containers/Authentication/Authentication.scss‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@
7777
justify-content: center;
7878
}
7979

80+
&__button-sso {
81+
margin-top: var(--g-spacing-2);
82+
}
83+
8084
&__show-password-button {
8185
margin-left: 4px;
8286
}

‎src/containers/Authentication/Authentication.tsx‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import React from 'react';
22

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

7-
import {parseQuery} from '../../routes';
7+
import routes, {getHomePagePath, parseQuery} from '../../routes';
88
import {basename} from '../../store';
99
import {authenticationApi} from '../../store/reducers/authentication/authentication';
10-
import {useLoginWithDatabase} from '../../store/reducers/capabilities/hooks';
10+
import {useLoginWithDatabase, useOidcAvailable} from '../../store/reducers/capabilities/hooks';
1111
import {cn} from '../../utils/cn';
1212
import {BRAND_BUTTON_CLASS} from '../../utils/constants';
1313
import {prepareCommonErrorMessage} from '../../utils/errors';
1414
import {useMetaAuth} from '../../utils/hooks/useMetaAuth';
1515

1616
import i18n from './i18n';
17-
import {isDatabaseError, isPasswordError, isUserError} from './utils';
17+
import {
18+
createSsoAuthorizeUrl,
19+
getSsoReturnTo,
20+
isDatabaseError,
21+
isPasswordError,
22+
isUserError,
23+
} from './utils';
1824

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

@@ -29,12 +35,15 @@ interface AuthenticationProps {
2935
function Authentication({closable = false}: AuthenticationProps) {
3036
const history = useHistory();
3137
const location = useLocation();
38+
const isDirectAuthPage = Boolean(useRouteMatch({path: routes.auth, exact: true}));
3239

3340
const needDatabase = useLoginWithDatabase();
41+
const oidcAvailable = useOidcAvailable();
3442

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

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

3948
const path = React.useMemo(() => {
4049
let path: string | undefined;
@@ -65,6 +74,22 @@ function Authentication({closable = false}: AuthenticationProps) {
6574

6675
const useMeta = useMetaAuth(path);
6776

77+
const ssoUrl = React.useMemo(() => {
78+
if (!oidcAvailable) {
79+
return undefined;
80+
}
81+
82+
const homePath = getHomePagePath(undefined, undefined, {withBasename: true});
83+
const currentUrl = new URL(currentHref);
84+
const returnTo = getSsoReturnTo({
85+
currentUrl,
86+
fallbackPath: homePath,
87+
isDirectAuthPage,
88+
returnUrl,
89+
});
90+
return createSsoAuthorizeUrl(currentUrl.host, returnTo);
91+
}, [currentHref, isDirectAuthPage, oidcAvailable, returnUrl]);
92+
6893
const [login, setLogin] = React.useState('');
6994
const [database, setDatabase] = React.useState(databaseFromQuery?.toString() || undefined);
7095
const [password, setPass] = React.useState('');
@@ -208,6 +233,17 @@ function Authentication({closable = false}: AuthenticationProps) {
208233
>
209234
Sign in
210235
</Button>
236+
{ssoUrl && (
237+
<Button
238+
view="outlined"
239+
href={ssoUrl}
240+
width="max"
241+
size="l"
242+
className={b('button-sso')}
243+
>
244+
{i18n('action_via-sso')}
245+
</Button>
246+
)}
211247
{/* always preserve place for general error to prevent container height jumping */}
212248
<div className={b('general-error')}>{generalError}</div>
213249
</form>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {getSsoReturnTo} from '../utils';
2+
3+
describe('getSsoReturnTo', () => {
4+
test('falls back when the implicit current path is not a safe local path', () => {
5+
const fallbackPath = '/ui/home';
6+
7+
expect(
8+
getSsoReturnTo({
9+
currentUrl: new URL('https://trusted-host//attacker.example'),
10+
fallbackPath,
11+
isDirectAuthPage: false,
12+
returnUrl: undefined,
13+
}),
14+
).toBe(fallbackPath);
15+
});
16+
});

‎src/containers/Authentication/i18n/en.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"description_default-error": "Unknown error occurred",
33
"action_show-password": "Show password",
44
"action_hide-password": "Hide password",
5-
"action_close": "Close"
5+
"action_close": "Close",
6+
"action_via-sso": "via SSO"
67
}

‎src/containers/Authentication/utils.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,58 @@ interface AuthError {
44
};
55
}
66

7+
export function createSsoAuthorizeUrl(host: string, returnTo: string) {
8+
const url = new URL('/meta/oidc/authorize', `https://${host}`);
9+
url.searchParams.set('return_to', returnTo);
10+
11+
return url.href;
12+
}
13+
14+
interface GetSsoReturnToParams {
15+
currentUrl: URL;
16+
fallbackPath: string;
17+
isDirectAuthPage: boolean;
18+
returnUrl: unknown;
19+
}
20+
21+
function isSafeLocalReturnTo(path: string) {
22+
return (
23+
path.startsWith('/') &&
24+
!path.startsWith('//') &&
25+
!path.includes('\\') &&
26+
!path.includes('\r') &&
27+
!path.includes('\n')
28+
);
29+
}
30+
31+
export function getSsoReturnTo({
32+
currentUrl,
33+
fallbackPath,
34+
isDirectAuthPage,
35+
returnUrl,
36+
}: GetSsoReturnToParams) {
37+
if (!isDirectAuthPage) {
38+
const path = `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`;
39+
return isSafeLocalReturnTo(path) ? path : fallbackPath;
40+
}
41+
42+
if (typeof returnUrl !== 'string') {
43+
return fallbackPath;
44+
}
45+
46+
try {
47+
const savedReturnUrl = new URL(decodeURIComponent(returnUrl));
48+
if (savedReturnUrl.origin !== currentUrl.origin) {
49+
return fallbackPath;
50+
}
51+
52+
const path = `${savedReturnUrl.pathname}${savedReturnUrl.search}${savedReturnUrl.hash}`;
53+
return isSafeLocalReturnTo(path) ? path : fallbackPath;
54+
} catch {
55+
return fallbackPath;
56+
}
57+
}
58+
759
function isAuthError(error: unknown): error is AuthError {
860
return Boolean(
961
error &&

‎src/store/reducers/capabilities/hooks.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export const useSchemaTopicDataAvailable = () => {
6060
return useGetMetaFeatureVersion('/meta/schema_topic_data') >= 1;
6161
};
6262

63+
export const useOidcAvailable = () => {
64+
const authorizeAvailable = useGetMetaFeatureVersion('/meta/oidc/authorize') >= 1;
65+
const callbackAvailable = useGetMetaFeatureVersion('/meta/oidc/callback') >= 1;
66+
67+
return authorizeAvailable && callbackAvailable;
68+
};
69+
6370
export const useCreateDirectoryFeatureAvailable = () => {
6471
return useGetFeatureVersion('/scheme/directory') > 0;
6572
};

‎src/types/api/capabilities.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export type MetaCapability =
6060
| '/meta/delete_cluster'
6161
| '/meta/events'
6262
| '/meta/login'
63+
| '/meta/oidc/authorize'
64+
| '/meta/oidc/callback'
6365
| '/meta/whoami'
6466
| '/meta/databases'
6567
| '/meta/environments'

0 commit comments

Comments
 (0)