広告

Azure Static Web AppsでサーバーレスSPAアプリを作ってみた

2021年9月9日

おさらいと予備知識

手順

  1. プロジェクト・Azureリソースの作成とインストール作業 
  2. 認証機能の実装 ←このページでやること
  3. ストレージの作成とAPIの実装
  4. フロントエンドアプリの実装と動作確認

Azure Static Web Appsにおける認証について

Azure Static Web Appsには、認証用のエンドポイントが用意されており、それらを利用するだけでサインイン機能が実現できます。

また、認証用のエンドポイントとは別に、ユーザーの情報(サービスプリンシパル)を取得するためのエンドポイントが用意されており、GETメソッドで/.auth/meのエンドポイントから、ユーザー情報を内包したクライアント プリンシパル データ オブジェクトが取得できます。オブジェクトには下記の情報が含まれています。

  • ユーザーID
  • ユーザー名
  • ユーザーロール
  • 認証プロバイダー名

下記のコードでクライアントサービスプリンシパルデータオブジェクトを取得することができます。

async function getUserInfo() {
  const response = await fetch("/.auth/me");
  const payload = await response.json();
  const { clientPrincipal } = payload;
  return clientPrincipal;
}

詳細は公式のドキュメントを参照しください。

ルーティングの設定(staticwebapp.config.jsonの編集)

ルーティングの設定はAzure Static Web Appsのリソースを作成した際にルートフォルダ下に作成されるstaticwebapp.config.jsonに記述します。

{
  "routes": [
    {
      "route": "/",
      "rewrite": "/index.html"
    },
    
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/login/aad",
      "rewrite": "/.auth/login/aad"
    },
    {
      "route": "/login/twitter",
      "statusCode": 404
    },
    {
      "route": "/login/facebook",
      "statusCode": 404
    },
    {
      "route": "/login/google",
      "rewrite": "/.auth/login/google"
    },
    {
      "route": "/login/github",
      "rewrite": "/.auth/login/github"
    },
    {
      "route": "/logout",
      "redirect": "/.auth/logout"
    }
  ]
}

APIはサインインしているユーザーのみに制限するため、apiルートに対して

"allowedRoles": ["authenticated"]

とすることで、サインインした場合のみAPIにアクセスできるように制限します。また、FacebookとTwitterの認証は無効化したいので、404のステータスコードを返すように設定します。
staticwebapp.config.jsonの詳細については公式のドキュメントを参照してください。

認証機能の動作確認

ここからはフロントエンドアプリで認証機能を確認するための実装を行います。今回はMaterial-UIを使うので、パッケージをインストールしておきます。

npm i @material-ui/core @material-ui/icons @material-ui/data-grid

App.tsxの編集に加え、新規にAppBar.tsxコンポーネントを作成します。

import { Box, Button, Container } from "@material-ui/core";
import axios from "axios";
import { useState, useEffect } from "react";
import MenuAppBar from "./AppBar";
type ClientPrincipal = {
  userId: string;
  userRoles: string[];
  userDetails: string;
} | null;
async function getUserInfo() {
  const response = await fetch("/.auth/me");
  const payload = await response.json();
  const { clientPrincipal } = payload;
  return clientPrincipal;
}
function App() {
  const [clientPrincipal, setClientPrincipal] =
    useState<ClientPrincipal>(null);
  const [data, setData] = useState("");
  useEffect(() => {
    (async function () {
      setClientPrincipal(await getUserInfo());
    })();
  }, []);
  useEffect(() => {
    (async function () {
      const res = await axios.get(`/api/storage`);
      setData(res.data);
    })();
  }, [clientPrincipal]);
  return (
    <Box>
      <MenuAppBar userName={clientPrincipal?.userDetails} />
      <Container maxWidth="md">
        <Box my={5}>
          {clientPrincipal ? (
            <Box>
              <h2>{data}</h2>
              <p>ユーザーID:{clientPrincipal.userId}</p>
              <p>ユーザー名:{clientPrincipal.userDetails}</p>
              <p>ユーザーロール:{clientPrincipal.userRoles}</p>
            </Box>
          ) : (
            <Container maxWidth="sm">
              <Box display="flex" justifyContent="space-between">
                <Button variant="contained" href="/login/aad">
                  Azure AD
                </Button>
                <Button variant="contained" href="/login/google">
                  Google
                </Button>
                <Button variant="contained" href="/login/twitter">
                  Twitter
                </Button>
                <Button variant="contained" href="/login/facebook">
                  Facebook
                </Button>
                <Button variant="contained" href="/login/github">
                  GitHub
                </Button>
              </Box>
            </Container>
          )}
        </Box>
      </Container>
    </Box>
  );
}
export default App;
import { makeStyles } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { FC } from "react";
const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
    color: "white",
  },
  title: {
    flexGrow: 1,
  },
}));
type Props = {
  userName?: string;
};
const MenuAppBar: FC<Props> = ({ userName }) => {
  const classes = useStyles();
  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <Typography variant="h6" className={classes.title}>
            Azure Static Web Apps CRUD
          </Typography>
          {userName && (
            <a className={classes.menuButton} href="/logout">
              サインアウト
            </a>
          )}
        </Toolbar>
      </AppBar>
    </div>
  );
};
export default MenuAppBar;

実装が終わったら実行します。

swa start http://localhost:3000 --run "npm start" --app-location api

http://0.0.0.0:4280にアクセスして下記の画面になれば成功です。

試しに「AZURE AD」を選択してみると、ロールやIDの設定画面に遷移するので、User IDUsernameをそれぞれ任意の値を設定します。

CLIエミュレータ

サインインに成功すると、ユーザー情報とAPIからの応答データが表示されます。
ユーザーロールは既定値としてanonymousに加え、サインインしているユーザーに対してはauthenticatedロールが割り当てられます。

また、先程無効化したTwitter, Facebookを選択すると、404ページが表示されることが確認できました。

次のページでは、ファイルを保存するためのストレージの作成とAPIの実装に取り掛かっていきます。

次のページへ >