提交
This commit is contained in:
153
internal/controller/auth/github.go
Normal file
153
internal/controller/auth/github.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"os"
|
||||
"ripper/internal/app/github_auth"
|
||||
"ripper/internal/middleware"
|
||||
"ripper/internal/response"
|
||||
jwtpkg "ripper/pkg/jwt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type postLoginDeviceCodeRequest struct {
|
||||
ClientId string `json:"client_id" form:"client_id"`
|
||||
}
|
||||
|
||||
type postLoginDeviceCodeResponse struct {
|
||||
DeviceCode string `json:"device_code"` // 设备代码
|
||||
UserCode string `json:"user_code"` // 用户代码
|
||||
VerificationUrl string `json:"verification_uri"` // 验证地址
|
||||
ExpiresIn int `json:"expires_in"` // 过期时间
|
||||
Interval int `json:"interval"` // 间隔时间
|
||||
}
|
||||
|
||||
type loginDeviceRequestInfo struct {
|
||||
Code string `json:"code"`
|
||||
Authorization string `json:"authorization"`
|
||||
DisplayUserName string `json:"displayUserName,omitempty"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func postLoginDeviceCode(ctx *gin.Context) {
|
||||
cli := postLoginDeviceCodeRequest{}
|
||||
if err := ctx.ShouldBind(&cli); err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Invalid client id.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
if cli.ClientId == "" {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Client id is required.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
uid, devid, err := github_auth.BindClientToCode(cli.ClientId, 1800)
|
||||
if err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: err.Error(),
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, postLoginDeviceCodeResponse{
|
||||
DeviceCode: devid,
|
||||
UserCode: uid,
|
||||
VerificationUrl: fmt.Sprintf("%s/login/device?user_code=%s", os.Getenv("DEFAULT_BASE_URL"), uid),
|
||||
ExpiresIn: 1800,
|
||||
Interval: 5,
|
||||
})
|
||||
}
|
||||
|
||||
func postLoginOauthAccessToken(ctx *gin.Context) {
|
||||
v, exists := ctx.Get("client_auth_info")
|
||||
if !exists {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"error": "authorization_pending",
|
||||
"error_description": "The authorization request is still pending.",
|
||||
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
|
||||
})
|
||||
return
|
||||
}
|
||||
cliAuthInfo := v.(*github_auth.ClientAuthInfo)
|
||||
t := time.Now()
|
||||
t.Add(24 * 3 * time.Hour)
|
||||
u, err := github_auth.GetClientAuthInfo(cliAuthInfo.UserCode)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"error": "access_denied",
|
||||
"error_description": "You must make a new request for a device code.",
|
||||
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
|
||||
})
|
||||
return
|
||||
}
|
||||
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
|
||||
UserDisplayName: cliAuthInfo.DisplayUserName,
|
||||
CardCode: u.CardCode,
|
||||
Client: cliAuthInfo.ClientId,
|
||||
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
|
||||
})
|
||||
_ = github_auth.RemoveClientAuthInfoByDeviceCode(cliAuthInfo.ClientId)
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"access_token": tk,
|
||||
"scope": "",
|
||||
"token_type": "bearer",
|
||||
})
|
||||
}
|
||||
|
||||
func postLoginDevice(ctx *gin.Context) {
|
||||
var info loginDeviceRequestInfo
|
||||
if err := response.BindStruct(ctx, &info); err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: 422,
|
||||
Msg: "请求参数错误",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
// 验证密码
|
||||
loginPassword := os.Getenv("LOGIN_PASSWORD")
|
||||
if loginPassword != "" && info.Password != loginPassword {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: 422,
|
||||
Msg: "访问密码错误",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
// 检查code是否存在
|
||||
authInfo, err := github_auth.GetClientAuthInfo(info.Code)
|
||||
if err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: 422,
|
||||
Msg: "授权码填写错误",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
err = github_auth.UpdateClientAuthStatusByDeviceCode(authInfo.DeviceCode, info.Authorization, info.DisplayUserName)
|
||||
if err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: 500,
|
||||
Msg: "系统异常, 请稍后再试",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
response.SuccessJson(ctx, "ok")
|
||||
}
|
||||
|
||||
func getLoginDevice(ctx *gin.Context) {
|
||||
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.HTML(http.StatusOK, "code.html", gin.H{})
|
||||
}
|
||||
|
||||
func getHelpPage(ctx *gin.Context) {
|
||||
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.HTML(http.StatusOK, "help.html", gin.H{})
|
||||
}
|
||||
105
internal/controller/auth/login.go
Normal file
105
internal/controller/auth/login.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"ripper/internal/response"
|
||||
)
|
||||
|
||||
const (
|
||||
clientID = "Iv1.b507a08c87ecfe98"
|
||||
deviceCodeURL = "https://github.com/login/device/code"
|
||||
tokenURL = "https://github.com/login/oauth/access_token"
|
||||
)
|
||||
|
||||
type githubLoginDeviceRequest struct {
|
||||
DeviceCode string `form:"device_code" json:"device_code" binding:"required"`
|
||||
}
|
||||
|
||||
// getDeviceCode returns the device code for GitHub login.
|
||||
func getDeviceCode(c *gin.Context) {
|
||||
body := map[string]string{
|
||||
"client_id": clientID,
|
||||
}
|
||||
|
||||
result, err := makeRequest(c, http.MethodPost, deviceCodeURL, body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// getGhuToken returns the GitHub user token.
|
||||
func getGhuToken(c *gin.Context) {
|
||||
var params githubLoginDeviceRequest
|
||||
if err := c.ShouldBind(¶ms); err != nil {
|
||||
response.FailJson(c, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Invalid request: " + err.Error(),
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"client_id": clientID,
|
||||
"device_code": params.DeviceCode,
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}
|
||||
|
||||
result, err := makeRequest(c, http.MethodPost, tokenURL, body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// getGithubLoginDevice returns the login page for GitHub.
|
||||
func getGithubLoginDevice(ctx *gin.Context) {
|
||||
ctx.Header("Content-Type", "text/html; charset=utf-8")
|
||||
ctx.HTML(http.StatusOK, "login.html", gin.H{})
|
||||
}
|
||||
|
||||
// makeRequest makes a request to the given URL with the given method and body.
|
||||
func makeRequest(c *gin.Context, method, url string, body map[string]string) (interface{}, error) {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(c, method, url, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("accept", "application/json")
|
||||
req.Header.Set("content-type", "application/json")
|
||||
req.Header.Set("editor-plugin-version", "copilot-intellij/1.5.21.6667")
|
||||
req.Header.Set("copilot-language-server-version", "1.228.0")
|
||||
req.Header.Set("user-agent", "GithubCopilot/1.228.0")
|
||||
req.Header.Set("editor-version", "JetBrains-IU/242.21829.142")
|
||||
|
||||
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
|
||||
client := &http.Client{Timeout: httpClientTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
100
internal/controller/auth/oauth.go
Normal file
100
internal/controller/auth/oauth.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"os"
|
||||
"ripper/internal/app/github_auth"
|
||||
"ripper/internal/cache"
|
||||
"ripper/internal/middleware"
|
||||
"ripper/internal/response"
|
||||
jwtpkg "ripper/pkg/jwt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type getLoginOauthAuthorizeRequest struct {
|
||||
ClientId string `json:"client_id" form:"client_id"`
|
||||
Prompt string `json:"prompt" form:"prompt"`
|
||||
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||
Scope string `json:"scope" form:"scope"`
|
||||
State string `json:"state" form:"state"`
|
||||
}
|
||||
|
||||
func getLoginOauthAuthorize(ctx *gin.Context) {
|
||||
req := getLoginOauthAuthorizeRequest{}
|
||||
err := ctx.BindQuery(&req)
|
||||
if err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Invalid request.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
vsCopilotClientId := os.Getenv("VS_COPILOT_CLIENT_ID")
|
||||
if req.ClientId != vsCopilotClientId {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Invalid client id.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
oauthCode := github_auth.GenDevicesCode(20)
|
||||
cai := github_auth.ClientOAuthInfo{
|
||||
ClientId: req.ClientId,
|
||||
Code: oauthCode,
|
||||
Scope: req.Scope,
|
||||
}
|
||||
cacheKey := "oauth2_authorize_" + req.ClientId
|
||||
caiInfo, _ := json.Marshal(cai)
|
||||
err = cache.Set(cacheKey, caiInfo, 300)
|
||||
if err != nil {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Internal error.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the client's redirect_uri
|
||||
browserSessionId := github_auth.GenDevicesCode(64)
|
||||
ctx.Redirect(302, req.RedirectUri+"?browserSessionId="+browserSessionId+"&code="+oauthCode+"&state="+req.State)
|
||||
}
|
||||
|
||||
func postLoginOauthAccessTokenForVs2022(ctx *gin.Context) {
|
||||
v, exists := ctx.Get("client_auth_info")
|
||||
if !exists {
|
||||
response.FailJson(ctx, response.FailStruct{
|
||||
Code: -1,
|
||||
Msg: "Invalid client id.",
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
cliAuthInfo := v.(*github_auth.ClientOAuthInfo)
|
||||
t := time.Now()
|
||||
t.Add(24 * 3 * time.Hour)
|
||||
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
|
||||
CardCode: cliAuthInfo.Code,
|
||||
Client: cliAuthInfo.ClientId,
|
||||
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
|
||||
})
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"access_token": tk,
|
||||
"scope": cliAuthInfo.Scope,
|
||||
"token_type": "bearer",
|
||||
})
|
||||
}
|
||||
|
||||
func getSiteSha(ctx *gin.Context) {
|
||||
ctx.Header("X-GitHub-Request-Id", "C0E1:6A1A:1A1F:2A1D:1A1F:1A1F:1A1F:1A1F")
|
||||
ctx.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func getLoginConfig(ctx *gin.Context) {
|
||||
loginPassword := os.Getenv("LOGIN_PASSWORD")
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"is_login_password": loginPassword != "",
|
||||
})
|
||||
}
|
||||
42
internal/controller/auth/router_register.go
Normal file
42
internal/controller/auth/router_register.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"ripper/internal/middleware"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GinApi(g *gin.RouterGroup) {
|
||||
g.GET("/help", getHelpPage)
|
||||
// 启动设备代码登录流程
|
||||
g.POST("/login/device/code", postLoginDeviceCode)
|
||||
g.POST("/login/device", postLoginDevice)
|
||||
g.GET("/login/device", getLoginDevice)
|
||||
g.POST("/login/oauth/access_token", func(ctx *gin.Context) {
|
||||
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
|
||||
middleware.AuthCodeFlowCheckAuth(ctx)
|
||||
} else {
|
||||
middleware.DeviceCodeCheckAuth(ctx)
|
||||
}
|
||||
}, func(ctx *gin.Context) {
|
||||
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
|
||||
postLoginOauthAccessTokenForVs2022(ctx)
|
||||
} else {
|
||||
postLoginOauthAccessToken(ctx)
|
||||
}
|
||||
})
|
||||
|
||||
// oauth2 登录
|
||||
g.GET("/login/oauth/authorize", getLoginOauthAuthorize)
|
||||
|
||||
// enterprise 验证
|
||||
g.GET("/site/sha", getSiteSha)
|
||||
|
||||
// 获取登录页面配置
|
||||
g.GET("/login/config", getLoginConfig)
|
||||
|
||||
// GitHub模拟登录获取 ghu_token
|
||||
g.GET("/github/login/device/code", getGithubLoginDevice)
|
||||
g.POST("/github/login/device/code", getDeviceCode)
|
||||
g.POST("/github/login/ghu-token", getGhuToken)
|
||||
}
|
||||
Reference in New Issue
Block a user