GoでCognitoのトークンを検証する
ユースケース
認証が完了して受け取った id_tokenやaccess_tokenをサーバーに送る
サーバー側でtokenからsub(Cognito User Pool上のUnique User ID)を取り出す
Cognitoの公式SDKにトークンの検証APIが生えていないため、自前で検証する必要がある AWS公式で検証フローを公開しているため、そのとおりに作れば良い
サンプル実装
非公式
ebiken.icon
サンプル実装とか検索しても全然なかったし、あんまり使われていないのでは?
フロー
code:_
1. parse jwt token
2. validate token's signing method
3. get kid from jwt header
4. get public keys from aws (cached on memory)
5. jwk matched with kid
6. create public key from jwk
7. verify token with public key
8. validate claim in token
- aud - should match the app clientID created in Cognito User Pool
- use - should be 'id' (token should be id_token)
- exp - should not be expired
9. get sub from claim and return as uid
ebiken.iconサンプル実装をベースに作ってみた
使ったライブラリ
メモ
httpClientinterfaceを作ってjwksを取ってくるところをモックに置き換えれるようにしている
CognitoClientID, CognitoPoolID, AWSRegionが必要
ebiken.icon id_tokenはjwtになっており、ClaimをidToken用にidTokenClaims 検証のたびにjwksをawsから取ってくるのは無駄なので、jwksCacheにいれることでオンメモリのキャッシュにしている
怪しいとこ
ebiken.iconどれくらいjwksキャッシュして良いのかわからない
jwkから公開鍵に変換してるとこの処理を理解できてない ほぼコピペになってしまった
車輪の再発明感がすごいので良いライブラリあったら書き換えたい
code:cognito.go
package main
import (
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
type httpClient interface {
Get(url string) (resp *http.Response, err error)
}
type CognitoClient struct {
HTTPClient httpClient
CognitoClientID string
CognitoPoolID string
AWSRegion string
}
const pubJWKsPath = "/.well-known/jwks.json"
var jwksCache *jwks
func NewCognitoClient(client httpClient, cognitoClientID string, cognitoPoolID string, awsRegion string) *CognitoClient {
return &CognitoClient{
HTTPClient: client,
CognitoClientID: cognitoClientID,
CognitoPoolID: cognitoPoolID,
AWSRegion: awsRegion,
}
}
type (
// jwks response from aws public jwks
jwks struct {
Keys []jwk json:"keys"
}
// See
jwk struct {
Alg string json:"alg" // Algorithm. Cryptographic algorithm used with the key. (must be RS256)
E string json:"e" // RSA Exponent. Represented as a Base64 url encoded value.
Kid string json:"kid" // Key ID. Hint indicating which key was used to secure JWS of the token.
Kty string json:"kty" // Key Type. Cryptographic algorithm family used with the key. (must be RSA)
N string json:"n" // RSA Modules. represented as a Base64 url encoded value.
Use string json:"use" // Use. Parameter describes the intended use of the public key.
}
// idTokenclaims claims in id_token from Cognito tokens
idTokenClaims struct {
TokenUse string json:"token_use" // usage. should be 'id'
Username string json:"cognito:username" // username of cognito user pool.
Email string json:"email" // email of cognito user pool.
EmailVerified bool json:"email_verified" // whether email is verified on cognito user pool.
AuthTime int64 json:"auth_time" // authenticated time.
EventID string json:"event_id" // event_id on cognito
// StandardClaims includes sub, iss, aud, exp, iat
// sub - unique user identifier from Cognito User Pool
// aud - audience. should be the app clientID created in Cognito User Pool
// exp - expiresAt.
// iat - issuedAt.
jwt.StandardClaims
}
)
func (client *CognitoClient) VerifyToken(tokenStr string) (*VerifyResult, error) {
var idToken idTokenClaims
token, err := jwt.ParseWithClaims(tokenStr, &idToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header"alg") }
kid, ok := token.Header"kid".(string) if !ok {
return "", fmt.Errorf("failed to get kid from token header")
}
pubKey, err := client.getPubKey(kid)
if err != nil {
return nil, err
}
return pubKey, nil
})
if err != nil {
return nil, &InvalidTokenError{token: tokenStr, err: err}
}
if !token.Valid {
return nil, &InvalidTokenError{token: tokenStr, err: fmt.Errorf("invalid token")}
}
if err := client.validateIDToken(idToken); err != nil {
return nil, &InvalidTokenError{token: tokenStr, err: err}
}
return &VerifyResult{UID: idToken.Subject}, nil
}
// see Step 3: Verify the Claims
func (client *CognitoClient) getIssuer() string {
}
// see Step 2: Validate the JWT Signature
func (client *CognitoClient) getPubJWKsURL() string {
return client.getIssuer() + pubJWKsPath
}
func (client *CognitoClient) getPubKey(kid string) (*rsa.PublicKey, error) {
keys, err := client.getJWKs()
if err != nil {
return nil, err
}
for _, key := range keys.Keys {
if key.Kid != kid {
continue
}
return client.convertJWKToPubKey(key)
}
return nil, fmt.Errorf("no public key matched. kid: %s", kid)
}
func (client *CognitoClient) getJWKs() (*jwks, error) {
// TODO(ebiken): research jwks expiration period
if jwksCache != nil {
return jwksCache, nil
}
url := client.getPubJWKsURL()
res, err := client.HTTPClient.Get(url)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("failed to get public key. statusCode: %d, message: %s", res.StatusCode, string(body))
}
var response jwks
if err := json.NewDecoder(strings.NewReader(string(body))).Decode(&response); err != nil {
return nil, err
}
jwksCache = &response
return jwksCache, nil
}
func (client *CognitoClient) convertJWKToPubKey(key jwk) (*rsa.PublicKey, error) {
decodedE, err := base64.RawURLEncoding.DecodeString(key.E)
if err != nil {
return nil, err
}
if len(decodedE) < 4 {
n := make([]byte, 4)
decodedE = n
}
pubKey := &rsa.PublicKey{
N: &big.Int{},
E: int(binary.BigEndian.Uint32(decodedE:)), }
decodedN, err := base64.RawURLEncoding.DecodeString(key.N)
if err != nil {
return nil, err
}
pubKey.N.SetBytes(decodedN)
return pubKey, nil
}
// See Step 3: Verify the Claims
func (client *CognitoClient) validateIDToken(idToken idTokenClaims) error {
if idToken.ExpiresAt < time.Now().Unix() {
return fmt.Errorf("token is expired. %s", time.Unix(idToken.ExpiresAt, 0))
}
if idToken.Audience != client.CognitoClientID {
return fmt.Errorf("token aud is invalid. aud: %s", idToken.Audience)
}
issuer := client.getIssuer()
if idToken.Issuer != issuer {
return fmt.Errorf("invalid iss. iss: %s", idToken.Issuer)
}
// verify token is id_token
// if token is access_token, tokenUse must be "access"
if idToken.TokenUse != "id" {
return fmt.Errorf("invalid token_use. token_use: %s", idToken.TokenUse)
}
return nil
}