Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple custom field on a JWT token #1033

Closed
khash opened this issue Dec 15, 2023 · 5 comments
Closed

Simple custom field on a JWT token #1033

khash opened this issue Dec 15, 2023 · 5 comments
Assignees

Comments

@khash
Copy link

khash commented Dec 15, 2023

I'm trying what I think is a simple scenario. I have an extra field (custom claims) on my JWT token and I'd like it to be made available in a custom JWT struct. I'm using this in conjunction with JWKS and the echo middleware, but I don't think those details matter in the outcome.

Here is a quick example:

type customToken struct {
  jwt.Token
  MyField  string `json:"myfield"`
}
//...
func (c *customToken) CheckMyField() bool {
  // some custom logic to check if MyField is valid
  return true
}

In using the JWX middleware, I'm also using TokenFactory to create a new customToken:

	protected.Use(jwx.JWXWithConfig(ks, jwx.Config{
		TokenFactory: func(c echo.Context) jwt.Token {
			return &customToken{}
		},
	}))

I've also registered the custom field:

jwt.RegisterCustomField("myfield", "")

All of the above, leads to getting a nil pointer error during the parsing of the token as part of the first check in isIssueAtValid, since the token is nil.

I've worked around the problem by not using a custom token and converting (c *customToken) CheckMyField() to CheckMyField(token jwt.Token) which works, but I would like to know what I'm doing wrong in the setup, please.

Thank you

@lestrrat
Copy link
Collaborator

I don't understand your question: You show snippets allegedly declaring a field, but I have no idea how you are using them, which gives me zero information as to exactly what you are expecting to happen. This is exactly why we have an issue template that explicitly asks for a stand alone Go test code.


This module provides objects that can handle custom fields as is, without having to declare them. If your JWT has

{
   "foo": "bar",
   ... other standard JWT claims ...
}

Then you can just parse the JWT without any setup, and access foo by:

token, _ := jwt.Parse(...)
v, ok := token.Get(`foo`)

The only annoyance is that since we (this module) does not know what the type of this field is, it's going to just give you back whatever encoding/json gave us. In this case, probably a string, which you then have to convert explicitly:

foo := v.(string)

RegisterCustomField helps in this regard, but I don't recommend using it unless you understand how this module works. For more examples on accessing values stored in a token, please see the jwt_get_claims_example_test.go file.

@khash
Copy link
Author

khash commented Dec 16, 2023

I'm sorry if the question is confusing to you and perhaps the echo middleware is the culprit here. I understand that in this module adding custom fields will yield them back from a parsed token if I were to use jwt.Parse method. However, when using this module in conjunction with the echo middleware (https://github.com/lestrrat-go/echo-middleware-jwx) the job of detecting the type of the desired Go struct is left to TokenFactory (https://github.com/lestrrat-go/echo-middleware-jwx/blob/a7763b9faf665e2be86d0f7ae60870c79fb0df54/interface.go#L83C7-L83C7) which takens in a "sample" object of the desired struct.

As for the issue template and standalone working code, given that the echo middleware is not compatible with v2 (there is an outstanding PR for it since March, putting it together was not very feasible, but I'll try.

@khash
Copy link
Author

khash commented Dec 16, 2023

Here is a sample code that shows what I mean (I had to build it against https://github.com/khash/echo-middleware-jwx so it works with v2) but it is identical to the sample code here https://github.com/lestrrat-go/echo-middleware-jwx

Obviously you'd need a valid JWKS endpoint that you can issue tokens for to make it run, but I have those already and the JWKs infrastructure is working as expected.

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	jwx "github.com/khash/echo-middleware-jwx"
	"github.com/labstack/echo/v4"
	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

type customToken struct {
	jwt.Token
}

func main() {
	const certs = `CHANGE_THIS`

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	e := echo.New()

	c := jwk.NewCache(ctx)
	c.Register(certs, jwk.WithMinRefreshInterval(15*time.Minute))
	ks, err := c.Refresh(ctx, certs)
	if err != nil {
		panic(fmt.Sprintf("failed to refresh google JWKS: %s\n", err))
	}

	works := e.Group("/works")
	doesntWork := e.Group("/doesnt-work")

	works.Use(jwx.JWX(ks))

	doesntWork.Use(jwx.JWXWithConfig(ks, jwx.Config{
		TokenFactory: func(c echo.Context) jwt.Token {
			return &customToken{}
		},
		ContextKey: "token",
	}))

	works.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})

	doesntWork.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})

	e.Start(":8000")
}

You will notice that added JWXWithConfig to the middleware so I can pass a config and a key set into it, it's a trivial piece here https://github.com/khash/echo-middleware-jwx/blob/227842a0b5842fed6cb3658c80cf380444274f9f/jwx.go#L111

In this example, I haven't added any custom fields to the customToken yet but the issue remains. When you hit /works/ all is ok. When you hit /doesnt-work you get the following panic:

echo: http: panic serving [::1]:57132: runtime error: invalid memory address or nil pointer dereference
goroutine 38 [running]:
net/http.(*conn).serve.func1()
	/usr/local/go/src/net/http/server.go:1868 +0xb0
panic({0x102df88a0?, 0x10305bea0?})
	/usr/local/go/src/runtime/panic.go:920 +0x26c
main.(*customToken).IssuedAt(0x140000d7478?)
	<autogenerated>:1 +0x28
github.com/lestrrat-go/jwx/v2/jwt.isIssuedAtValid({0x102e5e898, 0x140000a53e0}, {0x102e60370?, 0x140000aa540?})
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/validate.go:382 +0x38
github.com/lestrrat-go/jwx/v2/jwt.ValidatorFunc.Validate(0x102e5e898?, {0x102e5e898?, 0x140000a53e0?}, {0x102e60370?, 0x140000aa540?})
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/validate.go:302 +0x44
github.com/lestrrat-go/jwx/v2/jwt.Validate({0x102e60370, 0x140000aa540}, {0x0?, 0x0, 0x140000aa540?})
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/validate.go:92 +0x77c
github.com/lestrrat-go/jwx/v2/jwt.parse(0x140000d7868, {0x140000e82c0, 0x283, 0x2c0})
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/jwt.go:363 +0x6e8
github.com/lestrrat-go/jwx/v2/jwt.parseBytes({0x140000e82c0, 0x283, 0x2c0}, {0x140000b0200?, 0x2, 0x102a78b98?})
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/jwt.go:230 +0x804
github.com/lestrrat-go/jwx/v2/jwt.ParseString(...)
	/Users/khash/work/go/pkg/mod/github.com/lestrrat-go/jwx/v2@v2.0.18/jwt/jwt.go:98
github.com/khash/echo-middleware-jwx.(*Config).parseToken(0x140000ac820, {0x140000e8007, 0x283}, {0x102e61cf8, 0x140000c4140})
	/Users/khash/work/go/pkg/mod/github.com/khash/echo-middleware-jwx@v0.0.0-20231215163603-227842a0b584/jwx.go:88 +0x838
github.com/khash/echo-middleware-jwx.(*Config).Authenticate(0x140000ac820, {0x102e61cf8, 0x140000c4140})
	/Users/khash/work/go/pkg/mod/github.com/khash/echo-middleware-jwx@v0.0.0-20231215163603-227842a0b584/jwx.go:203 +0x108
github.com/khash/echo-middleware-jwx.JWXWithConfig.WithConfig.func1.1({0x102e61cf8, 0x140000c4140})
	/Users/khash/work/go/pkg/mod/github.com/khash/echo-middleware-jwx@v0.0.0-20231215163603-227842a0b584/jwx.go:228 +0x3c
github.com/labstack/echo/v4.(*Echo).add.func1({0x102e61cf8, 0x140000c4140})
	/Users/khash/work/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:582 +0x50
github.com/labstack/echo/v4.(*Echo).ServeHTTP(0x140001b7200, {0x102e5dfa8?, 0x140000ec000}, 0x140000a6100)
	/Users/khash/work/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:669 +0x374
net/http.serverHandler.ServeHTTP({0x140000a4f30?}, {0x102e5dfa8?, 0x140000ec000?}, 0x6?)
	/usr/local/go/src/net/http/server.go:2938 +0xbc
net/http.(*conn).serve(0x140000a23f0, {0x102e5e898, 0x140000a4e40})
	/usr/local/go/src/net/http/server.go:2009 +0x518
created by net/http.(*Server).Serve in goroutine 1
	/usr/local/go/src/net/http/server.go:3086 +0x4cc

@lestrrat
Copy link
Collaborator

I'm going to step back about adding custom fields, and will explain what I can tell you about the panic.

See:

main.(*customToken).IssuedAt(0x140000d7478?)
	<autogenerated>:1 +0x28

THIS is where the panic is occurring. Upon looking at your code, see where you define customToken

type customToken struct {
	jwt.Token
}

and how you initialize it:

			return &customToken{}

Your customToken is a struct which has an embedded field name Token whose type is the interface jwt.Token. However, you never initialize Token. So your initialization is basically equivalent to:

                       return &customToken{ Token: nil }

Then at jwt/validate.go, token.IssuedAt() is called. Since token is your &customToken{Token: nil}, it tries to delegate the method IssuedAt to the value in Token field, and it's nil, and there boom. panic.

Q.E.D. :)

@khash
Copy link
Author

khash commented Dec 16, 2023

Ah! I think as I was trying to migrate from the basic JWT echo middleware to this library I was looking around and saw that in custom fields there is a typ := reflect.TypeOf(object) and assumed they are related so much as pertaining to JSON unmarshaling and therefore assumed incorrectly that all is needed is a "sample" object for the type to be inferred. My mistake. Thank you

@khash khash closed this as completed Dec 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants