Feat: oauth2 api

pull/21/head
zijiren233 2 years ago
parent dab669708d
commit 3f1116e2e2

@ -0,0 +1,32 @@
package model
import (
"errors"
"github.com/gin-gonic/gin"
json "github.com/json-iterator/go"
)
type OAuth2CallbackReq struct {
Code string `json:"code"`
State string `json:"state"`
}
var (
ErrInvalidOAuth2Code = errors.New("invalid oauth2 code")
ErrInvalidOAuth2State = errors.New("invalid oauth2 state")
)
func (o *OAuth2CallbackReq) Validate() error {
if o.Code == "" {
return ErrInvalidOAuth2Code
}
if o.State == "" {
return ErrInvalidOAuth2State
}
return nil
}
func (o *OAuth2CallbackReq) Decode(ctx *gin.Context) error {
return json.NewDecoder(ctx.Request.Body).Decode(o)
}

@ -2,6 +2,7 @@ package auth
import ( import (
"net/http" "net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/synctv-org/synctv/internal/conf" "github.com/synctv-org/synctv/internal/conf"
@ -9,6 +10,7 @@ import (
"github.com/synctv-org/synctv/internal/provider" "github.com/synctv-org/synctv/internal/provider"
"github.com/synctv-org/synctv/server/middlewares" "github.com/synctv-org/synctv/server/middlewares"
"github.com/synctv-org/synctv/server/model" "github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -26,7 +28,31 @@ func OAuth2(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
} }
Render(ctx, pi.NewConfig(c.ClientID, c.ClientSecret), oauth2.AccessTypeOnline) state := utils.RandString(16)
states.Store(state, struct{}{}, time.Minute*5)
RenderRedirect(ctx, pi.NewConfig(c.ClientID, c.ClientSecret).AuthCodeURL(state, oauth2.AccessTypeOnline))
}
func OAuth2Api(ctx *gin.Context) {
t := ctx.Param("type")
p := provider.OAuth2Provider(t)
c, ok := conf.Conf.OAuth2[p]
if !ok {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 provider"))
}
pi, err := p.GetProvider()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
}
state := utils.RandString(16)
states.Store(state, struct{}{}, time.Minute*5)
ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{
"url": pi.NewConfig(c.ClientID, c.ClientSecret).AuthCodeURL(state, oauth2.AccessTypeOnline),
}))
} }
// /oauth2/callback/:type // /oauth2/callback/:type
@ -38,6 +64,53 @@ func OAuth2Callback(ctx *gin.Context) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 provider")) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 provider"))
} }
req := model.OAuth2CallbackReq{}
if err := req.Decode(ctx); err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
_, loaded := states.LoadAndDelete(req.State)
if !loaded {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 state"))
return
}
pi, err := p.GetProvider()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
}
ui, err := pi.GetUserInfo(ctx, pi.NewConfig(c.ClientID, c.ClientSecret), req.Code)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
user, err := op.CreateOrLoadUser(ui.Username, p, ui.ProviderUserID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
token, err := middlewares.NewAuthUserToken(user)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
RenderToken(ctx, "/web/", token)
}
// /oauth2/callback/:type
func OAuth2CallbackApi(ctx *gin.Context) {
t := ctx.Param("type")
p := provider.OAuth2Provider(t)
c, ok := conf.Conf.OAuth2[p]
if !ok {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 provider"))
}
code := ctx.Query("code") code := ctx.Query("code")
if code == "" { if code == "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 code")) ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 code"))

@ -6,8 +6,14 @@ func Init(e *gin.Engine) {
{ {
auth := e.Group("/oauth2") auth := e.Group("/oauth2")
auth.GET("/enabled", OAuth2EnabledApi)
auth.GET("/login/:type", OAuth2) auth.GET("/login/:type", OAuth2)
auth.POST("/login/:type", OAuth2Api)
auth.GET("/callback/:type", OAuth2Callback) auth.GET("/callback/:type", OAuth2Callback)
auth.POST("/callback/:type", OAuth2CallbackApi)
} }
} }

@ -0,0 +1,13 @@
package auth
import (
"github.com/gin-gonic/gin"
"github.com/synctv-org/synctv/internal/conf"
"golang.org/x/exp/maps"
)
func OAuth2EnabledApi(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"enabled": maps.Keys(conf.Conf.OAuth2),
})
}

@ -6,26 +6,30 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/synctv-org/synctv/utils"
synccache "github.com/synctv-org/synctv/utils/syncCache" synccache "github.com/synctv-org/synctv/utils/syncCache"
"golang.org/x/oauth2"
) )
//go:embed templates/redirect.html //go:embed templates/*.html
var temp embed.FS var temp embed.FS
var ( var (
redirectTemplate *template.Template redirectTemplate *template.Template
tokenTemplate *template.Template
states *synccache.SyncCache[string, struct{}] states *synccache.SyncCache[string, struct{}]
) )
func Render(ctx *gin.Context, c *oauth2.Config, option ...oauth2.AuthCodeOption) error { func RenderRedirect(ctx *gin.Context, url string) error {
state := utils.RandString(16) ctx.Header("Content-Type", "text/html; charset=utf-8")
states.Store(state, struct{}{}, time.Minute*5) return redirectTemplate.Execute(ctx.Writer, url)
return redirectTemplate.Execute(ctx.Writer, c.AuthCodeURL(state, option...)) }
func RenderToken(ctx *gin.Context, url, token string) error {
ctx.Header("Content-Type", "text/html; charset=utf-8")
return tokenTemplate.Execute(ctx.Writer, map[string]string{"Url": url, "Token": token})
} }
func init() { func init() {
redirectTemplate = template.Must(template.ParseFS(temp, "templates/redirect.html")) redirectTemplate = template.Must(template.ParseFS(temp, "templates/redirect.html"))
tokenTemplate = template.Must(template.ParseFS(temp, "templates/token.html"))
states = synccache.NewSyncCache[string, struct{}](time.Minute * 10) states = synccache.NewSyncCache[string, struct{}](time.Minute * 10)
} }

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting..</title>
</head>
<body>
<p>If you are not redirected, please click <a href="{{ .Url }}">here</a>.</p>
<script>localStorage.setItem("userToken", "{{ .Token }}"); window.location.href = "{{ .Url }}"</script>
</body>
</html>
Loading…
Cancel
Save