DEV Community

Jung
Jung

Posted on

Token Validation


I came across this line of code in an authentication tutorial but didn’t fully understand what it was doing under the hood, and the tutorial didn’t explain it either. So, I researched how it actually works internally and documented my findings here in the simplest way possible.

Firstly:
func ValidateToken(tokenString string)(*Claims , error)

  1. Takes a JWT string called tokenString
  2. Returns a pointer to a Claims struct containing the decoded payload if valid
  3. Returns an error if the token is invalid

my Claims Struct is as follow:

type Claims struct {
    UserID string json:"user_id"
    jwt.StandardClaims
}
Enter fullscreen mode Exit fullscreen mode

Secondly:

secretKey := GetJWTKey()
Enter fullscreen mode Exit fullscreen mode
  1. Fetches the global secret key used for signing and validating JWTs
  2. This is the same key for all users in my case since it is HS256
  3. Stored as []byte internally

Thirdly (This is the part that I had problem understanding)

jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
    return secretKey, nil
})
Enter fullscreen mode Exit fullscreen mode

Just a couple of things to understand: [
a. &Claims{} -> so Claims{} creates a new empty Claims struct first and we pass in the pointer to this created Claims struct. If we pass in just Claims{} (value), Go would copy the struct and fill in the token details in that copied struct instead of the original struct so the original struct remains unchanged

b. interface{} here is referring to generic type which can hold []byte (HS256) or RSA keys (RS256) etc...


First Step:

When jwt.ParseWithClaims function gets called firstly it does:
-> Splits the token string into three parts
-> header , payload , signature

Header:
-> Encoded in Base64
-> Contains metadata about the token, usually:
{
"alg": "HS256", // algorithm used to sign the token
"typ": "JWT" // type of token
}
alg → tells the server how to verify the signature (HMAC SHA256, RSA, etc.)
typ → usually "JWT", indicates the type of token
-> information used to verify the signature.

Payload (Claims):
-> Also encoded in Base64
-> Contains the data about the user and token
-> Payload in my case here corresponds to the Claims structure I defined above
-> An example payload JSON:

   {
  "user_id": "12345",
  "exp": 1712345678,  // expiration timestamp
  "iat": 1712342000,  // issued at timestamp
  "iss": "my-app",    // issuer
  "sub": "12345"      // subject (optional)
}
Enter fullscreen mode Exit fullscreen mode

and this payload is what gets parsed into &Claims{} struct

Signature
-> Calculated as:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
Purpose: ensures token hasn’t been tampered with
-> Only valid if the server knows the secretKey


Second Step:

Decodes header and payload into a temporary jwt.Token struct:

type Token struct {
    Raw       string
    Method    SigningMethod
    Header    map[string]interface{} -> alg types
    Claims    Claims
    Signature string
    Valid     bool
}
Enter fullscreen mode Exit fullscreen mode

Third Step:

Call the function:

func(token *jwt.Token)(interface{}, error) {return secretKey,nil}
Enter fullscreen mode Exit fullscreen mode

Here, we pass in the temporary jwt.Token struct formed in Step 2 into the function
-> Since this jwt.Token contains the header, we can dynamically choose which key to choose based on the method, but in my case as mentioned earlier I am using the same secretKey all the time so no dynamic selection is needed
-> Return the secretKey


Fourth Step:

After the key function returns secretKey, jwt.ParseWithClaims does the following internally:
-> Recalculate the expected signature:
a. Uses the decoded + + secretKey
b. Algorithm is determined by token.Method from the header(HS256, etc.)

-> Compare with the signature in the token:

type Token struct {
    Raw       string
    Method    SigningMethod
    Header    map[string]interface{} -> alg types
    Claims    Claims
    **Signature string** <- Compare with this
    Valid     bool
}
Enter fullscreen mode Exit fullscreen mode

a. If they match → token is valid and untampered
b. If they don’t → token is invalid

As mentioned above:
Signature
-> Calculated as:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)

-> Fill the Claims struct (&Claims{}):
a. Decoded payload is written into your Claims struct pointer
b. Standard claims (exp, iat, iss, etc.) and custom claims (user_id) are all set

-> token.Valid is set to true if signature matches and standard claims (e.g., expiration) are valid


Fifth Step:

After jwt.ParseWithClaims returns

if claims, ok := token.Claims.(*Claims); ok && token.Valid {
    return claims, nil
}
Enter fullscreen mode Exit fullscreen mode

-> We type assert the generic token.Claims to my concrete *Claims struct
-> If type assertion succeeds then return the value (claims of type *Claims) and ok (type boolean true)
-> Then we can safely access:
claims.UserID
claims.ExpiresAt
claims.Issuer

TLDR Flow:

  1. jwt.ParseWithClaims receives tokenString
  2. Splits token → header, payload, signature
  3. Decodes header + payload → temporary jwt.Token struct
  4. Calls key function with jwt.Token → returns secretKey
  5. Recalculates signature using secretKey
  6. Compares with token’s signature
    • Match → valid
    • No match → invalid
  7. Fills &Claims{} struct with payload data
  8. Returns token with token.Valid = true

Top comments (0)