

AWS IoT Core、証明書もIAMも使わずJWTだけで繋ぐカスタム認証の仕組み
この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。
モバイルアプリからAWS IoT CoreにMQTT over WebSocketで繋いで、APIログイン後に発行したJWTで認証したい、という要件があった。X.509証明書もIAM/SigV4署名も使わずに、自前のJWT検証だけで接続を許可する構成が組めるか調べた。
Custom Authorizerとは
AWS IoT Coreには「Custom Authorizer」という仕組みがあって、接続時の認証・認可ロジックをLambda関数に丸ごと委譲できる。証明書もIAMクレデンシャルも要らないので、既存のID基盤(今回で言えばJWT)をそのまま持ち込める。
流れはこう。
sequenceDiagram
participant App as モバイルアプリ
participant API as ログインAPI
participant IoT as AWS IoT Core
participant Lambda as Custom Authorizer(Lambda)
App->>API: ログイン
API-->>App: JWT発行
App->>IoT: MQTT over WebSocket接続(JWTをクエリパラメータで送付)
IoT->>Lambda: 認証リクエスト
Lambda->>Lambda: JWT検証(署名/iss/aud/exp/sub)
Lambda-->>IoT: isAuthenticated + IoTポリシー
IoT-->>App: 接続確立
JWTはブラウザ/WebView環境だとカスタムHTTPヘッダーが使いづらいので、クエリパラメータ経由でCONNECTのusername側に載せる形になる。
wss://{endpoint}/mqtt?x-amz-customauthorizer-name={authorizerName}&{tokenKeyName}={jwt}Lambda側の役割
Lambdaは受け取ったJWTを検証して、接続の可否とIoTポリシーを返す。iss / aud / exp / subあたりを見て、Publish/Subscribeできるトピックを動的に組み立てる形が一般的。
レスポンスはこういう形式。
{ "isAuthenticated": true, "principalId": "user123abc", "disconnectAfterInSeconds": 1800, "refreshAfterInSeconds": 300, "policyDocuments": [ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect"], "Resource": "*" }, { "Effect": "Allow", "Action": ["iot:Publish"], "Resource": "arn:aws:iot:example:example:topic/app/user123abc/#" }, { "Effect": "Allow", "Action": ["iot:Subscribe"], "Resource": "arn:aws:iot:example:example:topicfilter/app/user123abc/#" } ] } ]}disconnectAfterInSecondsとrefreshAfterInSecondsは300〜86400秒の範囲で指定する。JWTの有効期限に合わせてdisconnectAfterInSecondsを短めにしておくと、期限切れトークンでの接続維持を防げる。
Lambda関数のタイムアウトは5秒固定。JWKS取得なんかを都度やってると簡単に超えるので、キャッシュは必須。
実装するときにハマりそうな点
構成自体は素直だけど、細部で引っかかりそうなポイントが4つあった。
principalIdは英数字のみ
principalIdは([a-zA-Z0-9]){1,128}という正規表現に一致する必要がある。つまり英数字以外は一切ダメ。
JWTのsubをそのままuser:${sub}のような形で使うとコロンで弾かれるし、subがUUID(ハイフン入り)の場合もそのままだとNG。サニタイズしてから渡す必要がある。
const principalId = sub.replace(/[^a-zA-Z0-9]/g, "");ALPNはWebSocketなら不要
「443ポート+ALPN拡張でmqttを指定する」という要件は生MQTT(TCP)接続の場合の話で、MQTT over WebSocketではALPNは不要(プロトコル対応表だと「ALPN: N/A」)。WebSocket接続で設計するなら気にしなくていい。
ちなみに2024年10月のアップデートで、SNI拡張だけで認証方式を判定できるようになり、生MQTTでもALPNを使わない構成が可能になったらしい。
signingDisabledの設定を忘れない
AWS IoT Core組み込みの署名検証(x-amz-customauthorizer-signature)はデフォルトで有効になっている。JWT自体がすでに署名済みトークンなので、Authorizer側の署名検証は不要なケースが多いが、その場合は明示的にsigningDisabled: trueを設定しないと接続が弾かれる。
token-key-nameはAuthorizer設定と一致させる
クエリパラメータ名(例:token-key-name=の部分)は、Authorizer作成時に指定する「Token key name」と一致していないといけない。ここがズレていても静かに失敗するので、命名を揃えておく。
まとめ
- Custom Authorizerを使えば証明書もIAMも使わずJWTベースの認証ができる
principalIdは英数字のみ。UUIDやコロン付き文字列は必ずサニタイズする- WebSocket接続ならALPNは気にしなくていい
signingDisabledとToken key nameの整合は実装前に確認しておく