Skip to content

Commit 25cef28

Browse files
committed
add Amazon AWS Cognito JWK link support for token validation and verification through jwt.LoadAWSCognitoKeys package-level function
1 parent 0fc2a08 commit 25cef28

File tree

6 files changed

+255
-4
lines changed

6 files changed

+255
-4
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2020-2023 Gerasimos Maropoulos <[email protected]>
3+
Copyright (c) 2020-2024 Gerasimos Maropoulos <[email protected]>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Import as `import "github.com/kataras/jwt"` and use it as `jwt.XXX`.
3737
* [Encryption](#encryption)
3838
* [Benchmarks](_benchmarks)
3939
* [Examples](_examples)
40+
* [Amazon AWS Cognito Verification](_examples/aws-cognito-verify/main.go) **NEW**
4041
* [Basic](_examples/basic/main.go)
4142
* [Custom Header](_examples/custom-header/main.go)
4243
* [Multiple Key IDs](_examples/multiple-kids/main.go)

_examples/aws-cognito-verify/main.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/kataras/jwt"
7+
)
8+
9+
// |=================================================================================|
10+
// | Amazon's AWS Cognito integration example for token validation and verification. |
11+
// |=================================================================================|
12+
13+
func main() {
14+
/*
15+
cognitoConfig := jwt.AWSKeysConfiguration{
16+
Region: "us-west-2",
17+
UserPoolID: "us-west-2_xxx",
18+
}
19+
20+
keys, err := cognitoConfig.Load()
21+
if err != nil {
22+
panic(err)
23+
}
24+
OR:
25+
*/
26+
keys, err := jwt.LoadAWSCognitoKeys("us-west-2" /* region */, "us-west-2_xxx" /* user pool id */)
27+
if err != nil {
28+
panic(err) // handle error, e.g. pool does not exist in the region.
29+
}
30+
31+
var tokenToValidate = `xxx.xxx.xxx` // put a token here issued by your own aws cognito user pool to test it.
32+
33+
var claims jwt.Map // Your own custom claims here.
34+
if err := keys.VerifyToken([]byte(tokenToValidate), &claims); err != nil {
35+
panic(err) // handle error, e.g. token expired, or kid is empty.
36+
}
37+
38+
for k, v := range claims {
39+
fmt.Printf("%s: %v\n", k, v)
40+
}
41+
}

alg.go

+11
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,14 @@ var (
169169
EdDSA,
170170
}
171171
)
172+
173+
// parseAlg returns the algorithm by its name or nil.
174+
func parseAlg(name string) Alg {
175+
for _, alg := range allAlgs {
176+
if alg.Name() == name {
177+
return alg
178+
}
179+
}
180+
181+
return nil
182+
}

jwt.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package jwt
33
import (
44
"bytes"
55
"encoding/json"
6-
"io/ioutil"
6+
"os"
77
"reflect"
88
"time"
99
)
@@ -30,8 +30,8 @@ var CompareHeader HeaderValidator = compareHeader
3030
// ReadFile can be used to customize the way the
3131
// Must/Load Key function helpers are loading the filenames from.
3232
// Example of usage: embedded key pairs.
33-
// Defaults to the `ioutil.ReadFile` which reads the file from the physical disk.
34-
var ReadFile = ioutil.ReadFile
33+
// Defaults to the `os.ReadFile` which reads the file from the physical disk.
34+
var ReadFile = os.ReadFile
3535

3636
// Marshal same as json.Marshal.
3737
// This variable can be modified to enable custom encoder behavior

kid_keys_aws_cognito.go

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package jwt
2+
3+
import (
4+
"crypto/rsa"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"math/big"
9+
"net/http"
10+
)
11+
12+
// |=========================================================================|
13+
// | Amazon's AWS Cognito integration for token validation and verification. |
14+
// |=========================================================================|
15+
16+
// AWSCognitoKeysConfiguration is a configuration for fetching the JSON Web Key Set from AWS Cognito.
17+
// See `LoadAWSCognitoKeys` and its `Load` and `WithClient` methods.
18+
type AWSCognitoKeysConfiguration struct {
19+
Region string `json:"region" yaml:"Region" toml:"Region" env:"AWS_COGNITO_REGION"` // e.g. "us-west-2"
20+
UserPoolID string `json:"user_pool_id" yaml:"UserPoolID" toml:"Region" env:"AWS_COGNITO_USER_POOL_ID"` // e.g. "us-west-2_XXX"
21+
22+
httpClient HTTPClient
23+
}
24+
25+
// LoadAWSCognitoKeys loads the AWS Cognito JSON Web Key Set from the given region and user pool ID.
26+
// It returns the Keys object or an error if the request fails.
27+
// It uses the default http.Client to fetch the JSON Web Key Set.
28+
// It is a shortcut for the following:
29+
//
30+
// config := jwt.AWSKeysConfiguration{
31+
// Region: region,
32+
// UserPoolID: userPoolID,
33+
// }
34+
// return config.Load()
35+
func LoadAWSCognitoKeys(region, userPoolID string) (Keys, error) {
36+
config := AWSCognitoKeysConfiguration{
37+
Region: region,
38+
UserPoolID: userPoolID,
39+
}
40+
return config.Load()
41+
}
42+
43+
// WithClient sets the HTTP client to be used for fetching the JSON Web Key Set from AWS Cognito.
44+
// If not set, the default http.Client is used.
45+
func (c *AWSCognitoKeysConfiguration) WithClient(httpClient HTTPClient) *AWSCognitoKeysConfiguration {
46+
c.httpClient = httpClient
47+
return c
48+
}
49+
50+
// Load fetches the JSON Web Key Set from AWS Cognito and parses it into a jwt.Keys object.
51+
// It returns the Keys object or an error if the request fails.
52+
// If the HTTP client is not set, the default http.Client is used.
53+
//
54+
// Calls the `ParseAWSCognitoKeys` function with the given configuration.
55+
func (c *AWSCognitoKeysConfiguration) Load() (Keys, error) {
56+
httpClient := c.httpClient
57+
if httpClient == nil {
58+
httpClient = http.DefaultClient
59+
}
60+
61+
return ParseAWSCognitoKeys(httpClient, c.Region, c.UserPoolID)
62+
}
63+
64+
// JWKSet represents a JSON Web Key Set.
65+
type JWKSet struct {
66+
Keys []*JWK `json:"keys"`
67+
}
68+
69+
// JWK represents a JSON Web Key.
70+
type JWK struct {
71+
Kty string `json:"kty"`
72+
N string `json:"n"`
73+
E string `json:"e"`
74+
Kid string `json:"kid"`
75+
Alg string `json:"alg"`
76+
Use string `json:"use"`
77+
}
78+
79+
// HTTPClient is an interface that can be used to mock the http.Client.
80+
// It is used to fetch the JSON Web Key Set from AWS Cognito.
81+
type HTTPClient interface {
82+
Get(string) (*http.Response, error)
83+
}
84+
85+
// ParseAWSCognitoKeys fetches the JSON Web Key Set from AWS Cognito and parses it into a jwt.Keys object.
86+
func ParseAWSCognitoKeys(client HTTPClient, region, userPoolID string) (Keys, error) {
87+
set, err := fetchAWSCognitoJWKSet(client, region, userPoolID)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
return parseAWSCognitoJWKSet(set)
93+
}
94+
95+
// AWSCognitoError represents an error response from AWS Cognito.
96+
// It implements the error interface.
97+
type AWSCognitoError struct {
98+
StatusCode int
99+
Message string `json:"message"`
100+
}
101+
102+
// Error returns the error message.
103+
func (e AWSCognitoError) Error() string {
104+
return e.Message
105+
}
106+
107+
// fetchAWSCognitoJWKSet fetches the JSON Web Key Set from AWS Cognito.
108+
// It returns the JWKSet object or an error if the request fails.
109+
func fetchAWSCognitoJWKSet(
110+
client HTTPClient,
111+
region string,
112+
userPoolID string,
113+
) (*JWKSet, error) {
114+
url := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", region, userPoolID)
115+
116+
resp, err := client.Get(url)
117+
if err != nil {
118+
return nil, err
119+
}
120+
defer resp.Body.Close()
121+
122+
if resp.StatusCode >= 400 {
123+
fetchErr := AWSCognitoError{
124+
StatusCode: resp.StatusCode,
125+
}
126+
127+
err = json.NewDecoder(resp.Body).Decode(&fetchErr)
128+
if err != nil {
129+
return nil, fmt.Errorf("jwt: cannot decode error message: %w", err)
130+
}
131+
132+
return nil, fetchErr
133+
}
134+
135+
var jwkSet JWKSet
136+
err = json.NewDecoder(resp.Body).Decode(&jwkSet)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
return &jwkSet, nil
142+
}
143+
144+
// parseAWSCognitoJWKSet parses the JWKSet object into a jwt.Keys object.
145+
// It returns the Keys object or an error if the parsing fails.
146+
// It filters out unsupported algorithms.
147+
func parseAWSCognitoJWKSet(set *JWKSet) (Keys, error) {
148+
keys := make(Keys, len(set.Keys))
149+
for _, key := range set.Keys {
150+
alg := parseAlg(key.Alg)
151+
if alg == nil {
152+
continue
153+
}
154+
155+
publicKey, err := convertJWKToPublicKey(key)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
keys[key.Kid] = &Key{
161+
ID: key.Kid,
162+
Alg: alg,
163+
Public: publicKey,
164+
}
165+
}
166+
167+
return keys, nil
168+
}
169+
170+
// convertJWKToPublicKey converts a JWK object to a *rsa.PublicKey object.
171+
func convertJWKToPublicKey(jwk *JWK) (*rsa.PublicKey, error) {
172+
// decode the n and e values from base64.
173+
nBytes, err := base64.RawURLEncoding.DecodeString(jwk.N)
174+
if err != nil {
175+
return nil, err
176+
}
177+
eBytes, err := base64.RawURLEncoding.DecodeString(jwk.E)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
// construct a big.Int from the n bytes.
183+
n := new(big.Int).SetBytes(nBytes)
184+
185+
// construct an int from the e bytes.
186+
var e int
187+
for _, b := range eBytes {
188+
e = e<<8 + int(b)
189+
}
190+
191+
// construct a *rsa.PublicKey from the n and e values.
192+
pubKey := &rsa.PublicKey{
193+
N: n,
194+
E: e,
195+
}
196+
197+
return pubKey, nil
198+
}

0 commit comments

Comments
 (0)