├── README.md ├── .env.sample ├── .gitignore ├── Gopkg.toml ├── getToken.go ├── Gopkg.lock └── verifyToken.go /README.md: -------------------------------------------------------------------------------- 1 | # go-cognito 2 | aws cognito, JWT, golang application 3 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # common 2 | COGNITO_USER_POOL_ID= 3 | 4 | # getToken 5 | COGNITO_USER_NAME= 6 | COGNITO_PASSWORD= 7 | COGNITO_CLIENT_ID= 8 | COGNITO_NEW_PASSWORD= 9 | 10 | # verifyToken 11 | COGNITO_REGION= 12 | COGNITO_TOKEN_STRING= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/9385cd828876076f3c11ad1daa9ef4e5df1a5689/Go.gitignore 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | 18 | vendor/ 19 | node_modules/ 20 | .env 21 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | 22 | -------------------------------------------------------------------------------- /getToken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/session" 7 | "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" 8 | "github.com/joho/godotenv" 9 | "os" 10 | "log" 11 | ) 12 | 13 | func main() { 14 | loadEnv() 15 | username := os.Getenv("COGNITO_USER_NAME") 16 | password := os.Getenv("COGNITO_PASSWORD") 17 | clientId := os.Getenv("COGNITO_CLIENT_ID") 18 | userPoolId := os.Getenv("COGNITO_USER_POOL_ID") 19 | // newPassword := os.Getenv("COGNITO_NEW_PASSWORD") 20 | 21 | svc := cognitoidentityprovider.New(session.New(), &aws.Config{Region: aws.String("us-west-2")}) 22 | 23 | // ログイン 24 | params := &cognitoidentityprovider.AdminInitiateAuthInput{ 25 | AuthFlow: aws.String("ADMIN_NO_SRP_AUTH"), 26 | AuthParameters: map[string]*string{ 27 | "USERNAME": aws.String(username), 28 | "PASSWORD": aws.String(password), 29 | }, 30 | ClientId: aws.String(clientId), 31 | UserPoolId: aws.String(userPoolId), 32 | } 33 | 34 | resp, err := svc.AdminInitiateAuth(params) 35 | if err != nil { 36 | fmt.Println(err.Error()) 37 | return 38 | } 39 | fmt.Println(resp) 40 | 41 | // 初期パスワード変更 42 | // session := resp.Session 43 | // r_params := &cognitoidentityprovider.AdminRespondToAuthChallengeInput{ 44 | // ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"), 45 | // ChallengeResponses: map[string]*string{ 46 | // "NEW_PASSWORD": aws.String(newPassword), 47 | // "USERNAME": aws.String(username), 48 | // }, 49 | // ClientId: aws.String(clientId), 50 | // Session: session, 51 | // UserPoolId: aws.String(userPoolId), 52 | // } 53 | 54 | // r_resp, err := svc.AdminRespondToAuthChallenge(r_params) 55 | // if err != nil { 56 | // fmt.Println(err.Error()) 57 | // return 58 | // } 59 | // fmt.Println(r_resp) 60 | 61 | // // ログアウト by AccessToken 62 | // o_params := &cognitoidentityprovider.GlobalSignOutInput{ 63 | // AccessToken: aws.String(*resp.AuthenticationResult.AccessToken), 64 | // } 65 | // o_resp, err := svc.GlobalSignOut(o_params) 66 | // if err != nil { 67 | // fmt.Println(err.Error()) 68 | // return 69 | // } 70 | // fmt.Println(o_resp) 71 | } 72 | 73 | func loadEnv() { 74 | err := godotenv.Load(".env") 75 | if err != nil { 76 | log.Fatalf("Error loading %v\n", err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/aws/aws-sdk-go" 6 | packages = [ 7 | "aws", 8 | "aws/awserr", 9 | "aws/awsutil", 10 | "aws/client", 11 | "aws/client/metadata", 12 | "aws/corehandlers", 13 | "aws/credentials", 14 | "aws/credentials/ec2rolecreds", 15 | "aws/credentials/endpointcreds", 16 | "aws/credentials/stscreds", 17 | "aws/defaults", 18 | "aws/ec2metadata", 19 | "aws/endpoints", 20 | "aws/request", 21 | "aws/session", 22 | "aws/signer/v4", 23 | "internal/shareddefaults", 24 | "private/protocol", 25 | "private/protocol/json/jsonutil", 26 | "private/protocol/jsonrpc", 27 | "private/protocol/query", 28 | "private/protocol/query/queryutil", 29 | "private/protocol/rest", 30 | "private/protocol/xml/xmlutil", 31 | "service/cognitoidentityprovider", 32 | "service/sts" 33 | ] 34 | revision = "1b176c5c6b57adb03bb982c21930e708ebca5a77" 35 | version = "v1.12.70" 36 | 37 | [[projects]] 38 | name = "github.com/dgrijalva/jwt-go" 39 | packages = ["."] 40 | revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29" 41 | version = "v3.1.0" 42 | 43 | [[projects]] 44 | name = "github.com/go-ini/ini" 45 | packages = ["."] 46 | revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a" 47 | version = "v1.32.0" 48 | 49 | [[projects]] 50 | branch = "master" 51 | name = "github.com/gopherjs/gopherjs" 52 | packages = ["js"] 53 | revision = "178c176a91fe05e3e6c58fa5c989bad19e6cdcb3" 54 | 55 | [[projects]] 56 | name = "github.com/jmespath/go-jmespath" 57 | packages = ["."] 58 | revision = "0b12d6b5" 59 | 60 | [[projects]] 61 | name = "github.com/joho/godotenv" 62 | packages = ["."] 63 | revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b" 64 | version = "v1.2.0" 65 | 66 | [[projects]] 67 | name = "github.com/jtolds/gls" 68 | packages = ["."] 69 | revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" 70 | version = "v4.2.1" 71 | 72 | [[projects]] 73 | name = "github.com/pkg/errors" 74 | packages = ["."] 75 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 76 | version = "v0.8.0" 77 | 78 | [[projects]] 79 | name = "github.com/smartystreets/assertions" 80 | packages = [ 81 | ".", 82 | "internal/go-render/render", 83 | "internal/oglematchers" 84 | ] 85 | revision = "0b37b35ec7434b77e77a4bb29b79677cced992ea" 86 | version = "1.8.1" 87 | 88 | [[projects]] 89 | name = "github.com/smartystreets/goconvey" 90 | packages = [ 91 | "convey", 92 | "convey/gotest", 93 | "convey/reporting" 94 | ] 95 | revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" 96 | version = "1.6.3" 97 | 98 | [solve-meta] 99 | analyzer-name = "dep" 100 | analyzer-version = 1 101 | inputs-digest = "83f90627f7b5d26113d8b4236f96b9677de8636e90805dfc7d1b43eb2f410dc0" 102 | solver-name = "gps-cdcl" 103 | solver-version = 1 104 | -------------------------------------------------------------------------------- /verifyToken.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | jwt "github.com/dgrijalva/jwt-go" 9 | "crypto/rsa" 10 | "encoding/base64" 11 | "strings" 12 | "errors" 13 | "math/big" 14 | "encoding/binary" 15 | "github.com/joho/godotenv" 16 | "os" 17 | "log" 18 | ) 19 | 20 | func main() { 21 | loadEnv() 22 | region := os.Getenv("COGNITO_REGION") 23 | userPoolID := os.Getenv("COGNITO_USER_POOL_ID") 24 | tokenString := os.Getenv("COGNITO_TOKEN_STRING") 25 | 26 | // 1. Download and store the JSON Web Key (JWK) for your user pool. 27 | jwkURL := fmt.Sprintf("https://cognito-idp.%v.amazonaws.com/%v/.well-known/jwks.json", region, userPoolID) 28 | fmt.Println(jwkURL) 29 | jwk := getJWK(jwkURL) 30 | 31 | fmt.Println(jwk) 32 | 33 | token, err := validateToken(tokenString, region, userPoolID, jwk) 34 | if err != nil || !token.Valid { 35 | // jwtの検証に失敗 36 | fmt.Printf("token is not valid\n%v", err) 37 | } else { 38 | fmt.Println("success") 39 | } 40 | } 41 | 42 | func validateToken(tokenStr, region, userPoolID string, jwk map[string]JWKKey) (*jwt.Token, error) { 43 | 44 | // 2. Decode the token string into JWT format. 45 | token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { 46 | 47 | // cognito user pool : RS256 48 | if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { 49 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 50 | } 51 | 52 | // 5. Get the kid from the JWT token header and retrieve the corresponding JSON Web Key that was stored 53 | if kid, ok := token.Header["kid"]; ok { 54 | if kidStr, ok := kid.(string); ok { 55 | key := jwk[kidStr] 56 | // 6. Verify the signature of the decoded JWT token. 57 | rsaPublicKey := convertKey(key.E, key.N) 58 | return rsaPublicKey, nil 59 | } 60 | } 61 | 62 | // rsa public key取得できず 63 | return "", nil 64 | }) 65 | 66 | if err != nil { 67 | return token, err 68 | } 69 | 70 | claims := token.Claims.(jwt.MapClaims) 71 | 72 | iss, ok := claims["iss"] 73 | if !ok { 74 | return token, fmt.Errorf("token does not contain issuer") 75 | } 76 | issStr := iss.(string) 77 | if strings.Contains(issStr, "cognito-idp") { 78 | // 3. 4. 7.のチェックをまとめて 79 | err = validateAWSJwtClaims(claims, region, userPoolID) 80 | if err != nil { 81 | return token, err 82 | } 83 | } 84 | 85 | if token.Valid { 86 | return token, nil 87 | } 88 | return token, err 89 | } 90 | 91 | // validateAWSJwtClaims validates AWS Cognito User Pool JWT 92 | func validateAWSJwtClaims(claims jwt.MapClaims, region, userPoolID string) error { 93 | var err error 94 | // 3. Check the iss claim. It should match your user pool. 95 | issShoudBe := fmt.Sprintf("https://cognito-idp.%v.amazonaws.com/%v", region, userPoolID) 96 | err = validateClaimItem("iss", []string{issShoudBe}, claims) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // 4. Check the token_use claim. 102 | validateTokenUse := func() error { 103 | if tokenUse, ok := claims["token_use"]; ok { 104 | if tokenUseStr, ok := tokenUse.(string); ok { 105 | if tokenUseStr == "id" || tokenUseStr == "access" { 106 | return nil 107 | } 108 | } 109 | } 110 | return errors.New("token_use should be id or access") 111 | } 112 | 113 | err = validateTokenUse() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | // 7. Check the exp claim and make sure the token is not expired. 119 | err = validateExpired(claims) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func validateClaimItem(key string, keyShouldBe []string, claims jwt.MapClaims) error { 128 | if val, ok := claims[key]; ok { 129 | if valStr, ok := val.(string); ok { 130 | for _, shouldbe := range keyShouldBe { 131 | if valStr == shouldbe { 132 | return nil 133 | } 134 | } 135 | } 136 | } 137 | return fmt.Errorf("%v does not match any of valid values: %v", key, keyShouldBe) 138 | } 139 | 140 | func validateExpired(claims jwt.MapClaims) error { 141 | if tokenExp, ok := claims["exp"]; ok { 142 | if exp, ok := tokenExp.(float64); ok { 143 | now := time.Now().Unix() 144 | fmt.Printf("current unixtime : %v\n", now) 145 | fmt.Printf("expire unixtime : %v\n", int64(exp)) 146 | if int64(exp) > now { 147 | return nil 148 | } 149 | } 150 | return errors.New("cannot parse token exp") 151 | } 152 | return errors.New("token is expired") 153 | } 154 | 155 | // https://gist.github.com/MathieuMailhos/361f24316d2de29e8d41e808e0071b13 156 | func convertKey(rawE, rawN string) *rsa.PublicKey { 157 | decodedE, err := base64.RawURLEncoding.DecodeString(rawE) 158 | if err != nil { 159 | panic(err) 160 | } 161 | if len(decodedE) < 4 { 162 | ndata := make([]byte, 4) 163 | copy(ndata[4-len(decodedE):], decodedE) 164 | decodedE = ndata 165 | } 166 | pubKey := &rsa.PublicKey{ 167 | N: &big.Int{}, 168 | E: int(binary.BigEndian.Uint32(decodedE[:])), 169 | } 170 | decodedN, err := base64.RawURLEncoding.DecodeString(rawN) 171 | if err != nil { 172 | panic(err) 173 | } 174 | pubKey.N.SetBytes(decodedN) 175 | // fmt.Println(decodedN) 176 | // fmt.Println(decodedE) 177 | // fmt.Printf("%#v\n", *pubKey) 178 | return pubKey 179 | } 180 | 181 | // JWK is json data struct for JSON Web Key 182 | type JWK struct { 183 | Keys []JWKKey 184 | } 185 | 186 | // JWKKey is json data struct for cognito jwk key 187 | type JWKKey struct { 188 | Alg string 189 | E string 190 | Kid string 191 | Kty string 192 | N string 193 | Use string 194 | } 195 | 196 | func getJWK(jwkURL string) map[string]JWKKey { 197 | 198 | jwk := &JWK{} 199 | 200 | getJSON(jwkURL, jwk) 201 | 202 | jwkMap := make(map[string]JWKKey, 0) 203 | for _, jwk := range jwk.Keys { 204 | jwkMap[jwk.Kid] = jwk 205 | } 206 | return jwkMap 207 | } 208 | 209 | func getJSON(url string, target interface{}) error { 210 | var myClient = &http.Client{Timeout: 10 * time.Second} 211 | r, err := myClient.Get(url) 212 | if err != nil { 213 | return err 214 | } 215 | defer r.Body.Close() 216 | 217 | return json.NewDecoder(r.Body).Decode(target) 218 | } 219 | 220 | func loadEnv() { 221 | err := godotenv.Load(".env") 222 | if err != nil { 223 | log.Fatalf("Error loading %v\n", err) 224 | } 225 | } 226 | --------------------------------------------------------------------------------