Verifying JWT Tokens

When to verify JWT Tokens?

In every API call to your backend server, you should include the access_token in the header of your requests. You need to verify the access_token on each endpoint that you deem necessary. Usually, you would use a middleware so it automatically handles the verification for each of your routes.
Some good JWT middleware libraries that you can use:

Don't have a backend server? Use the API.

How to Verify the Access Token

The access token is a JWT Token, and it's signed using Asymmetric Signing Algorithm ES256. This means, unlike symmetric JWT tokens that are signed and verified using the same secret key, this asymmetric JWT Token is signed using a secret key that only Cotter knows, but can be verified using a public key that you can find here.
  • Make sure that the token is not expired
  • Make sure that the aud matches your API_KEY_ID
  • Check the authentication_method and scopes to match your API requirements (scopes are defaulted to access for now, and cannot be changed).
  • Check that the JWT is well-formed.
  • Check the signature.
  • Check that the issuer iss = https://www.cotter.app

Checking other attributes of the user

If you want to check the user's email or phone number before allowing access, for example, you want to allow only emails with a specific domain to log in, you should do the check here.

Third-Party JWT Libraries to Verify the Tokens

You can use third party libraries to verify JWT tokens. Check the list of third party libraries here. Make sure you check for the algorithm that the JWT token uses.

For Cotter's JWT Tokens, use:

  • Algorithm: ES256
    • take the key with kid = SPACE_JWT_PUBLIC:8028AAA3-EC2D-4BAA-BE7A-7C8359CCB9F9
    • Make sure you take the keys from this endpoint, and cache when necessary, but don't hard-code it. The key may change.

Examples

Access tokens are usually included in oauth_token.access_token in the responses from the SDK, or in the Authorization Header.
Node.js
Ruby
Python (Flask)
PHP
Go
1
// Install Dependency
2
yarn add cotter-node
3
​
4
// Validate token
5
var cotterNode = require("cotter-node");
6
var cotterToken = require("cotter-token-js");
7
​
8
// Validate access token
9
const access_token = oauth_token.access_token;
10
try {
11
var valid = await cotterNode.CotterValidateJWT(access_token);
12
} catch (e) {
13
// Not Valid
14
console.log(e)
15
}
16
​
17
// Read access token
18
let decoded = new cotterToken.CotterAccessToken(access_token);
19
console.log(decoded);
20
​
21
// Check that `aud` is your API KEY
22
const audience = decoded.getAudience();
23
if (audience !== YOUR_API_KEY_ID) {
24
throw "Audience doesn't match"
25
}
26
​
27
// (Optional) Checking other attributes of the user
28
// Example, only allow my company domain to login
29
const email = decoded.getIdentifier();
30
if (email.split("@")[1] !== "cotter.app") {
31
throw "Please use cotter business email instead of a personal email";
32
}
Copied!
1
# Using https://github.com/nov/json-jwt
2
​
3
require 'net/http'
4
require 'json/jwt'
5
​
6
​
7
jwks_raw = Net::HTTP.get URI("https://www.cotter.app/api/v0/token/jwks")
8
jwk_set = JSON::JWK::Set.new(
9
JSON.parse(
10
jwks_raw
11
)
12
)
13
​
14
access_token_string = 'eyJhbGciOiJFUzI1NiIsImtpZCI6IlNQQUNFX0pXVF9QVUJM...'
15
decoded_token = JSON::JWT.decode access_token_string, jwk_set
16
​
17
expected_aud = '<YOUR_API_KEY_ID>'
18
expected_iss = 'https://www.cotter.app'
19
unless (
20
decoded_token[:iss] == expected_iss &&
21
decoded_token[:aud] == expected_aud &&
22
decoded_token[:sub].present? &&
23
Time.at(decoded_token[:iat]).between?(5.minutes.ago, Time.now) &&
24
Time.at(decoded_token[:exp]) > Time.now
25
)
26
raise 'Access Token Verification Failed!'
27
end
28
​
29
print 'Cotter User id = ' + decoded_token[:sub]
Copied!
1
# Install Dependencies
2
pip install cotter
3
pip install -U flask-cors
4
​
5
# Add a flask Endpoint
6
from flask import Flask
7
from flask import request
8
from flask_cors import CORS
9
from cotter import validate
10
​
11
​
12
app = Flask(__name__)
13
CORS(app)
14
​
15
@app.route('/login', methods=['POST'])
16
def login(name=None):
17
req = request.get_json();
18
​
19
# Getting access token and validate it
20
token = req["oauth_token"]["access_token"]
21
access_token_decoded = validate.validate_access_token(token, API_KEY_ID)
22
​
23
# User Authenticated!
24
# a) Either use Cotter's Access Token for your entire API authorization
25
# OR
26
# b) You can Generate your JWT Tokens or other session management here
27
28
return resp;
Copied!
1
<?php
2
require __DIR__ . '/vendor/autoload.php';
3
use Jose\Component\Core\AlgorithmManager;
4
use Jose\Component\Core\JWK;
5
use Jose\Component\Signature\Algorithm\ES256;
6
use Jose\Component\Signature\Serializer\JWSSerializerManager;
7
use Jose\Component\Signature\Serializer\CompactSerializer;
8
use Jose\Component\Signature\JWSVerifier;
9
​
10
// The algorithm manager with the HS256 algorithm.
11
$algorithmManager = new AlgorithmManager([
12
new ES256(),
13
]);
14
​
15
// We instantiate our JWS Verifier.
16
$jwsVerifier = new JWSVerifier(
17
$algorithmManager
18
);
19
​
20
// The serializer manager. We only use the JWS Compact Serialization Mode.
21
$serializerManager = new JWSSerializerManager([
22
new CompactSerializer(),
23
]);
24
​
25
$http = new GuzzleHttp\Client();
26
$response = $http->request('GET', 'https://www.cotter.app/api/v0/token/jwks', []);
27
​
28
$keys = json_decode((string) $response->getBody(), true);
29
$jwk = new JWK($keys["keys"][0]);
30
​
31
​
32
$token = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlNQQU...";
33
​
34
// We try to load the token.
35
$jws = $serializerManager->unserialize($token);
36
​
37
// We verify the signature. This method does NOT check the header.
38
// The arguments are:
39
// - The JWS object,
40
// - The key,
41
// - The index of the signature to check. See
42
$isVerified = $jwsVerifier->verifyWithKey($jws, $jwk, 0);
43
​
44
// continue your logic here
45
?>
Copied!
1
package middleware
2
​
3
import (
4
"encoding/json"
5
"errors"
6
"fmt"
7
"io/ioutil"
8
"net/http"
9
"strings"
10
​
11
"github.com/google/uuid"
12
"github.com/labstack/echo/v4"
13
"gopkg.in/square/go-jose.v2"
14
"gopkg.in/square/go-jose.v2/jwt"
15
)
16
​
17
// πŸ‘‡ Enter your API KEY ID here
18
const API_KEY_ID = "YOUR_API_KEY_ID"
19
const JWKSURL = "https://www.cotter.app/api/v0/token/jwks"
20
const JWKSLookupKeyID = "SPACE_JWT_PUBLIC:8028AAA3-EC2D-4BAA-BE7A-7C8359CCB9F9"
21
​
22
func getKey() ([]byte, error) {
23
// Fetch the JWT Public Key from the URL
24
resp, err := http.Get(JWKSURL)
25
if err != nil {
26
return nil, err
27
}
28
body, err := ioutil.ReadAll(resp.Body)
29
if err != nil {
30
return nil, err
31
}
32
​
33
// Parse the response into our keys struct
34
keyset := make(map[string][]map[string]interface{})
35
err = json.Unmarshal(body, &keyset)
36
if err != nil {
37
return nil, err
38
}
39
​
40
// It's a Key Set = there might be multiple keys
41
// Find the key with kid = JWKSLookupKeyID
42
if len(keyset["keys"]) <= 0 {
43
return nil, errors.New("Key set is empty")
44
}
45
for _, k := range keyset["keys"] {
46
if k["kid"] == JWKSLookupKeyID {
47
key, err := json.Marshal(k)
48
if err != nil {
49
return nil, err
50
}
51
return key, nil
52
}
53
}
54
return nil, errors.New("Cannot find key with kid")
55
}
56
​
57
// validateClientAccessToken validates access token created above
58
func validateClientAccessToken(accessToken string) (map[string]interface{}, error) {
59
tok, err := jwt.ParseSigned(accessToken)
60
if err != nil {
61
return nil, errors.New("Fail parsing access token")
62
}
63
​
64
keys, err := getKey()
65
if err != nil {
66
return nil, err
67
}
68
key := jose.JSONWebKey{}
69
key.UnmarshalJSON(keys)
70
​
71
token := make(map[string]interface{})
72
if err := tok.Claims(key, &token); err != nil {
73
return nil, errors.New("Fail parsing access token to claims")
74
}
75
​
76
// Check that the aud is our API KEY ID
77
apiKeyID, ok := token["aud"].(string)
78
if !ok {
79
return nil, errors.New("fail asserting aud from jwt.MapClaims")
80
}
81
if apiKeyID != API_KEY_ID {
82
return nil, errors.New("Invalid aud, not meant for this api key id")
83
}
84
​
85
return token, nil
86
}
87
​
88
// This middleware assumes that your API endpoint looks like this:
89
// GET https://something.com/user/1
90
// Authorization: Bearer <access_token> πŸ‘ˆ We want to verify this
91
​
92
// CotterAuth is used to authenticate cotter access_tokens
93
func CotterAuth(next echo.HandlerFunc) echo.HandlerFunc {
94
return func(ctx echo.Context) error {
95
// get the access token from auth header
96
reqToken := ctx.Request().Header.Get("Authorization")
97
splitToken := strings.Split(reqToken, " ")
98
if len(splitToken) != 2 {
99
return errors.New("authorization header malformed")
100
}
101
tokenString := splitToken[1]
102
​
103
// Validate that the access token and signature is valid
104
token, err := validateClientAccessToken(tokenString)
105
if err != nil {
106
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
107
}
108
​
109
// Read other claims, such as sub = Cotter User ID
110
uIDStr, ok := token["sub"].(string)
111
if !ok {
112
return echo.NewHTTPError(http.StatusBadRequest, "fail asserting sub from jwt.MapClaims")
113
}
114
cotterUserID, err := uuid.Parse(uIDStr)
115
if err != nil {
116
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id format in jwt claims")
117
}
118
fmt.Println("User logging in = ", cotterUserID)
119
​
120
return next(ctx)
121
}
122
}
Copied!
Last modified 10mo ago