概要
- AWSのAPI Gatewayの認証にCognito UserPoolsのAuthorizerを用いるときに、API側でもユーザーの一意性を保つために認証します
- その際Cognitoから取得できるJWTのデコードが必要になるのでGo言語でのデコード方法についてまとめました
- クライアント側でAmplifyやリダイレクト後のURLなどを用いてCognitoからIDトークンを取得する方法については既知とします
Amazon Cognito ユーザープール認証
- Cognitoを用いたログインをクライアントから行うと、ユーザープールからJWT(JSON Web Token)が返されます
- JWTはBase64でエンコードされたJSON文字列であり、ユーザーに関する情報(クレーム)が含まれています
- (このBase64でエンコードされていると言う情報によりかなり苦しむことになりました...)
- エンコードされた文字列は
***.***.***
のようにドット(.)
で3つのセクションに分けられています - 3つのセクションはヘッダー、ペイロード、署名で構成されています
ヘッダーは以下のような情報があります
(キーID("kid")と、トークンの署名に使用されるアルゴリズム("alg")が含まれています)
{
"kid": "abcdefghijklmnopqrsexample=",
"alg": "RS256"
}
ペイロードには以下のような情報があります
(ユーザーに関する情報と、トークンの作成および有効期限のタイムスタンプが含まれています)
{
"sub": "aaaaaaaa-bbbb-cccc-dddd-example",
"aud": "xxxxxxxxxxxxexample",
"email_verified": true,
"token_use": "id",
"auth_time": 1500009400,
"iss": "https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-2_example",
"cognito:username": "example",
"exp": 1500013000,
"given_name": "Example",
"iat": 1500009400,
"email": "example@example.com"
}
署名には、ヘッダーとペイロードの組み合わせをハッシュして暗号化したものが入ります
ヘッダーとペイロードのデコード
署名の認証周りは一旦なしにして、JWTをデコードしてトークンに含まれるヘッダとペイロードを取得する方法を説明します
Base64でエンコードされているとのことだったのでBase64でデコードすればいいやと思い、デコードしていたのですが、たまにデコードできない現象が発生してしまい、かなり苦しみました...
最終的な結論としてはBase64URLでデコードすれば間違いがありません
Base64とBase64URLの違い
Base64エンコードはデータを印字可能な64種類のデータで表現するエンコード方式です
しかし、URLの一部として利用する場合は「+」や「/」とと言ったURLセーフではない文字列が含まれるのでURLセーフにエンコードしなければなりません
そこで用いられるのがBase64URLのようです
おそらくAmplifyなどで直接JWTを取得する場合は問題ないと思いますが、リダイレクト後のURLなどからJWTをゲットした場合にURLセーフにエンコードされているのでBase64ではデコードできなかったものと考えられます
どちらにせよBase64URLでデコードすれば、ヘッダーとペイロードの値を取得できます
コーディング例
base64urlのデコーディングにはgithub.com/dvsekhvalnov/jose2go/base64url
をimportして用います
(そのほかには"strings"
や"fmt"
や"encoding/json"
のimportが必要です)
jwt := "***.***.***" // Cognitoユーザープールから取得したJWT
// // API GatewayでLambda Proxyを用いてる場合、AuthorizationヘッダにJWTを付与するので、以下で取得可能
// jwt := events.APIGatewayProxyRequest.Headers["Authorization"]
// ドットで文字列を分割して配列にして、エンコーディングされたヘッダとペイロードを取得
headerEnc := strings.Split(jwt, ".")[0]
payloadEnc := strings.Split(jwt, ".")[1]
// base64urlを用いてヘッダーとペイロードをデコードします
headerDec, _ := base64url.Decode(headerEnc) // 第二戻り値のエラーは無視しています
payloadDec, _ := base64url.Decode(payloadEnc) // 同様
// 文字列として出力
fmt.Println(string(headerDec))
fmt.Println(string(payloadDec))
// デコードされた戻り値は[]byte型なので、以下のように構造体に入れたり、マップに入れ込んだりできます
type Header struct {
KeyID string `json:"kid"`
Algorithm string `json:"alg"`
}
headerStruct := Header{}
json.Unmarshal(headerDec, &headerStruct)
headerMap := map[string]string{}
json.Unmarshal(headerDec, &headerMap)
以上のようにデコード自体は難しくないのですが、あまり載っていなかったので苦労しました