appleboy / gin-jwt

JWT Middleware for Gin framework
MIT License
2.73k stars 382 forks source link

Support for SSO/Oauth #310

Open JPFrancoia opened 1 year ago

JPFrancoia commented 1 year ago

Hi,

This library looks very useful. However at the moment I don't think it supports oauth? Basically I don't want my users to register with a username/password, but instead I'd like them to use SSO, maybe provided by Facebook or Google. Once the oauth flow is complete, I'd like to use my own JWT tokens though. I'd like to use the user info I get from SSO to build the token.

So far I have something like this:

auth.go

func init() {
    // TODO: callback url should be an env var
    facebookProvider := facebook.New(
        os.Getenv("CLIENT_ID"),
        os.Getenv("CLIENT_SECRET"),
        "http://localhost:8080/auth_callback",
    )
    goth.UseProviders(facebookProvider)
}

func Login(c *gin.Context) {
    // TODO: exit if request has valid token?

    // Insert provider into context
    // https://github.com/markbates/goth/issues/411#issuecomment-891037676
    q := c.Request.URL.Query()
    // TODO: make sure :provider is in {'facebook', 'google'},
    // we can't add just any param to the url. Return 400 if not
    q.Add("provider", c.Param("provider"))
    c.Request.URL.RawQuery = q.Encode()

    // try to get the user without re-authenticating
    if user, err := gothic.CompleteUserAuth(c.Writer, c.Request); err == nil {
        logger.Debug("Re-using existing credentials")
        // t, _ := template.New("foo").Parse(userTemplate)
        // t.Execute(c.Writer, user)

        // Return token if we have a valid user
        token, err := createJWT(user.UserID, user.Email)
        if err != nil {
            c.AbortWithError(http.StatusUnauthorized, err)
        }
        c.JSON(http.StatusOK, map[string]string{"token": token})

    } else {
        logger.Debug("Starting auth flow")
        gothic.BeginAuthHandler(c.Writer, c.Request)
    }
}

func AuthCallback(c *gin.Context) {
    user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
    logger.Debug(user)

    if err != nil {
        c.AbortWithError(http.StatusUnauthorized, err)
    }

    // TODO: write user into DB, including provider used

    token, err := createJWT(user.UserID, user.Email)
    if err != nil {
        c.AbortWithError(http.StatusUnauthorized, err)
    }
    c.JSON(http.StatusOK, map[string]string{"token": token})
}

type MyCustomClaims struct {
    Id    string `json:"id"`
    Email string `json:"email"`
    jwt.RegisteredClaims
}

func createJWT(userId string, email string) (string, error) {

    // TODO: in module's init. Make sure this env var is set
    signingKey := []byte(os.Getenv("JWT_SECRET"))

    // Create claims while leaving out some of the optional fields
    claims := MyCustomClaims{
        Id:    userId,
        Email: email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    signedString, err := token.SignedString(signingKey)

    if err != nil {
        return "", err
    }

    return signedString, nil
}

main.go

func main() {

    router := gin.New()

    router.GET("/auth/:provider", auth.Login)
    router.GET("/auth_callback", auth.AuthCallback)
}

I have a HTML page that redirects to auth/{provider} and then the OAuth flow begins. The user is redirected to Facebook, authorizes my app, and then is redirected to my app where they will hit the /auth_callback. That's where I get the user info and I build the token.

This works, but I was wondering if this library supports the 2-steps OAuth process. In the example above, I haven't implemented the refresh token, the logout, or the validation of the token. I'd prefer trusting this library rather than re-implementing that myself, potentially with some security vulnerabilities haha.

If the library can't do that, maybe you heard of another one that could do that?