diff --git a/conf/wide.go b/conf/wide.go index 978662c..e035cb3 100644 --- a/conf/wide.go +++ b/conf/wide.go @@ -18,7 +18,6 @@ package conf import ( "encoding/json" "html/template" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -66,6 +65,11 @@ type conf struct { Autocomplete bool // default autocomplete SiteStatCode template.HTML // site statistic code ReadOnly bool // read-only mode + OAuthLoginURL string + OAuthAccessTokenURL string + OAuthUserInfoURL string + OAuthClientID string + OAuthClientSecret string } // Logger. @@ -124,7 +128,7 @@ func initUsers() { user := &User{} - bytes, _ := ioutil.ReadFile(filepath.Join(Wide.Data, "users", name)) + bytes, _ := os.ReadFile(filepath.Join(Wide.Data, "users", name)) err := json.Unmarshal(bytes, user) if err != nil { logger.Errorf("Parses [%s] error: %v, skip loading this user", name, err) @@ -156,7 +160,7 @@ func initUsers() { } func initWide(confPath, confData, confServer, confLogLevel, confReadOnly string, confSiteStatCode template.HTML) { - bytes, err := ioutil.ReadFile(confPath) + bytes, err := os.ReadFile(confPath) if nil != err { logger.Error(err) diff --git a/conf/wide.json b/conf/wide.json index 981f071..e80771d 100644 --- a/conf/wide.json +++ b/conf/wide.json @@ -7,5 +7,10 @@ "StaticResourceVersion": "${time}", "Locale": "zh_CN", "SiteStatCode": "", - "ReadOnly": false + "ReadOnly": false, + "OAuthLoginURL": "", + "OAuthAccessTokenURL": "", + "OAuthUserInfoURL": "", + "OAuthClientID": "", + "OAuthClientSecret": "" } \ No newline at end of file diff --git a/go.mod b/go.mod index c80c0d9..1a357bc 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,23 @@ module github.com/88250/wide -go 1.12 +go 1.20 require ( github.com/88250/gulu v1.1.0 - github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 // indirect github.com/fsnotify/fsnotify v1.4.9 github.com/gorilla/sessions v1.2.0 github.com/gorilla/websocket v1.4.2 github.com/parnurzeal/gorequest v0.2.16 +) + +require ( + github.com/davidebianchi/go-jsonclient v1.5.0 // indirect + github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b // indirect + golang.org/x/text v0.3.2 // indirect moul.io/http2curl v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3822836..f7d03f7 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,9 @@ github.com/88250/gulu v1.1.0 h1:aU8HncW1XNssT1insCOz6rvmTUDrtsDwSnzeqTEqNFA= github.com/88250/gulu v1.1.0/go.mod h1:a2POIziN+QFeNT4Mj7FHH2lz1HEaFlMRF6wPE0NHM4U= +github.com/davidebianchi/go-jsonclient v1.5.0 h1:PVDunAF/6c30D2SSx711efsrUP3hhml+uGT6oD3lzVY= +github.com/davidebianchi/go-jsonclient v1.5.0/go.mod h1:lXd/hkx23H590Dod74j5GsmpgxF8RIAGZDt1P5+2mqo= github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 h1:TEmChtx8+IeOghiySC8kQIr0JZOdKUmRmmkuRDuYs3E= github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -31,7 +32,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b h1:h03Ur1RlPrGTjua4koYdpGl8W0eYo8p1uI9w7RPlkdk= golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/session/oauth.go b/session/oauth.go index 796c50d..ccd0605 100644 --- a/session/oauth.go +++ b/session/oauth.go @@ -15,6 +15,7 @@ package session import ( + "fmt" "html/template" "math/rand" "net/http" @@ -34,11 +35,12 @@ var states = map[string]string{} // LoginRedirectHandler redirects to HacPai auth page. func LoginRedirectHandler(w http.ResponseWriter, r *http.Request) { - loginAuthURL := "https://ld246.com/login?goto=" + conf.Wide.Server + "/login/callback" + loginAuthURL := conf.Wide.OAuthLoginURL + "?response_type=code&redirect_uri=" + conf.Wide.Server + "/login/callback" + // надо будет добавить ttlcache для state и проверять для предотвращения атак CSRF state := gulu.Rand.String(16) states[state] = state - path := loginAuthURL + "&state=" + state + "&v=" + conf.WideVersion + path := loginAuthURL + "&state=" + state + "&client_id=" + conf.Wide.OAuthClientID http.Redirect(w, r, path, http.StatusSeeOther) } @@ -51,8 +53,25 @@ func LoginCallbackHandler(w http.ResponseWriter, r *http.Request) { } delete(states, state) - accessToken := r.URL.Query().Get("access_token") - userInfo := util.HacPaiUserInfo(accessToken) + code := r.URL.Query().Get("code") + accessToken, err := util.GetOAuthToken( + conf.Wide.OAuthAccessTokenURL, + conf.Wide.OAuthClientID, + conf.Wide.OAuthClientSecret, + code, + conf.Wide.Server+`/login/callback`) + if err != nil { + http.Error(w, fmt.Sprintf(`get access_token failed. error: %s`, err.Error()), http.StatusBadRequest) + + return + } + + userInfo, err := util.OpenIdUserInfo(conf.Wide.OAuthUserInfoURL, accessToken) + if err != nil { + http.Error(w, fmt.Sprintf(`get user_info failed. error: %s`, err.Error()), http.StatusBadRequest) + + return + } userId := userInfo["userId"].(string) userName := userInfo["userName"].(string) diff --git a/util/oauth_token.go b/util/oauth_token.go new file mode 100644 index 0000000..96f75c1 --- /dev/null +++ b/util/oauth_token.go @@ -0,0 +1,65 @@ +package util + +import ( + "fmt" + "github.com/davidebianchi/go-jsonclient" + "net/http" + "net/url" +) + +type oAuthAccessTokenReq struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + Code string `json:"code"` + GrantType string `json:"grant_type"` + RedirectURI string `json:"redirect_uri"` +} +type oAuthAccessTokenResp struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` +} + +func GetOAuthToken(oAuthAccessTokenURL, clientID, clientSecret, code, redirectURI string) (string, error) { + u, err := url.Parse(oAuthAccessTokenURL) + if err != nil { + logger.Errorf(`failed to parse oAuthAccessTokenURL. error: %s`, err.Error()) + return ``, err + } + + client, err := jsonclient.New(jsonclient.Options{ + BaseURL: fmt.Sprintf(`%s://%s/`, u.Scheme, u.Host), + }) + if err != nil { + logger.Errorf(`failed to create access_token client. error: %s`, err.Error()) + return ``, err + } + + req, err := client.NewRequest(http.MethodPost, u.Path, &oAuthAccessTokenReq{ + ClientID: clientID, + ClientSecret: clientSecret, + Code: code, + GrantType: `authorization_code`, + RedirectURI: redirectURI, + }) + if err != nil { + logger.Errorf(`failed to create access_token request. error: %s`, err.Error()) + return ``, err + } + + atResp := new(oAuthAccessTokenResp) + + resp, err := client.Do(req, atResp) + if err != nil { + logger.Errorf(`failed to request access_token. error: %s`, err.Error()) + return ``, err + } + + if resp.StatusCode >= 300 { + logger.Errorf(`access_token request return wrong code. code: %d, err_msg: %s`, resp.StatusCode, resp.Status) + return ``, err + } + + return atResp.AccessToken, nil +} diff --git a/util/open_id.go b/util/open_id.go new file mode 100644 index 0000000..3295332 --- /dev/null +++ b/util/open_id.go @@ -0,0 +1,61 @@ +package util + +import ( + "fmt" + "github.com/davidebianchi/go-jsonclient" + "net/http" + "net/url" +) + +type openIdUserInfoResp struct { + Name string `json:"name"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + PreferredUsername string `json:"preferred_username"` + Email string `json:"email"` + Picture string `json:"picture"` +} + +func OpenIdUserInfo(openIdUserInfoURL, accessToken string) (map[string]any, error) { + u, err := url.Parse(openIdUserInfoURL) + if err != nil { + logger.Errorf(`failed to parse openIdUserInfoURL. error: %s`, err.Error()) + return nil, err + } + + client, err := jsonclient.New(jsonclient.Options{ + BaseURL: fmt.Sprintf(`%s://%s/`, u.Scheme, u.Host), + Headers: map[string]string{ + `Authorization`: fmt.Sprintf(`Bearer %s`, accessToken), + }, + }) + if err != nil { + logger.Errorf(`failed to create user_info client. error: %s`, err.Error()) + return nil, err + } + + req, err := client.NewRequest(http.MethodGet, u.Path, nil) + if err != nil { + logger.Errorf(`failed to create user_info request. error: %s`, err.Error()) + return nil, err + } + + uiResp := new(openIdUserInfoResp) + + resp, err := client.Do(req, uiResp) + if err != nil { + logger.Errorf(`failed to request user_info. error: %s`, err.Error()) + return nil, err + } + + if resp.StatusCode >= 300 { + logger.Errorf(`user_info request return wrong code. code: %d, err_msg: %s`, resp.StatusCode, resp.Status) + return nil, err + } + + return map[string]any{ + `userId`: uiResp.PreferredUsername, + `userName`: uiResp.Name, + `avatar`: uiResp.Picture, + }, nil +}