Skip to content

Commit

Permalink
Merge pull request #616 from aws-samples/feat/extension-saml
Browse files Browse the repository at this point in the history
ブラウザ拡張機能: SAML認証対応
  • Loading branch information
tbrand authored Aug 26, 2024
2 parents 08145d6 + 218b92c commit 26ee4cf
Show file tree
Hide file tree
Showing 23 changed files with 523 additions and 96 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
node_modules

!.gitkeep
!.gitkeep

/*/dist/**
12 changes: 8 additions & 4 deletions browser-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

- [ビルド手順](../docs/EXTENSION_BUILD.md)
- [インストール手順](../docs/EXTENSION_INSTALL.md)
- [SAML 認証の利用方法](../docs/EXTENSION_SAML.md)

## FAQ

Expand All @@ -57,9 +58,12 @@
- Web アプリから保存したシステムコンテキストを利用できます。
- 拡張機能の「プロンプト設定」画面から、利用設定を行なってください。
- ログインに失敗します
- 以下のいずれかの原因が考えられます。
- ユーザが登録されていない。
- Web アプリからログインできるか確認してください。
- 設定が間違っている
- まずは、GenU の Web アプリから正常にログインできるか、ご確認ください。
- Web アプリにログインできない場合
- ユーザー登録されていない可能性があります。
- Web アプリにログインできる場合
- 拡張機能の設定が間違っている可能性があります。
- 拡張機能の「設定」画面を開き、各設定項目が正しいかご確認ください。
- 設定値は、[こちらの方法](../docs/EXTENSION_BUILD.md#その他のユーザー-windows-等)で確認できます。
- SAML 連携を利用している場合、Cognito の設定が間違っている可能性があります。
- [こちらの手順](../docs/EXTENSION_SAML.md)を参考に、Cognito の設定を確認してください。
4 changes: 4 additions & 0 deletions browser-extension/src/@types/settings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type Settings = {
lambdaArn: strng;
region: string;
apiEndpoint: string;
enabledSamlAuth: boolean;
enabledSelfSignUp: boolean;
cognitoDomain?: string;
federatedIdentityProviderName?: string;
};

export type PromptSetting = SystemContext & {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect } from 'react';
import { Amplify } from 'aws-amplify';
import '@aws-amplify/ui-react/styles.css';
import useSettings from '../../settings/useSettings';
import Browser from 'webextension-polyfill';
import useAuth from '../hooks/useAuth';
import Button from './Button';
import { PiCircleNotchBold } from 'react-icons/pi';

type Props = {
children: React.ReactNode;
};

const AuthWithSAML: React.FC<Props> = (props) => {
const { settings } = useSettings();
const { loading, authenticate, hasAuthenticated } = useAuth();

useEffect(() => {
if (settings) {
Amplify.configure({
Auth: {
Cognito: {
userPoolId: import.meta.env.VITE_APP_USER_POOL_ID,
userPoolClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID,
identityPoolId: import.meta.env.VITE_APP_IDENTITY_POOL_ID,
loginWith: {
oauth: {
domain: settings.cognitoDomain ?? '',
scopes: ['openid', 'email', 'profile'],
redirectSignIn: [`${window.location.origin}/index.html`],
redirectSignOut: [window.location.origin],
responseType: 'code',
},
},
},
},
});
// 認証の設定をしたら認証を実行
authenticate();
}
}, [authenticate, settings]);

const signIn = () => {
const url = Browser.runtime.getURL('/index.html');
Browser.tabs.create({ url });
};

return (
<div className="flex justify-center mt-3">
{loading ? (
<div>
<div className="italic">Loading...</div>
<PiCircleNotchBold className="text-6xl animate-spin" />
</div>
) : !hasAuthenticated ? (
<div>
<Button onClick={() => signIn()}>ログイン画面へ</Button>
</div>
) : (
<>{props.children}</>
)}
</div>
);
};

export default AuthWithSAML;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Authenticator, translations } from '@aws-amplify/ui-react';
import React, { useEffect, useMemo } from 'react';
import useSettings from '../../settings/useSettings';
import { Amplify } from 'aws-amplify';
import { I18n } from 'aws-amplify/utils';

type Props = {
children: React.ReactNode;
};

const AuthWithUserPool: React.FC<Props> = (props) => {
const { settings } = useSettings();

useEffect(() => {
if (settings) {
Amplify.configure({
Auth: {
Cognito: {
userPoolId: settings.userPoolId,
userPoolClientId: settings.userPoolClientId,
identityPoolId: settings.identityPoolId,
},
},
});
}
}, [settings]);

const enabledSelfSignUp = useMemo(() => {
return settings?.enabledSelfSignUp ?? false;
}, [settings?.enabledSelfSignUp]);

I18n.putVocabularies(translations);
I18n.setLanguage('ja');

return <Authenticator hideSignUp={!enabledSelfSignUp}>{props.children}</Authenticator>;
};

export default AuthWithUserPool;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import useSettings from '../../settings/useSettings';
import AuthWithUserPool from './AuthWithUserPool';
import AuthWithSAML from './AuthWithSAML';
import { PiCircleNotchBold } from 'react-icons/pi';

type Props = {
children: React.ReactNode;
};

const RequiresAuth: React.FC<Props> = (props) => {
const { settings } = useSettings();

return (
<>
{!settings ? (
<div className="flex flex-col items-center">
<div className="italic">Loading...</div>
<PiCircleNotchBold className="text-6xl animate-spin" />
</div>
) : (
<>
{settings.enabledSamlAuth && <AuthWithSAML>{props.children}</AuthWithSAML>}
{!settings.enabledSamlAuth && <AuthWithUserPool>{props.children}</AuthWithUserPool>}
</>
)}
</>
);
};

export default RequiresAuth;
30 changes: 30 additions & 0 deletions browser-extension/src/app/features/common/components/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { BaseProps } from '../../../../@types/common';

type Props = BaseProps & {
checked: boolean;
onSwitch: (newValue: boolean) => void;
label: string;
};

const Switch: React.FC<Props> = (props) => {
return (
<div>
<label className="relative inline-flex cursor-pointer items-center hover:underline">
<input
type="checkbox"
value=""
className="peer sr-only"
checked={props.checked}
onChange={() => {
props.onSwitch(!props.checked);
}}
/>
<div className="peer-checked:bg-aws-smile peer h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:size-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white rtl:peer-checked:after:-translate-x-full"></div>
<span className="ml-1 text-xs font-medium">{props.label}</span>
</label>
</div>
);
};

export default Switch;
14 changes: 12 additions & 2 deletions browser-extension/src/app/features/common/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { fetchAuthSession, signOut } from 'aws-amplify/auth';
import { useMemo } from 'react';
import useSWR from 'swr';
import useSettings from '../../settings/useSettings';

const useAuth = () => {
const { data: session } = useSWR('auth', () => {
const { settings } = useSettings();

const { data: session, mutate } = useSWR(settings ? 'auth' : null, () => {
return fetchAuthSession();
});

const loading = useMemo(() => {
return !settings || !session;
}, [session, settings]);

return {
hasAuthrized: !!session?.tokens,
authenticate: mutate,
loading,
hasAuthenticated: !!session?.tokens,
email: (session?.tokens?.idToken?.payload.email ?? null) as string | null,
token: session?.tokens?.idToken?.toString() ?? null,
signOut,
Expand Down
120 changes: 77 additions & 43 deletions browser-extension/src/app/features/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';
import React, { useMemo } from 'react';
import InputText from '../common/components/InputText';
import useSettings from './useSettings';
import Button from '../common/components/Button';
import { PiCaretLeft } from 'react-icons/pi';
import useAuth from '../common/hooks/useAuth';
import Switch from '../common/components/Switch';

type Props = {
onBack: () => void;
};

const Settings: React.FC<Props> = (props) => {
const { settings, setSetting, save } = useSettings();
const { hasAuthrized, email, signOut } = useAuth();
const { hasAuthenticated, email, signOut } = useAuth();

const enabledSamlAuth = useMemo(() => {
return settings?.enabledSamlAuth ?? false;
}, [settings?.enabledSamlAuth]);

return (
<div className="p-2">
Expand All @@ -22,48 +27,77 @@ const Settings: React.FC<Props> = (props) => {
</div>

<div className="flex flex-col mt-3 gap-2">
<InputText
label="リージョン(Region)"
value={settings?.region ?? ''}
onChange={(val) => {
setSetting('region', val);
}}
/>
<InputText
label="ユーザプールID(UserPoolId)"
value={settings?.userPoolId ?? ''}
onChange={(val) => {
setSetting('userPoolId', val);
}}
/>
<InputText
label="ユーザープールクライアントID(UserPoolClientId)"
value={settings?.userPoolClientId ?? ''}
onChange={(val) => {
setSetting('userPoolClientId', val);
}}
/>
<InputText
label="アイデンティティプールID(IdPoolId)"
value={settings?.identityPoolId ?? ''}
onChange={(val) => {
setSetting('identityPoolId', val);
}}
/>
<InputText
label="推論関数ARN(PredictStreamFunctionArn)"
value={settings?.lambdaArn ?? ''}
onChange={(val) => {
setSetting('lambdaArn', val);
}}
/>
<InputText
label="APIエンドポイント(ApiEndpoint)"
value={settings?.apiEndpoint ?? ''}
onChange={(val) => {
setSetting('apiEndpoint', val);
<Switch
label="SAML 認証を利用する"
checked={enabledSamlAuth}
onSwitch={(val) => {
setSetting('enabledSamlAuth', val);
}}
/>
{enabledSamlAuth && (
<>
<InputText
label="Cognit ドメイン(SamlCognitoDomainName)"
value={settings?.cognitoDomain ?? ''}
onChange={(val) => {
setSetting('cognitoDomain', val);
}}
/>
<InputText
label="フェデレーテッドアイデンティティプロバイダー(SamlCognitoFederatedIdentityProviderName)"
value={settings?.federatedIdentityProviderName ?? ''}
onChange={(val) => {
setSetting('federatedIdentityProviderName', val);
}}
/>
</>
)}
{!enabledSamlAuth && (
<>
<InputText
label="リージョン(Region)"
value={settings?.region ?? ''}
onChange={(val) => {
setSetting('region', val);
}}
/>
<InputText
label="ユーザプールID(UserPoolId)"
value={settings?.userPoolId ?? ''}
onChange={(val) => {
setSetting('userPoolId', val);
}}
/>
<InputText
label="ユーザープールクライアントID(UserPoolClientId)"
value={settings?.userPoolClientId ?? ''}
onChange={(val) => {
setSetting('userPoolClientId', val);
}}
/>
<InputText
label="アイデンティティプールID(IdPoolId)"
value={settings?.identityPoolId ?? ''}
onChange={(val) => {
setSetting('identityPoolId', val);
}}
/>
<InputText
label="推論関数ARN(PredictStreamFunctionArn)"
value={settings?.lambdaArn ?? ''}
onChange={(val) => {
setSetting('lambdaArn', val);
}}
/>
<InputText
label="APIエンドポイント(ApiEndpoint)"
value={settings?.apiEndpoint ?? ''}
onChange={(val) => {
setSetting('apiEndpoint', val);
}}
/>
</>
)}
</div>
<div className="flex justify-between">
<Button className="mt-3" outlined icon={<PiCaretLeft />} onClick={props.onBack}>
Expand All @@ -77,7 +111,7 @@ const Settings: React.FC<Props> = (props) => {
<div className="mt-5">
<div className="text-base font-semibold mb-1">ログイン情報</div>
<div className="text-aws-font-color-gray">
{hasAuthrized ? (
{hasAuthenticated ? (
<div>
<div>ログイン者:{email}</div>
<div className="flex justify-end">
Expand Down
Loading

0 comments on commit 26ee4cf

Please sign in to comment.