2023年3月7日火曜日

QuasarでOpenIDConnect認証連携する方法

QuasarでOpenIDConnect認証連携する方法

概要

Quasar で OpenIDConnect 認証する方法を紹介します
バックエンドに Flask アプリケーションを使って連携します
今回連携する OpenIDConnect 先は Onelogin になります

環境

  • macOS 11.7.4
  • nodejs 19.6.0
    • yarn 1.22.19
    • quasar 2.0.0
  • Python 3.10.9
    • Flask 2.2.3
    • Flask-CORS 3.0.10
    • authlib 1.2.0

Quasarプロジェクトの作成

こちらを参考に作成しましょう
error @achrinza/node-ipc@9.2.5: The engine "node" is incompatible with this module. が発生する場合は

  • yarn config set ignore-engines true

を設定しましょう

Quasar 側のログインテストページの作成

  • vim src/pages/LoginPage.vue
<template>
  <div>
    <h1>Login Test Page</h1>
    <button @click="login">Login</button>
    <button @click="logout">Logout</button>
    <div>{{ data }}</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const data = ref('nodata')

const api = axios.create({ 
  baseURL: 'http://localhost:5000',
  withCredentials: true
})

const fetchUser = async(value) => {
  api.get('/user')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

const login = async(value) => {
    window.location.href = 'http://localhost:5000/login';
}

const logout = async(value) => {
  api.get('/logout')
    .then((response) => {
      data.value = response.data
    })
    .catch((error) => {
      console.log(error)
    })
}

onMounted(() => {
  fetchUser()
})
</script>

またルーティングを追加しましょう

  • vim src/router/routes.js
const routes = [
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') }
    ]
  },
  {
    path: '/login',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/LoginPage.vue') }
    ]
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue')
  }
]

export default routes

起動は

  • quasar dev

で行います
localhost:8080/#/login にアクセスすると OpenIDConnect のログインテスト用のページが表示されます

バックエンドの Flask アプリをまだ作成していないので動作はしません

Flask でバックエンドアプリの作成

flask-cors, authlib を使うのでインストールしましょう

  • pipenv install flask-cors authlib

今回 OpenIDConnect との連携はすべて Flask 側で行います
認証が成功して Flask 側で ID トークンを受け取ったらそれを session (cookies) に保存して SPA 側と共有します

from flask import (Flask,
                   url_for,
                   session,
                   redirect)
from flask_cors import CORS
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = "xxx"
app.config["ONELOGIN_CLIENT_ID"] = "xxxx"
app.config["ONELOGIN_CLIENT_SECRET"] = "xxxx"
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
CORS(app,
     supports_credentials=True,
     max_age=300)

oauth = OAuth(app)
oauth.register(
  name="onelogin",
  server_metadata_url="https://xxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration",
  client_kwargs={"scope": "openid email profile"}
)


@app.route("/login")
def login():
    redirect_uri = url_for("auth", _external=True)
    return oauth.onelogin.authorize_redirect(redirect_uri)


@app.route("/user")
def user():
    user = session.get("user")
    if user is None:
        return "Not logged in yet."
    return f"Login successful. => {user}"


@app.route("/callback")
def auth():
    token = oauth.onelogin.authorize_access_token()
    session["user"] = token["userinfo"]
    return redirect("http://localhost:8080/#/login")


@app.route("/logout")
def logout():
    session.pop("user", None)
    return "Logout successful."

起動しましょう

  • pipenv run flask run

これでバックエンドは localhost:5000 で起動します
当然ですが Quasar は localhost:8080 で起動しバックエンドは localhost:5000 で起動しているので CORS の設定が必要です

ポイント

ログインはSPA側からバックエンドにリダイレクトする

OpenIDConnet のログインページへのリダイレクトは Flask 側で行います
なので SPA 側からログインページを表示したい場合はまずバックエンドの /login URI へリダイレクトする必要があります

SPA からリダイレクトをせずに Flask から直接ログインページへリダイレクトしようとすると CORS エラーになるので注意が必要です

SPA側とバックエンド側の認証情報の共有はセッション(cookies)を使う

クッキー情報はドメインごとに管理されます
しかし今回はドメインが異なるので異なるドメイン間でクッキーを共有する必要があります

Quasar 側では withCredentials: true を設定することで axios リクエスト時にバックエンドにクッキー情報を送信できます

Flask 側では CORS(app, supports_credentials=True, max_age=300) という感じで supports_credentials=True にすることで送信された異なるドメインから送信されたクッキーを受け入れることができます

セキュリティ的な面を考慮する

今回はクッキーの Secure 属性と HTTP-Only 属性のみ有効にしています
あとは CORS 側の設定で max_age を設定して共有する有効期限を定めています

そもそもクッキーを使わない方法もあるようなのでプロダクションではセキュリティ麺を考慮した設定が実装の追加が必要になるかなと思います

最後に

Quasar に OpenIDConnect を使った認証機能を追加してみました
基本はバックエンド側のアプリが必要になると思います (調べてみると SPA オンリーでもできるっぽいですが)

SPA はモダンなのですが考えることが多いのが厄介です
そういうのも含めると普通に Web アプリケーションとして作成するのがいいのかもしれません

0 件のコメント:

コメントを投稿