Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
```

Expand All @@ -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.
28 changes: 28 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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+"")
Expand Down