diff --git a/README.md b/README.md index dab78fe..63c277b 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" @@ -32,6 +32,7 @@ auth := 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. 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+"")