概要
aws-amplifyの認証をシンプルに実装してみたメモ。
cognitoは前回の疎通確認したメモで作成したものに手を加えて使う。
ソースコード(client)
ソースコード(api)
ソースコード(cdk)
cognitユーザプール作成コマンド
環境
- Windows10
- gitbash
- aws-cli/2.7.21 Python/3.9.11 Windows/10 exe/AMD64 prompt/off
- cdk v2.35
- node v16.16.0
変更点
ユーザプールクライアント
amazon-cognito-identity-jsではクライアントシークレットをサポートしないので false に設定
When creating the App, the generate client secret box must be unchecked because the JavaScript SDK doesn't support apps that have a client secret.
const userPoolClient = userPool.addClient('client', {
oAuth: {
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PROFILE,
],
callbackUrls: props.callbackUrls,
logoutUrls: props.logoutUrls,
flows: { authorizationCodeGrant: true },
},
+ generateSecret: false,
idTokenValidity: Duration.minutes(5)
})
ApiGateway
ブラウザからfetch
APIでアクセスするため、ヘッダとクレデンシャルの設定を追加で行う。
(参考:Access-Control-Allow-Credentials)
const httpApi = new apigw.HttpApi(this, `${props.projectId}-apigw`, {
createDefaultStage: false,
corsPreflight: {
allowOrigins: '*',
allowMethods: [apigw.CorsHttpMethod.ANY],
+ allowHeaders: ['authorization', ' Content-Type', 'X-Api-Key',],
+ allowCredentials: true
},
})
httpApi.addRoutes({
methods: [apigw.HttpMethod.GET],
path: '/scenario',
integration: new intg.HttpLambdaIntegration(`${props.projectId}-scenarioIntegration`,handler,),
authorizer,
})
Lambdaの修正
cors用のヘッダを追加
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { format } from 'date-fns';
import { formatDate } from '@/common/index';
+ const corsHeaders = {
+ 'Content-type': 'application/json',
+ 'Access-Control-Allow-Origin': '*'
+ }
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const datetime = formatDate(new Date(event.requestContext.timeEpoch));
return {
'statusCode': 200,
+ headers: corsHeaders,
'body': JSON.stringify({
message: `hello world. ${datetime} = ${event.requestContext.timeEpoch}. now: ${format(new Date(), 'yyyy-MM-dd HH:mm:ss.SSS')}`,
})
};
};
ユーザプールの作成
今回はawsconsoleではなく、awscliから作成を行った。(参考)
#!/bin/bash
COGNITO_CLIENT_ID=hoge
COGNITO_USER_NAME=test@example.com
COGNITO_USER_PASSWORD=SamplePassw0rd!
COGNITO_UER_MAIL_ADDRESS=test@example.com
COGNITO_USER_POOL_ID=ap-northeast-1_xxxxxx
# ステータスが FORCE_CHANGE_PASSWORD のユーザを作成
# --message-action SUPPRESS : メッセージをメールアドレスに送信しない
aws cognito-idp admin-create-user \
--user-pool-id $COGNITO_USER_POOL_ID \
--username $COGNITO_USER_NAME \
--message-action SUPPRESS \
--user-attributes Name=email,Value=$COGNITO_UER_MAIL_ADDRESS
# オプション --permanent を指定することで、パスワードは恒久的なパスワードとなりステータスが CONFIRMED となる
aws cognito-idp admin-set-user-password \
--user-pool-id $COGNITO_USER_POOL_ID \
--username $COGNITO_USER_NAME \
--password $COGNITO_USER_PASSWORD \
--permanent
クライアントからの問い合わせ
基本は、Amplify を使わず React で AWS Cognito 認証を使うの通り。
サインイン
import {
createAsyncThunk,
createSlice,
SerializedError,
} from '@reduxjs/toolkit'
import { Auth } from 'aws-amplify'
interface AuthState {
username?: string
authenticated: boolean
error?: SerializedError
}
const initialState: AuthState = {
username: undefined,
authenticated: false,
error: undefined,
}
export const signIn = createAsyncThunk<AuthState, { username: string, password: string }>(
'signIn',
async ({ username, password }, thunkAPI) => {
try {
const result = await Auth.signIn(username, password)
return { username: result.username, authenticated: true }
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
},
)
export const signOut = createAsyncThunk('logout', async (_, thunkAPI) => {
try {
await Auth.signOut()
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
})
export const initUser = createAsyncThunk<AuthState>(
'initUser',
async (req, thunkAPI) => {
try {
const result = await Auth.currentAuthenticatedUser()
return { username: result.username, authenticated: true }
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
},
)
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, action) => {
state.username = action.payload.username
state.authenticated = true
})
builder.addCase(signIn.rejected, (state, action) => {
state.error = action.error
state.authenticated = false
state.username = initialState.username
})
builder.addCase(signOut.fulfilled, (state) => {
state.authenticated = false
state.username = initialState.username
})
builder.addCase(signOut.rejected, (state, action) => {
state.error = action.error
state.authenticated = false
state.username = initialState.username
})
builder.addCase(initUser.fulfilled, (state, action) => {
state.authenticated = true
state.username = action.payload.username
})
}
})
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import { authencicatedApi } from './rtkQuery/api'
import { authSlice, initUser } from './slices/auth'
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
[authencicatedApi.reducerPath]: authencicatedApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(authencicatedApi.middleware),
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
store.dispatch(initUser())
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAppDispatch } from '@/store/hooks'
import { signIn } from '@/store/slices/auth'
const Login: React.FC = () => {
const navigate = useNavigate()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const dispatch = useAppDispatch()
const executeSignIn = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const result = await dispatch(signIn({ username, password }))
if (result.meta.requestStatus !== 'fulfilled') {
alert('認証に失敗しました。')
return
}
navigate({ pathname: '/admin/top' })
}
return (
<form noValidate onSubmit={executeSignIn}>
<div>
<label htmlFor="username">メールアドレス: </label>
<input
id="username"
type="email"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">パスワード: </label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">ログイン</button>
</form>
)
}
export default Login
import React from 'react'
import { Navigate, Outlet, RouteProps } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks'
import { isUserAuthenticatedSelector } from '@/store/selectors/auth'
export const PrivateRoute: React.FC<RouteProps> = ({ element: _, ...rest }) => {
const isAuthenticated = useAppSelector(isUserAuthenticatedSelector)
if (!isAuthenticated) {
return <Navigate to="/login" />
}
const isAdminPath = rest && rest.path && rest.path.indexOf('/admin') >= 0
if (isAdminPath) {
return <Navigate to="/admin" />
}
return <Outlet />
}
ログイン状態に応じたページ遷移制御
- 認証状態 / 非認証状態で、閲覧できるページを制限する。
- react-router-domはv6を使用。
- Outlet機能を使ってグルーピング (参考: ルーターライブラリ「React Router v6」でReactをページ分けをする)
import React from 'react'
import { Route, Routes } from 'react-router-dom'
import Login from '@/components/Pages/Login'
import Redirect from '@/components/Pages/Redirect'
import Test from '@/components/Pages/Test'
import Top from '@/components/Pages/Top'
import { PrivateRoute } from './context/PriveteRoute'
import { PublicRoute } from './context/PublicRoute'
const App: React.FC = () => {
return (
<Routes>
<Route path="/admin" element={<PrivateRoute />}>
<Route path="/admin/top" element={<Top />} />
</Route>
<Route element={<PublicRoute />}>
<Route path="/" element={<Redirect />} />
<Route path="/login" element={<Login />} />
<Route path="/test" element={<Test />} />
</Route>
</Routes>
)
}
export default App
import React from 'react'
import { Outlet, RouteProps } from 'react-router-dom'
export const PublicRoute: React.FC<RouteProps> = () => {
return <Outlet />
}
import React from 'react'
import { Navigate, Outlet, RouteProps } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks'
import { isUserAuthenticatedSelector } from '@/store/selectors/auth'
export const PrivateRoute: React.FC<RouteProps> = ({ element: _, ...rest }) => {
const isAuthenticated = useAppSelector(isUserAuthenticatedSelector)
if (!isAuthenticated) {
return <Navigate to="/login" />
}
return <Outlet />
}
RTK Query
リフレッシュトークンは、Auth.currentSession()
でよしなにやってくれるので、明示的な指定は必要ない。
(参考:はじめてのCognito: aws-amplify のAuthでリフレッシュトークンしてる処理を追ってみた)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Auth } from 'aws-amplify'
export const authencicatedApi = createApi({
reducerPath: 'authencicatedApi',
baseQuery: fetchBaseQuery({
baseUrl: import.meta.env.VITE_APP_API_DOMAIN,
prepareHeaders: async (headers, _) => {
const session = await Auth.currentSession()
const token = session?.getIdToken().getJwtToken()
headers.set('authorization', token)
return headers
},
mode: 'cors', // Fetch API では mode cors を設定する必要がある
}),
endpoints: (builder) => ({
getScenario: builder.query<{ message: string }, void>({
query: () => `api/scenario`,
}),
}),
})
export const { useGetScenarioQuery } = authencicatedApi
参考
Amplify を使わず React で AWS Cognito 認証を使う
AWS cognito と React でログインを実装する
AWS CDKv2 で Cognito 認証された API Gateway を構築して swaggerui で疎通確認したメモ
AWS Cli で Cognito に CONFIRMED ユーザーを作成し、emialを設定する
調査
Amplify UI を使って React アプリに Amazon Cognito の認証フォームを実装する
はじめてのcognito
Vite のプロジェクトに Amplify の設定を適用する方法
Amplify で既存 Cognito と 既存 APIGateway を使ってみる
Amplify プロジェクトに既存の Cognito ユーザープールと ID プールを使用する
Amplify UI
ViteをVue+Amplifyプロジェクトに導入する時に困ったこと&解決策
REST API リソースの CORS を有効にする
cdk
API Gateway コンソールを使用してリソースで CORS を有効にする
なんとなく CORS がわかる...はもう終わりにする。
v2 cdk
allow credentias