クライアント(React)でAuth0からアクセストークンを取得し、そのアクセストークンをAuthorizationヘッダーに付与して保護されたリソースサーバー(Ktor)にリクエストし、Ktorでアクセストークンを検証する仕組みを作成します。
著者が開発中に遭遇したエラーを再現しながら進めていきます。OAuth2.0はまだまだ勉強中の身ですので、間違いなどありましたら、ご指摘いただけると幸いです。
サーバーサイドフレームワークにはKtorを使用しています。(kotlinサーバーサイド開発が個人的なトレンドです。)
Ktorの需要があまり高くないかもしれませんが、Auth0上での設定やアプリケーションコードの本質的な処理内容はフレームワークに依存せず同じです。
フレームワークに依存する細かいアプリケーションコードは他の記事を参照して頂ければと思います。
Auth0のアカウント作成
- Account Type:
Other
- I need advanced settings:
Checked
Auth0でテナント作成
- Tenant Domain:
良い感じの名前
- Region:
Japan
Auth0でSample Appを作成
Tenantを作成すると、Auth0の素晴らしいオンボーディングが始まるので、これを体験していきます。
Auh0のダッシュボードでポチポチとクリックするだけで、OAuth2.0が組み込まれたReactを起動できます。
Step 1
プラットフォームのタイプを選択します。ReactなのでSPAです。
- Select a platform for your app:
Single-Page App
- Select the technology:
React
Step 2
ログイン/サインアップページのUIをカスタマイズできます。
何も設定を変えずにContinueを押しても大丈夫です。(著者は Social Connections に GitHub を追加する変更だけ行ないました。)
Step 3
Try Loginを押すと、カスタマイズしたUIで、ログイン/サインアップページのプレビューを閲覧できます。
Step 4
DOWNLOAD SAMPLE APPボタンを押すと、Reactをダウンロードできます。
画面右に記載の設定は後ほど行います。
Auth0でApplicationとAPIを作成
ApplicationはAuth0上でクライアントアプリケーション(Reactにあたる)を管理するコンポーネント、APIはAuth0上でリソースサーバー(Ktorにあたる)を管理するコンポーネントです。
Application作成
Applications > Applicationsタブを開きます。
Application
はDefault App
が既に作成されているので、これを使用します。
API作成
Applications > APIs > Create API から作成します。
- Name:
良い感じの名前
- Identifier:
一意の識別子(URI)
- アクセスを許可するリソースサーバーをTenant内で識別するための値です
- JSON Web Token (JWT) Profile:
Auth0
- JSON Web Token (JWT) Signing Algorithm:
RS256
これでAPIの作成も完了です。
Reactの起動
ダウンロードしたReactを npm install && npm start
で起動します。
Callback URL mismatch
ログインボタンを押すと、先ほどプレビューで閲覧したログインページを表示して欲しいところですが、エラーページが表示されてしまいます。
エラーページには以下のように記載されています。
Callback URL mismatch.
The provided redirect_uri is not in the list of allowed callback URLs.
Please go to the Application Settings page and make sure you are sending a valid callback url from your application.
「Auth0で許可されているRedirect URI(Callback URL)」と「クライアント(React)が指定したRedirect URI(Callback URL)」が一致していない、とのことです。
Redirect URIはAuth0での認証結果を返却するURIを指します。
Redirect URIは、Auth0でもクライアント(React)でも何も設定していないので、当然と言えば当然です。オンボーディングの最後の画面の右に記載されていた内容が、この設定に関する内容です。
Monitoring > Logs でエラー調査
今回のエラーは、エラー原因が画面に表示されていましたが、エラーが起きた際は、Auth0のダッシュボードから原因を調査することが可能です。
先ほどと同じ主旨の内容が記載されています。
Callback URL mismatch. http://localhost:3000 is not in the list of allowed callback URLs
Auth0 Application と Reactの設定変更
クライアント(React)でAuth0からアクセストークンを取得する仕組みを作成します。
Auth0 Applicationの設定変更
Default Appの設定を変更します。
Settings > Application URIs の以下の3つの入力欄にhttp://localhost:3000
と入力します。
Allowed Callback URLs
Allowed Logout URLs
Allowed Web Origins
localhostなので、httpsではなく、httpです。
Reactの設定変更
auth_confg.json
の値を変更します。
{
"domain": "**********",
"clientId": "**********",
"audience": "**********"
}
domainとclientIdはAuth0のapplicationに記載されています。
audienceはAPIのIdentifierです。
Application authentication methods
この段階でログインボタンを押すと、ログインページが表示されます。
サインアップ画面に移動し、ユーザー登録も成功しますが、アクセストークンの取得に失敗し、ホーム画面にリダイレクトされません。
エラーのlogを見ても、Failed Exchange
とかfeacft
とかの記載だけで、良く分かりません。
この問題は修正されるかもしれませんが、最初に作成されるDefault AppのアプリケーションタイプがSPAであるにもかかわらず、Application authentication methodsがClient Secret (Post)に設定されています。
SPAの場合、ここはNoneであるべきなので、設定を変更します。
もう一度、localhost:3000にアクセスしてログインすると、アクセストークンの取得に成功し、ホーム画面にリダイレクトされます。
ユーザーの確認
User Management > Users から実際に作成されたユーザーを確認することができます。
Ktorでトークンの検証を行う
クライアント(React)でAuth0からアクセストークンを取得できました。
リソースサーバー(Ktor)は保護されているサーバーです。誰でもリソースにアクセスできるわけではありません。
アクセスを許可されている(認可されている)ことを証明するために、クライアントがリクエスト時に付与するものがアクセストークンです。
なので、リソースサーバーでは、リクエストを受けた時に、このアクセストークンが正しいものであるか検証する必要があります。
ReactからKtorにリクエストする
ReactのExtarnal API
> Ping API
ボタンを押すと、RESULT
にJsonが表示されます。しかし、これはモックなので、Ktorのレスポンスを表示します。
まず、Ktorに簡単なAPIを1つ作成しておきます。
エンドポイント:http://localhost:8080/health
レスポンス:{"msg":"OK!"}
※ モックと同じJosn構造です。
fun Application.module() {
routing {
get("health") {
call.respondText("""{"msg":"OK!"}""", ContentType.Application.Json)
}
}
}
ReactのExtarnalAPI.js
のcallApi
関数でKtorのエンドポイントを指定します。
const callApi = async () => {
.....
- const response = await fetch(`${apiOrigin}/api/external`, {
+ const response = await fetch("http://localhost:8080/health", {
headers: {
Authorization: `Bearer ${token}`,
},
});
.....
}
もう一度、Ping API
ボタンを押すと、{"msg":"OK!"}
というJsonがKtorから返却されました。
※ もし新しいJsonが画面に反映されない場合、画面をリロードしてから、Ping API
ボタンを押してみてください。
Ktorでトークン検証の仕組みを作る
KtorのApplication.kt
のApplication.module
関数内に以下のコードを記述します。
issuer
、audience
、myRealm
の${...}
は置き換えてください。
import com.auth0.jwk.JwkProviderBuilder
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
fun Application.module() {
val issuer = "https://${Auth0で作成したTenantの名称}.jp.auth0.com/"
val audience = "${Auth0で作成したAPIのIdentifier}"
val myRealm = "${このリソースサーバーを示す名称}"
val jwkProvider = JwkProviderBuilder(issuer).build()
install(Authentication) {
jwt("auth0") {
realm = myRealm
verifier(jwkProvider, issuer)
validate { credential ->
val containsAudience = credential.payload.audience.contains(audience)
if (containsAudience) JWTPrincipal(credential.payload) else null
}
}
}
}
エンドポイントを保護するには以下のようにコードを追記します。
fun Application.module() {
routing {
+ authenticate("auth0") {
get("health") {
call.respondText("""{"msg":"OK!"}""", ContentType.Application.Json)
}
+ }
}
}
ブラウザからhttp://localhost:8080/health
にリクエストすると、401 Unauthorized
が返却され、エンドポイントが保護されているのが確認できます。
しかし、同じエンドポイントにReactからリクエストすると、正常にレスポンスが返却されます。
これは、Reactからリクエストするとき、Authorizationヘッダーにアクセストークンが付与されているからです。Ktorでトークンの検証が行われ、認可されているユーザーと判断されました。
試しに、トークンに適当な文字列を追加してみると、Ktorからは401 Unauthorized
が返却されます。
const callApi = async () => {
.....
const response = await fetch("http://localhost:8080/health", {
headers: {
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${token}abc`,
},
});
.....
}
参考