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担当

ログイン状態保持

15. 最終構成

Angular
= UIのみ

Django(BFF)
= 認証/認可

Cognito
= MFA

Browser
= HttpOnly Cookieのみ保持

この構成が、
社外公開・機密データ・管理者操作ありシステムでは
最も安全性が高い。