Angular + Django(BFF) MFA認証 実装手順書
1. 目的
本手順書では、
Angular
+
Django(BFF)
+
AWS Cognito
+
MFA(TOTP)
を使用した安全な認証機能を実装する。
本システムでは、
AngularはJWTを保持しない
構成を採用する。
JWTはDjango(BFF)のみが扱う。
2. システム構成
@startuml
actor User
rectangle "Browser" {
component "Angular" as Angular
}
rectangle "Django BFF" {
component "Session認証"
}
rectangle "Cognito" {
component "MFA"
}
User --> Angular
Angular --> "Django BFF" : /auth/login
"Django BFF" --> Cognito
Cognito --> "Django BFF" : callback
"Django BFF" --> Angular : Session Cookie
Angular --> "Django BFF" : Cookie付きAPI
@enduml
```plantuml
@startuml
title BFF方式 ログイン全体シーケンス
actor User as user
participant "Browser\nAngular" as angular
participant "Django BFF\n/auth/login" as login
participant "Django Session" as session
participant "Cognito Hosted UI\nMFA" as cognito
participant "Django BFF\n/auth/callback" as callback
participant "Cognito Token Endpoint\n/oauth2/token" as token
database "RDS MySQL\nUser / Role" as db
user -> angular : /analysis/123 へアクセス
angular -> login : GET /api/me
login --> angular : 401 Unauthorized
angular -> login : GET /auth/login?next=/analysis/123
login -> login : state生成
login -> login : PKCE生成
login -> session : state / next / code_verifier 保存
login --> angular : 302 Redirect to Cognito
angular -> cognito : Hosted UI表示
user -> cognito : email / password 入力
user -> cognito : TOTP MFA入力
cognito --> callback : GET /auth/callback?code=xxx&state=yyy
callback -> session : state / code_verifier / next 取得
callback -> callback : state検証
callback -> token : POST /oauth2/token\ncode + code_verifier
token --> callback : id_token / access_token / refresh_token
callback -> callback : id_token検証\nissuer / audience / exp / token_use
callback -> db : Cognito sub / email でユーザー確認
db --> callback : AppUser / 権限情報
callback -> session : Django login()\nsessionid発行
callback --> angular : 302 Redirect to /analysis/123\nSet-Cookie: sessionid; HttpOnly; Secure; SameSite=Lax
angular -> login : GET /api/me\nCookie付き
login --> angular : 200 OK\nログインユーザー情報
angular -> angular : /analysis/123 表示
@enduml
```
3. なぜBFF方式なのか
NG方式
AngularがJWTを保持
問題:
XSS時にJWT漏洩リスク
OK方式
DjangoだけがJWTを保持
Angularは:
Session Cookieのみ
を使用する。
4. Angular実装
4.1 Angularの役割
Angularの役割:
画面表示
ログイン状態確認
未ログイン時のリダイレクト
Angularは:
JWTを扱わない
4.2 認証Service作成
作成ファイル
src/app/core/auth/auth.service.ts
auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface MeResponse {
authenticated: boolean;
username: string;
email: string;
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
constructor(
private http: HttpClient
) {}
me(): Observable<MeResponse> {
return this.http.get<MeResponse>(
'/api/me',
{
withCredentials: true,
}
);
}
login(returnUrl: string): void {
window.location.href =
`/auth/login?next=${encodeURIComponent(returnUrl)}`;
}
logout(): Observable<{ logout_url: string }> {
return this.http.post<{ logout_url: string }>(
'/auth/logout',
{},
{
withCredentials: true,
}
);
}
}
4.3 withCredentials の意味
重要:
withCredentials: true
意味:
Cookieを自動送信する
これがないと:
sessionid Cookie
が送信されない。
4.4 Route Guard作成
作成ファイル
src/app/core/auth/auth.guard.ts
auth.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router,
} from '@angular/router';
import {
catchError,
map,
of,
} from 'rxjs';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.me().pipe(
map(() => true),
catchError(() => {
const returnUrl = router.url;
authService.login(returnUrl);
return of(false);
})
);
};
4.5 Guardの役割
ログイン済み:
画面表示
未ログイン:
Cognitoログインへ移動
4.6 Router設定
app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './core/auth/auth.guard';
export const routes: Routes = [
{
path: 'analysis/:id',
canActivate: [authGuard],
loadComponent: () =>
import('./pages/analysis/analysis.component')
.then(m => m.AnalysisComponent),
},
];
4.7 ログアウトボタン
logout-button.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../auth/auth.service';
@Component({
selector: 'app-logout-button',
template: `
<button
type="button"
(click)="logout()"
>
ログアウト
</button>
`,
})
export class LogoutButtonComponent {
constructor(
private authService: AuthService
) {}
logout(): void {
this.authService
.logout()
.subscribe((res) => {
window.location.href =
res.logout_url;
});
}
}
4.8 Angular側でやってはいけないこと
禁止:
JWT保存
localStorage保存
sessionStorage保存
Token decode
Token検証
全部Django側で行う。
5. Django(BFF)実装
5.1 Djangoの役割
Django(BFF)の役割:
Cognitoログイン
Token交換
JWT検証
Session作成
Cookie発行
認可
5.2 必要ライブラリ
pip install \
requests \
PyJWT \
cryptography \
boto3
5.3 settings.py
本番設定
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "Lax"
SECURE_SSL_REDIRECT = True
5.4 各設定の意味
| 設定 | 意味 |
|---|---|
| HttpOnly | JSからCookie参照禁止 |
| Secure | HTTPSのみ |
| SameSite | CSRF軽減 |
| SSL_REDIRECT | HTTP禁止 |
5.5 ログイン開始View
auth_views.py
import secrets
from django.http import HttpResponseRedirect
from core.cognito import (
create_pkce_pair,
build_authorize_url,
)
def auth_login(request):
next_url = request.GET.get(
"next",
"/"
)
state = secrets.token_urlsafe(32)
code_verifier, code_challenge = (
create_pkce_pair()
)
request.session["oauth_state"] = state
request.session["oauth_next"] = next_url
request.session["pkce_code_verifier"] = (
code_verifier
)
authorize_url = build_authorize_url(
state=state,
code_challenge=code_challenge,
)
return HttpResponseRedirect(
authorize_url
)
5.6 この処理でやっていること
1. state作成
2. PKCE生成
3. session保存
4. Cognitoへredirect
5.7 callback View
def auth_callback(request):
code = request.GET.get("code")
state = request.GET.get("state")
expected_state = request.session.get(
"oauth_state"
)
if state != expected_state:
return JsonResponse(
{"detail": "Invalid state"},
status=400,
)
# token交換
# JWT検証
# login()
return redirect("/")
5.8 callback の役割
Cognitoログイン完了処理
画面ではない。
5.9 Django login()
from django.contrib.auth import login
実行すると:
sessionid Cookie
が自動発行される。
5.10 /api/me
@login_required
def me(request):
return JsonResponse({
"authenticated": True,
"username": request.user.username,
})
役割:
ログイン確認API
5.11 ログアウト
from django.contrib.auth import logout
def auth_logout(request):
logout(request)
return JsonResponse({
"logout_url": build_logout_url()
})
6. CSRF対策
Cookie認証では:
CSRF対策必須
6.1 csrf endpoint
from django.views.decorators.csrf import ensure_csrf_cookie
@ensure_csrf_cookie
def csrf(request):
return JsonResponse({
"detail": "ok"
})
6.2 Angular側
POST前に:
/api/csrf
を呼ぶ。
7. 直リンク対応
例:
/analysis/123
へ直接アクセス。
未ログイン時:
/auth/login?next=/analysis/123
へ移動。
ログイン後:
/analysis/123
へ戻る。
8. callback URLについて
Callback URLは:
固定
にする。
例:
https://motion.example.com/auth/callback
9. state の役割
state は:
元画面URL保持
+
CSRF対策
10. CloudWatchログ
記録する内容:
ログイン成功
ログイン失敗
MFA失敗
JWT検証失敗
権限拒否
管理者操作
11. 管理者API
admin_required
if not request.user.is_staff:
return 403
12. セキュリティチェック
| 項目 | 必須 |
|---|---|
| MFA | ○ |
| HTTPS | ○ |
| HttpOnly Cookie | ○ |
| Secure Cookie | ○ |
| CSRF対策 | ○ |
| JWT検証 | ○ |
| SSH禁止 | ○ |
| Secrets Manager | ○ |
13. やってはいけないこと
禁止:
AngularでJWT保持
localStorage保存
Implicit Flow
Client SecretをGit管理
Public RDS
SSH開放
14. 新入社員向け理解ポイント
Angular
画面担当
Django
認証担当
Cognito
MFA担当
Cookie
ログイン状態保持
15. 最終構成
Angular
= UIのみ
Django(BFF)
= 認証/認可
Cognito
= MFA
Browser
= HttpOnly Cookieのみ保持
この構成が、
社外公開・機密データ・管理者操作ありシステムでは
最も安全性が高い。