package oauth2 import ( "clapclap/internal/apperr" "clapclap/internal/auth" "clapclap/internal/env" "clapclap/internal/ginctx" "time" "github.com/gin-gonic/gin" "golang.org/x/oauth2" ) // Middlewares exposed by the `Authenticator` struct below are // described by this interface: type OAuth2Authenticator interface { auth.Authenticator // middlware: // 1. gets the user's token object from the oauth2 server // 2. stores the token in the context (at "oauth2.token") GetToken(c *gin.Context) // middleware (handler): // redirects the user to the oauth2 server endpoint for the given // scope Redirect(scope string) } // The underlying implementation uses the golang.org/x/oauth2 package type Authenticator struct { OAuth2Authenticator states States duration time.Duration oauth2Config oauth2.Config authenticate func(*gin.Context, *oauth2.Token) register func(*gin.Context, *oauth2.Token) } func OAuth2Error(c *gin.Context, err error, msg string) { c.Redirect(302, env.C.CLIENT_URL+"/log-in/error/oauth2-error:"+msg) apperr.AbortWithError(c, apperr.NewBadRequestError(err, msg)) } // middleware (authentication): // authenticates a request coming from the oauth2 server // two possible behaviors (based on the query string): // - sign up user // - log in user func (a Authenticator) Authenticate(c *gin.Context) { // parse the query state := c.Query("state") if state == "" { OAuth2Error(c, nil, "Callback: failed to get state") return } // validate the state err := a.states.Validate(state) if err != nil { OAuth2Error(c, err, "Callback: failed to validate state") return } // get oauth2 token from context token, err := ginctx.GetOAuth2Token(c, "oauth2.token") if err != nil { OAuth2Error(c, err, "Failed to retrieve OAuth2 token") return } // execute operation switch a.states[state].scope { case "sign-up": a.register(c, token) case "log-in": a.authenticate(c, token) default: panic("invalid state scope for oauth2. no fucking way we get here") } } func (a Authenticator) GetToken(c *gin.Context) { code := c.Query("code") if code == "" { OAuth2Error(c, nil, "failed to get code") return } ctx := c.Request.Context() token, err := a.oauth2Config.Exchange(ctx, code) if err != nil { OAuth2Error(c, err, "failed to exchange code for token") return } c.Set("oauth2.token", token) } func (a Authenticator) Redirect(scope string) gin.HandlerFunc { return func(c *gin.Context) { // stores a new state (random key with given duration) state, err := a.states.GenerateState(scope, a.duration) if err != nil { apperr.AbortWithError(c, apperr.NewInternalServerError(err, "Failed to generate OAuth state")) return } // gets the url of the fird party app and stores the state in the query url := a.oauth2Config.AuthCodeURL(state) // redirects the user c.JSON(200, gin.H{"url": url}) } } // clean up utilty: removes all expired states from the memory storage // run it as go routine when init the authenticator func (a Authenticator) CleanUpStatesEach(d time.Duration) { for { time.Sleep(d) a.cleanUpStates() } } func (a Authenticator) cleanUpStates() { now := time.Now() for k, v := range a.states { if !v.exp.IsZero() && v.exp.Before(now) { a.states.Delete(k) } } }