From d2a55c0db8499abe27682f932d090fad09bcd1f8 Mon Sep 17 00:00:00 2001 From: goosenotduck Date: Mon, 8 Jun 2026 13:47:48 -0400 Subject: [PATCH 1/4] Fix header auth + http compliancy --- auth.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index da230d9..59abe07 100644 --- a/auth.go +++ b/auth.go @@ -7,6 +7,7 @@ import ( "errors" "io" "net/http" + "strings" "time" "github.com/coreos/go-oidc/v3/oidc" @@ -164,12 +165,14 @@ func (auth *Auth) HeaderMiddleware() gin.HandlerFunc { c.AbortWithStatus(http.StatusUnauthorized) return } - if header[0:8] != "Bearer: " { + if !strings.HasPrefix(header, "Bearer ") { c.Header("WWW-Authenticate", "Bad Authentication Header") c.AbortWithStatus(http.StatusUnauthorized) return } - err := auth.setGinContext(c, header) + + jwt := strings.TrimPrefix(header, "Bearer ") + err := auth.setGinContext(c, jwt) if err != nil { log.Error("failed to set context") c.Header("WWW-Authenticate", "Authentication Token Invalid") From d3b5192c7f5a032cccf6235d1ef4028602031095 Mon Sep 17 00:00:00 2001 From: goosenotduck Date: Sat, 1 Aug 2026 23:52:22 -0400 Subject: [PATCH 2/4] Add refresh capabilities to auth --- auth.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/auth.go b/auth.go index 59abe07..aaed178 100644 --- a/auth.go +++ b/auth.go @@ -130,9 +130,37 @@ func (auth *Auth) HandleCallback(c *gin.Context) { } c.SetCookie(CookieName, oauthJWT.AccessToken, int(oauthJWT.ExpiresIn), "", "", false, true) + // refresh token + c.SetCookie("Refresh", oauthJWT.RefreshToken, 0, "", "", auth.secure, true) c.Redirect(http.StatusFound, c.Query("referer")) } +func (auth *Auth) HandleRefresh(c *gin.Context) { + refreshToken, err := c.Cookie("Refresh") + + if err != nil || refreshToken == "" { + c.AbortWithStatus(401) + return + } + + token := &oauth2.Token{ + RefreshToken: refreshToken, + } + + tokenSource := auth.oauth.TokenSource(auth.ctx, token) + newToken, err := tokenSource.Token() + if err != nil { + log.Error("failed to refresh token: ", err) + c.SetCookie("Auth", "", 0, "", "", auth.secure, true) + c.SetCookie("Refresh", "", 0, "", "", auth.secure, true) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.SetCookie(CookieName, newToken.AccessToken, int(time.Until(newToken.Expiry).Seconds()), "", "", auth.secure, true) + c.Status(204) +} + func (auth *Auth) HandleLogout(c *gin.Context) { c.SetCookie(CookieName, "", 0, "", "", false, true) c.Redirect(http.StatusFound, ProviderURI+"/protocol/openid-connect/logout?post_logout_redirect_uri="+auth.serverURL+"/&client_id="+auth.clientID+"") From 79ab0acc1a309655eef1fd5b45c814ff7ad70a63 Mon Sep 17 00:00:00 2001 From: goosenotduck Date: Sat, 1 Aug 2026 23:52:49 -0400 Subject: [PATCH 3/4] Fix documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dab78fe..204baad 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ import cshauth "github.com/computersciencehouse/csh-auth/v2" ### Initialize your csh-auth object ```go -auth := cshauth.Init( +auth, err := cshauth.Init( clientID // the OIDC client ID clientSecret // the OIDC client secret serverURL // the "base" URL that this service is hosted from, e.g. "http://localhost:8000" From 9d6bc07da1a70e56f4c1efdf5f00d2bb986ad03f Mon Sep 17 00:00:00 2001 From: goosenotduck Date: Sun, 2 Aug 2026 17:23:50 -0400 Subject: [PATCH 4/4] documentation --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 204baad..63c277b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ auth, err := cshauth.Init( ```go r.GET("/auth/login", auth.HandleLogin) // This endpoint should match the path for loginURL r.GET("/auth/callback", auth.HandleCallback) // This endpoint should match the path for callbackURL +r.POST("/auth/refresh", auth.HandleRefresh) r.GET("/auth/logout", auth.HandleLogout) ``` @@ -46,6 +47,36 @@ This works because Gin will run the widest scope function to the most narrow sco For more/all routes: Check the [Gin Middleware documentation](https://gin-gonic.com/en/docs/middleware/) page. +### Using refresh tokens + +By default, a refresh token is stored in the browser's cookies. This token can be used to fetch a new auth token wihout having to redirect the user through SSO. + +csh-auth handles this throught the refresh handler. +```go +router.POST("/auth/refresh", auth.HandleRefresh) +``` + +Whenever the auth token is expired, the browser should make a POST request to this endpoint. The server will update the browser's cookie with a new token. + +This is particularly useful for SPAs, where the token validity isn't being check with every page interaction. + +#### Example Implementation + +The following is an example implementation that could be used in the browser. + +```ts +const response = await fetch("/api/endpoint") + +if (response.status === 401) { // token expired + fetch('/auth/refresh', { // this will update the browser's cookie without requiring a page refresh + method: 'POST', + credentials: 'include', + }) +} +``` + +In a real application, the original request would typically be retried after a successful refresh. This example only demonstrates the basic refresh mechanism. + ### Get user information The information for a user is located at `gin.Context#Get("cshauth")`. This information includes the JWT information as well.