Skip to content

Instantly share code, notes, and snippets.

@darul75
Last active April 8, 2024 23:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save darul75/e2b233d8c2cf8bf7518169bac5023a81 to your computer and use it in GitHub Desktop.
Save darul75/e2b233d8c2cf8bf7518169bac5023a81 to your computer and use it in GitHub Desktop.
Clerk Go SDK : Gin Web Framework middleware
package clerk
import (
"errors"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/clerkinc/clerk-sdk-go/clerk"
"github.com/gin-gonic/gin"
"github.com/go-jose/go-jose/v3/jwt"
)
const ACTIVE_SESSION_CLAIMS = "ActiveSessionClaims"
func WithSessionV2Gin(client clerk.Client, verifyTokenOptions ...clerk.VerifyTokenOption) func(handler gin.HandlerFunc) gin.HandlerFunc {
return func(next gin.HandlerFunc) gin.HandlerFunc {
return gin.HandlerFunc(func(c *gin.Context) {
// ****************************************************
// *
// HEADER AUTHENTICATION *
// *
// ****************************************************
_, authorizationHeaderExists := c.Request.Header["Authorization"]
if authorizationHeaderExists {
headerToken := strings.TrimSpace(c.Request.Header.Get("Authorization"))
headerToken = strings.TrimPrefix(headerToken, "Bearer ")
_, err := client.DecodeToken(headerToken)
if err != nil {
next(c)
return
}
claims, err := client.VerifyToken(headerToken, verifyTokenOptions...)
if err == nil { // signed in
c.Set(ACTIVE_SESSION_CLAIMS, claims)
next(c)
return
}
// Clerk.js should refresh the token and retry
c.Status(http.StatusUnauthorized)
return
}
// In development or staging environments only, based on the request User Agent, detect non-browser
// requests (e.g. scripts). If there is no Authorization header, consider the user as signed out
// and prevent interstitial rendering
if isDevelopmentOrStaging(client) && !strings.HasPrefix(c.Request.UserAgent(), "Mozilla/") {
// signed out
next(c)
return
}
// in cross-origin requests the use of Authorization
// header is mandatory
if isCrossOrigin(c.Request) {
// signed out
next(c)
return
}
// ****************************************************
// *
// COOKIE AUTHENTICATION *
// *
// ****************************************************
cookieToken, _ := c.Request.Cookie("__session")
clientUat, _ := c.Request.Cookie("__client_uat")
if isDevelopmentOrStaging(client) && (c.Request.Referer() == "" || isCrossOrigin(c.Request)) {
renderInterstitial(client, c.Writer)
return
}
if isProduction(client) && clientUat == nil {
c.Next()
return
}
if clientUat != nil && clientUat.Value == "0" {
c.Next()
return
}
if clientUat == nil {
renderInterstitial(client, c.Writer)
return
}
var clientUatTs int64
ts, err := strconv.ParseInt(clientUat.Value, 10, 64)
if err == nil {
clientUatTs = ts
}
if cookieToken == nil {
renderInterstitial(client, c.Writer)
return
}
claims, err := client.VerifyToken(cookieToken.Value, verifyTokenOptions...)
if err == nil {
if claims.IssuedAt != nil && clientUatTs <= int64(*claims.IssuedAt) {
c.Set(ACTIVE_SESSION_CLAIMS, claims)
next(c)
return
}
renderInterstitial(client, c.Writer)
return
}
if errors.Is(err, jwt.ErrExpired) || errors.Is(err, jwt.ErrIssuedInTheFuture) {
renderInterstitial(client, c.Writer)
return
}
// signed out
next(c)
})
}
}
func isDevelopmentOrStaging(c clerk.Client) bool {
return strings.HasPrefix(c.APIKey(), "test_") || strings.HasPrefix(c.APIKey(), "sk_test_")
}
func isProduction(c clerk.Client) bool {
return !isDevelopmentOrStaging(c)
}
func renderInterstitial(c clerk.Client, w http.ResponseWriter) {
w.Header().Set("content-type", "text/html")
w.WriteHeader(401)
resp, _ := c.Interstitial()
w.Write(resp)
}
var urlSchemeRe = regexp.MustCompile(`(^\w+:|^)\/\/`)
func isCrossOrigin(r *http.Request) bool {
// origin contains scheme+host and optionally port (ommitted if 80 or 443)
// ref. https://www.rfc-editor.org/rfc/rfc6454#section-6.1
origin := strings.TrimSpace(r.Header.Get("Origin"))
origin = urlSchemeRe.ReplaceAllString(origin, "") // strip scheme
if origin == "" {
return false
}
// parse request's host and port, taking into account reverse proxies
u := &url.URL{Host: r.Host}
host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
if host == "" {
host = u.Hostname()
}
port := strings.TrimSpace(r.Header.Get("X-Forwarded-Port"))
if port == "" {
port = u.Port()
}
if port != "" && port != "80" && port != "443" {
host = net.JoinHostPort(host, port)
}
return origin != host
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment