I’m building one of those profile link services called Orbytal.ink as a demo app for PlanetScale. I wanted to build something to also learn how to use DrizzleORM for TypeScript, but when I started the project I naturally started writing the backend in Go. Well I also wanted to use this opportunity to learn Clerk.dev, a simple auth platform that you can roll into your custom apps. I couldn’t find a guide on how to validate tokens in Go so I manually wrote the code to do so. Here’s how.

Getting the token

Clerk stores JWTs as cookies which are automatically sent with every web request to the same origin as the front end, so I needed to extract the token from the cookies. Here’s what that looks like in the request:

__client_uat=1684631064; __session=eyJhbGciOiJSUfadfadfzI1NiIsucahjdilfjuakldjfaklsdjfklasjdImtpZCI6Imluc18yUGxUeUNYTXFucFNnMmdjbWU1R1ZLRmUwcVYiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgiLCJdfasdcacdfadleHAiOjE2ODQ4NDU1MjQsImlhdCI6MTY4NDg0NTQ2NCwiaXNzIjoiaHR0cHM6Ly9tYXNzaXZlLXB1bWEtMjAuY2xlcmsuYWNjb3VudHMuZGV2IiwibmJmIjoxNjg0ODQ1NDU0LCJzaWQiOiJzZXNzXzJRNTZPaE5GdERid1V4YjJsN05TTUE2OW9pViIsInN1YiI6InVzZXJfMlBsVjFOTVN0YVhidlpaanFBSkhlOVhuWjE3In0.SsUUnmngfdgdfgsfgWs7EvBMkUmIuB4__EVNsXu6QG5Sp3u7o-2mQRx5uQFqP2koIkrbaOX4XBFhR1AE5smlqdwrNO8KLgXRHkaLH3-yBx20nqM1nh296vwm_tzJ9YWgm9nsTFuav6WuyTs-le08OMjKkS6komn8_Cj2zv7dRy5Dw-6sbxq24H4SG61b76LAn6jX-oRy9ItDAQ-NVCecAC4vwJGRezgSj7Z_q_RdsYinc7gryqJlLKf5q25BXqs9VTt4qOl5pNlRGuXYQGZc2OnSUxXfSUNozsvpLGklPxRFHOmohEkLxE9XSDMxbEFeLUXVl1CMtwLNw; ph_phc_QTaOhKQ4JXEeUtAFs4WxtgeBF85ynNdS9YM2089UJOn_posthog=%7B%22distinct_id%22%3A%22188008b2c4bd0d-09e4fa75ebe5d9-3d626b4b-4b9600-188008b2c4d6738%22%2C%22%24device_id%22%3A%22188008b2c4bd0d-09e4fa75ebe5d9-3d626b4b-4b9600-188008b2c4d6738%22%2C%22%24user_state%22%3A%22anonymous%22%2C%22%24sesid%22%3A%5B1684418550053%2C%221882f23f7f31e80-04ee7216d5692a-3a626b4b-4b9600-1882f23f7f46777%22%2C1684418066419%5D%2C%22%24session_recording_enabled_server_side%22%3Afalse%2C%22%24autocapture_disabled_server_side%22%3Afalse%2C%22%24active_feature_flags%22%3A%5B%5D%2C%22%24enabled_feature_flags%22%3A%7B%7D%2C%22%24feature_flag_payloads%22%3A%7B%7D%7D; __hs_cookie_cat_pref=1:true,2:true,3:true; __hstc=181257784.33fdc6dde2e6dae6f236ed60be43e38d.1684257742463.1684257742463.1684257742463.1; hubspotutk=33fdc6dde2e6dae6f236ed60be43e38d; _www_session=eyJhbm9ueW1IjMzNDQifdsdfsdfsdfsdfsLCJhbm9ueW1vdXNJZDMxYml0IjoyMDYwODQ0NTQ0LCJhbm9ueW1vdXNVdWlkIjoiOWVlMDBlYjItYWJiNi00MDhkLWJkMTAtMGYyZTdiZjAxZWE5IiwidGltZXN0YW1wIjoxNjg0MjQ2MDM0LCJ0aW1lc3RhbXBFeHBpcmFibGUiOiJ7XCJwYWdlXCI6e1wicGF0aFwiOlwiL2V2ZW5sdcasrerwsdC1mb3ItYXBwbGljYXRpb24tZGV2ZWxvcGVycy10ZWNoLXRhbGsvdGhhbmsteW91XCIsXCJyZWZlcnJlclwiOlwiXCIsXCJzZWFyY2hcIjpcIlwiLFwidGl0bGVcIjpcIk15U1FMIGZvciBearwefsdcsdrsfsdGlvbiBkZXZlbG9wZXJzIFRlY2ggVGFsa1wiLFwidXJsXCI6XCJodHRwczovL3BsYW5ldHNjYWxlLmNvbS9ldmVudHMvbXlzcWwtZm9yLWFwcGxpY2F0aW9uLWRldmVsb3BlcnMtdGVjaC10YWxrL3RoYW5rLXlvdVwifX0ifQ%3D%3D.aBnuUcadsaerw2BP5G66JPqYd2ktoGUSOHVdHz5FYLbck; __gads=ID=30dc869b326c313c-22565dc51ddd0032:T=1682943998:RT=1682943998:S=ALNI_MZu_aYs06Gb4orC-dEjM-B44LDHgw; __gpi=UID=00000984f6db8d86:T=1682943998:RT=1682943998:S=ALNI_MZSA9O4xGeXdqxaw2jTbBeCu63dNQ

The cookies are separated by ; so I started by splitting the string. The token itself is stored in the __session bit so I can extract the value by splitting each entry in the array by = and finding one that starts with __session. If the token exists, it will be put into the token variable.

token := ""
if cookie, ok := request.Headers["cookie"]; ok {
	spl := strings.Split(cookie, ";")
	for _, el := range spl {
		if strings.HasPrefix(el, "__session=") {
			spl2 := strings.Split(el, "__session=")
			if len(spl2) == 2 {
				token = spl2[1]
			}
		}
	}
}

If the token variable is still empty, I’ll return a 401.

if token == "" {
	return &events.APIGatewayProxyResponse{
		StatusCode: 401,
	}, nil
}

Validating the token

Once I get the token, I’ll spin up a new instance of the Clerk client and run the VerifyToken method to validate the token and return the claims. The user subject (unique ID) is part of those claims, so I can use it to pull the detailed info about the user who’s logged in. Each step will return 500 to the caller if it errors out.

apiKey := os.Getenv("CLERK_API_KEY")

client, err := clerk.NewClient(apiKey)
if err != nil {
	log.Println(err)
	return &events.APIGatewayProxyResponse{
		StatusCode: 500,
	}, nil
}

claims, err := client.VerifyToken(token)
if err != nil {
	log.Println(err)
	return &events.APIGatewayProxyResponse{
		StatusCode: 500,
	}, nil
}

user, err := client.Users().Read(claims.Subject)
if err != nil {
	log.Println(err)
	return &events.APIGatewayProxyResponse{
		StatusCode: 500,
	}, nil
}

Once I have this user’s info, I can be confident of who actually made the request and I can pull info for that user or let them perform operations that require them to be logged in!