前回、Next.js + express-openid-connect を使って、全てのページでOpenID Connectによる認証が必要な Relying Party を作りました。
Next.js + express-openid-connect を使って、バックエンドで OpenID Provider と通信する Relying Party を作ってみた - メモ的な思考的な
そんな中、上記の Relying Party に認証が不要なページも追加してみたところ、いくつか悩んだところがあったため、メモを残します。
なお、「この実装で良い」と言い切れない部分があるので、もしより良い実装があれば教えていただけるとありがたいです。
目次
環境
実装すること
- ベースは、前回作成した RP
- このコミット より開始
- 認証が必要なページと不要なページを用意
/
- 認証不要
/profile
- 認証必要
構成
- macOS
- Open ID Provider
- Relying Party
localhost:3784
で起動- Next.js 11.1.2
- express 4.17.1
- Next.js の Custom Server 機能として使用
- express-openid-connect 2.5.0
- redis 3.1.2
- connect-redis 6.0.0
- セッションストア: Redis
- Dockerで作成し、ポート
16380
で起動
- Dockerで作成し、ポート
失敗
express-openid-connect の 例を見ると、 requiresAuth()
を使えば 認証が必要/不要なページを制御できそうでした。
2. Require authentication for specific routes | express-openid-connect/EXAMPLES.md at master · auth0/express-openid-connect
そこで、 server.js
に対し、
app.prepare().then(() => { const server = express(); // ... server.use(auth({ // ... // 以下を追加し、指定したルートで認証の要不要を指定 authRequired: false, })); // `/` は認証不要 server.get('/', (req, res) => { return handle(req, res) }) // 上記以外は認証必要 server.all('*', requiresAuth(), (req, res) => { return handle(req, res) })
として、Next.js を起動しました (このコミット)。
すると、 /
にアクセスした時に
のようなエラーがChromeのConsoleに表示されました。
エラーは
# まとめあげ Refused to execute script from '<URL>' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled. # 詳細 Refused to execute script from 'http://localhost:3780/users/sign_in' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
であり、ワーニングは
# まとめあげ Cross-Origin Read Blocking (CORB) blocked cross-origin response <URL> with MIME type text/html. See <URL> for more details. # 詳細 Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:3780/users/sign_in with MIME type text/html. See https://www.chromestatus.com/feature/5629709824032768 for more details.
でした。
また、OPのログを見てみると、 /
を開いた瞬間に、OPの認可エンドポイントへリクエストが飛んでいました。
Started GET "/oauth/authorize? client_id=*** &scope=openid &response_type=code &redirect_uri=http%3A%2F%2Flocalhost%3A3784%2Fcallback &nonce=*** &code_challenge_method=S256&code_challenge=***" for 127.0.0.1 at 2021-09-09 21:04:39 +0900
一方、 /profile
へ直接アクセスすると、OPのログイン画面が表示されました。ログイン後は /
や /profile
にアクセスしても問題ありませんでした。
対応
今回、 express-openid-connect
はミドルウェアとして express に組み込んでいるため、ミドルウェアまわりの処理がうまくいっていないように見えました。
そこで、 requiresAuth()
を使うのではなく、 requiresAuth()
相当の処理を Next.js の getServerSideProps
へ実装することにしました。
なお、Next.js のドキュメントには
Note: You can import modules in top-level scope for use in getServerSideProps. Imports used in getServerSideProps will not be bundled for the client-side. This means you can write server-side code directly in getServerSideProps. This includes reading from the filesystem or a database.
https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
とあったため、今回は /profile
のページの getServerSideProps
に直接書くこととしました。
req.oidc.isAuthenticated の追加
express-openid-connect
のソースコードを読むと、 req.oidc.isAuthenticated
にて認証済か否かを判断していました。
- https://github.com/auth0/express-openid-connect/blob/v2.5.0/middleware/requiresAuth.js#L40
- https://github.com/auth0/express-openid-connect/blob/v2.5.0/middleware/requiresAuth.js#L4
そこで、
import Link from 'next/link'; export const getServerSideProps = async function ({ req, res }) { // requireAuth() の中身を移植 if (!req.oidc.isAuthenticated()) { return { redirect: { // 途中のエラーが表示される destination: '/login', permanent: false }, } } // 以降は同じ return await req.oidc.fetchUserInfo() .then(response => { return { props: { email: response.email } } }) } export default function Profile({ email }) { return ( <> <h1>Your Profile</h1> <p>Email: {email}</p> <Link href="/" passHref> <button>Go Home</button> </Link> </> ) }
と、認証されていない !req.oidc.isAuthenticated
時は Next.js でログインページへリダイレクトするようにしてみました (このコミット)。
- Authenticating Server-Rendered Pages | Authentication | Next.js
- html - Next.js Redirect from / to another page - Stack Overflow
すると、 /
にアクセスしてもエラーが表示されなくなりました。
続いて /profile
へ遷移するボタンをクリックしたところ、ログインページは表示されるようになったものの、一瞬エラーページが表示されました。
Chrome の Network を見てみると、HTTP 404 となっていました。
詳細を確認すると、クライアントサイドでルーティングしたことにより、 HTTP 404 が表示されているようでした。
そこで、サーバサイドでルーティングするよう、ホストとポートを指定してみました (このコミット)。
export const getServerSideProps = async function ({ req, res }) { if (!req.oidc.isAuthenticated()) { return { redirect: { // destination: '/login', // Next.js の外としてルーティング destination: `${process.env.NEXT_HOST}:${process.env.PORT}/login`, permanent: false }, } } // ...
すると、クライアントサイドではルーティングされなくなり、HTTP 404 ページも表示されなくなりました。
ただ、 /profile
へ遷移した後にログインしたのに、ログイン後は /
に戻されてしまいました。
res.oidc.login の追加
express-openid-connect
のソースコードをさらに読むと、 res.oidc.login
を使ってログイン処理を行っていました。
https://github.com/auth0/express-openid-connect/blob/v2.5.0/middleware/requiresAuth.js#L20
そこで、
export const getServerSideProps = async function ({ req, res }) { if (!req.oidc.isAuthenticated()) { // 追加 if (!res.oidc.errorOnRequiredAuth && req.accepts('html')) { await res.oidc.login() return { props: { email: '' } } } return { notFound: true } }
と、 res.oidc.login
を使うようにしました。
また、 !res.oidc.errorOnRequiredAuth && req.accepts('html')
を満たさない場合は HTTP 404 ページを表示させるため、Next.js 10 で追加された notFound
を使うようにしました。
notFound Support | Blog - Next.js 10 | Next.js
それに合わせて、カスタムの 404 ページを pages/404.js
として用意しました(ここまででこのコミット)。
Customizing The 404 Page | Advanced Features: Custom Error Page | Next.js
export default function Custom404() { return <h1>404 - Page Not Found</h1> }
準備ができたため、
/
へアクセスGo Profile
ボタンをクリック- OPのログイン画面でログイン
の順で操作したところ、ログイン後は /profile
へと遷移するようになりました。
ただ、Chrome の Network タブを見たところ
と、 CORS error: Cross-Origin Resource Sharing error: MissingAllowOriginHeader
エラーが表示されていました。
クライアントサイドでの /profile
への遷移をあきらめる
デバッグしてみたところ、 res.oidc.login()
の処理後に CORS エラーが出ていました。
とはいえ、res.oidc.login()
の削除もできません。
そこで、pages/index.js
の Link コンポーネントによるクライアントサイドでの遷移を、 <a>
タグでの遷移に切り替えました (このコミット)。
Linking between pages | Routing: Introduction | Next.js
export const getServerSideProps = async function ({ req, res }) { return { props: { host: process.env.NEXT_HOST, port: process.env.PORT, } } } export default function Home({ host, port }) { return ( <> <h1>Index page</h1> {/* クライアントでのルーティングができないので、aタグに差し替え */} <a href={`${host}:${port}/profile`}>Go</a> </> ) }
再度実行したところ、CORSエラーが出なくなりました。
これで、認証が必要/不要な各ページを持つ Relying Party が実装できました。
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/oidc_op_rp-sample
今回のPRはこちらです。
https://github.com/thinkAmi-sandbox/oidc_op_rp-sample/pull/6