Last active
November 19, 2024 18:32
-
-
Save darul75/e2b233d8c2cf8bf7518169bac5023a81 to your computer and use it in GitHub Desktop.
Clerk Go SDK : Gin Web Framework middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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