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 (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/synctv-org/synctv/internal/conf"
@ -9,6 +10,7 @@ import (
"github.com/synctv-org/synctv/internal/provider"
"github.com/synctv-org/synctv/server/middlewares"
"github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
"golang.org/x/oauth2"
)
@ -26,7 +28,31 @@ func OAuth2(ctx *gin.Context) {
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
@ -38,6 +64,53 @@ func OAuth2Callback(ctx *gin.Context) {
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")
if code == "" {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("invalid oauth2 code"))

@ -6,8 +6,14 @@ func Init(e *gin.Engine) {
{
auth := e.Group("/oauth2")
auth.GET("/enabled", OAuth2EnabledApi)
auth.GET("/login/:type", OAuth2)
auth.POST("/login/:type", OAuth2Api)
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"
"github.com/gin-gonic/gin"
"github.com/synctv-org/synctv/utils"
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 (
redirectTemplate *template.Template
tokenTemplate *template.Template
states *synccache.SyncCache[string, struct{}]
)
func Render(ctx *gin.Context, c *oauth2.Config, option ...oauth2.AuthCodeOption) error {
state := utils.RandString(16)
states.Store(state, struct{}{}, time.Minute*5)
return redirectTemplate.Execute(ctx.Writer, c.AuthCodeURL(state, option...))
func RenderRedirect(ctx *gin.Context, url string) error {
ctx.Header("Content-Type", "text/html; charset=utf-8")
return redirectTemplate.Execute(ctx.Writer, url)
}
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() {
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)
}

@ -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