diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index f843d289..a4e0cb32 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -112,8 +112,7 @@ version = "v1.0.0" [[projects]] - branch = "master" - digest = "1:495f89256b4fef47e64160d109454685afa4949f400d02833207e2af383d1173" + digest = "1:2e6b942dd80c33bba11b9567f3f8a4338e80d597959e4a5d182d5251953bebe6" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -124,7 +123,8 @@ "token", ] pruneopts = "UT" - revision = "46d3a954882f41fc0efdf551e2c1b070799e0f54" + revision = "a4dab49e2656a32ab7eb1255980c24f8560dc298" + version = "v0.1.1" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 61ed2f0a..5f91ae12 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -70,7 +70,7 @@ func TestServerApp_DevMode(t *testing.T) { time.Sleep(100 * time.Millisecond) // let server start assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers()), "extra auth provider") - assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name, "dev auth provider") + assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name(), "dev auth provider") // send ping resp, err := http.Get("http://localhost:18085/api/v1/ping") require.Nil(t, err) diff --git a/backend/app/rest/api/rest_public.go b/backend/app/rest/api/rest_public.go index 49134e3c..cfba32f4 100644 --- a/backend/app/rest/api/rest_public.go +++ b/backend/app/rest/api/rest_public.go @@ -238,7 +238,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { cnf.Auth = []string{} for _, ap := range s.Authenticator.Providers() { - cnf.Auth = append(cnf.Auth, ap.Name) + cnf.Auth = append(cnf.Auth, ap.Name()) } if cnf.Admins == nil { // prevent json serialization to nil diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index f1b6a1d3..f6f4b8b5 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -13,9 +13,10 @@ This library provides "social login" with Github, Google, Facebook and Yandex. - Black list with user-defined validator - Multiple aud (audience) supported - Secure key with customizable `SecretReader` -- Ability to store extra information to token and retrieve on login +- Ability to store an extra information to token and retrieve on login - Pre-auth and post-auth hooks to handle custom use cases. - Middleware for easy integration into http routers +- Wrappers to extract user info from the request ## Install @@ -107,35 +108,35 @@ It also has placeholders for fields application can populate with custom `token. - `IP` - hash of user's IP address - `Email` - user's email -- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. +- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provided, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. ### Avatar proxy -Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy +Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides an automatic proxy - On each login the proxy will retrieve user's picture and save it to `AvatarStore` - Local (proxied) link to avatar included in user's info (jwt token) - API for avatar removal provided as a part of `AvatarStore` -- User can leverage one of provided stores: +- User can leverage one of the provided stores: - `avatar.LocalFS` - file system, each avatar in a separate file - - `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). + - `avatar.BoltDB` - single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). - `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db). -- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). +- In case of need custom implementations of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). - All avatar-related setup done as a part of `auth.Opts` and needs: - `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")` - - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") - - `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. + - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links like this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") + - `AvatarResizeLimit` - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. ### Customization There are several ways to adjust functionality of the library: -1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification +1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification 1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. 2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. -All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. +All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. ### Implementing black list logic or some other filters @@ -157,11 +158,10 @@ Working with oauth2 providers can be a pain, especially during development phase ```go // runs dev oauth2 server on :8084 go func() { - p, err := service.Provider("dev") + devAuthServer, err := service.DevAuth() if err != nil { log.Fatal(err) } - devAuthServer := provider.DevAuthServer{Provider: p} devAuthServer.Run() }() ``` @@ -175,7 +175,7 @@ _Warning: this is not the real oauth2 server but just a small fake thing for dev In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate: 1. Send JWT header as `X-JWT`. This shouldn't be used for web application, however can be helpful for service-to-service authentication. -2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode by default disabled and will be enabled it `Opts.AdminPasswd` defined. This will allow access with basic auth admin: with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts. +2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode disabled by default and will be enabled if `Opts.AdminPasswd` defined. This will allow access with basic auth admin: with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts. ### Logging @@ -239,4 +239,4 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine. -`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible. \ No newline at end of file +`go-pkgz/auth` library still in development and until version 1 released some breaking changes possible. \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index 3b2161ec..a5b04f8a 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -131,7 +131,7 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl if elems[len(elems)-1] == "list" { list := []string{} for _, p := range s.providers { - list = append(list, p.Name) + list = append(list, p.Name()) } rest.RenderJSON(w, r, list) return @@ -189,15 +189,15 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { switch strings.ToLower(name) { case "github": - s.providers = append(s.providers, provider.NewGithub(p)) + s.providers = append(s.providers, provider.NewService(provider.NewGithub(p))) case "google": - s.providers = append(s.providers, provider.NewGoogle(p)) + s.providers = append(s.providers, provider.NewService(provider.NewGoogle(p))) case "facebook": - s.providers = append(s.providers, provider.NewFacebook(p)) + s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p))) case "yandex": - s.providers = append(s.providers, provider.NewFacebook(p)) + s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p))) case "dev": - s.providers = append(s.providers, provider.NewDev(p)) + s.providers = append(s.providers, provider.NewService(provider.NewDev(p))) default: return } @@ -212,13 +212,13 @@ func (s *Service) DevAuth() (*provider.DevAuthServer, error) { return nil, errors.Wrap(err, "dev provider not registered") } // make and start dev auth server - return &provider.DevAuthServer{Provider: p, L: s.logger}, nil + return &provider.DevAuthServer{Provider: p.Provider.(provider.Oauth2Handler), L: s.logger}, nil } // Provider gets provider by name func (s *Service) Provider(name string) (provider.Service, error) { for _, p := range s.providers { - if p.Name == name { + if p.Name() == name { return p, nil } } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go index 98549af1..8d93fc96 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -27,7 +27,7 @@ const devAuthPort = 8084 // desired user name, this is the mode used for development. Non-interactive mode for tests only. type DevAuthServer struct { logger.L - Provider Service + Provider Oauth2Handler Automatic bool username string // unsafe, but fine for dev @@ -39,7 +39,7 @@ type DevAuthServer struct { // Run oauth2 dev server on port devAuthPort func (d *DevAuthServer) Run(ctx context.Context) { d.username = "dev_user" - d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL) + d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.redirectURL) d.lock.Lock() var err error d.iconGen, err = identicon.New("github", 5, 3) @@ -77,7 +77,7 @@ func (d *DevAuthServer) Run(ctx context.Context) { } state := r.URL.Query().Get("state") - callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state) + callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.redirectURL, state) d.Logf("[DEBUG] callback url=%s", callbackURL) w.Header().Add("Location", callbackURL) w.WriteHeader(http.StatusFound) @@ -150,17 +150,17 @@ func (d *DevAuthServer) Shutdown() { } // NewDev makes dev oauth2 provider for admin user -func NewDev(p Params) Service { - return initService(p, Service{ - Name: "dev", - Endpoint: oauth2.Endpoint{ +func NewDev(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "dev", + endpoint: oauth2.Endpoint{ AuthURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/authorize", devAuthPort), TokenURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/access_token", devAuthPort), }, - RedirectURL: p.URL + "/auth/dev/callback", - Scopes: []string{"user:email"}, - InfoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), - MapUser: func(data userData, _ []byte) token.User { + redirectURL: p.URL + "/auth/dev/callback", + scopes: []string{"user:email"}, + infoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort), + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: data.value("id"), Name: data.value("name"), diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go new file mode 100644 index 00000000..17ec0134 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/auth/provider/oauth2.go @@ -0,0 +1,213 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/rest" + "golang.org/x/oauth2" + + "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/token" +) + +// Oauth2Handler implements /login, /callback and /logout handlers from aouth2 flow +type Oauth2Handler struct { + Params + + // all of these fields specific to particular oauth2 provider + name string + redirectURL string + infoURL string + endpoint oauth2.Endpoint + scopes []string + mapUser func(userData, []byte) token.User // map info from InfoURL to User + conf oauth2.Config +} + +// Params to make initialized and ready to use provider +type Params struct { + logger.L + URL string + JwtService TokenService + Cid string + Csecret string + Issuer string + AvatarSaver AvatarSaver +} + +type userData map[string]interface{} + +func (u userData) value(key string) string { + // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string + if val, ok := u[key]; ok && val != nil { + return fmt.Sprintf("%v", val) + } + return "" +} + +// initOauth2Handler makes oauth2 handler for given provider +func initOauth2Handler(p Params, service Oauth2Handler) Oauth2Handler { + if p.L == nil { + p.L = logger.Func(func(fmt string, args ...interface{}) {}) + } + p.Logf("[INFO] init oauth2 service %s", service.name) + service.Params = p + service.conf = oauth2.Config{ + ClientID: service.Cid, + ClientSecret: service.Csecret, + RedirectURL: service.redirectURL, + Scopes: service.scopes, + Endpoint: service.endpoint, + } + + p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", + service.name, service.Cid, service.endpoint, service.redirectURL) + return service +} + +// Name returns provider name +func (p Oauth2Handler) Name() string { return p.name } + +// LoginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 +func (p Oauth2Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { + + p.Logf("[DEBUG] login with %s", p.Name) + // make state (random) and store in session + state, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state") + return + } + + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + + claims := token.Claims{ + Handshake: &token.Handshake{ + State: state, + From: r.URL.Query().Get("from"), + }, + SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", + StandardClaims: jwt.StandardClaims{ + Id: cid, + Audience: r.URL.Query().Get("site"), + ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), + NotBefore: time.Now().Add(-1 * time.Minute).Unix(), + }, + } + + if err := p.JwtService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") + return + } + + // return login url + loginURL := p.conf.AuthCodeURL(state) + p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims) + + http.Redirect(w, r, loginURL, http.StatusFound) +} + +// AuthHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser +// GET /callback +func (p Oauth2Handler) AuthHandler(w http.ResponseWriter, r *http.Request) { + oauthClaims, _, err := p.JwtService.Get(r) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get token") + return + } + + if oauthClaims.Handshake == nil { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "invalid handshake token") + return + } + + retrievedState := oauthClaims.Handshake.State + if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") + return + } + + p.Logf("[DEBUG] token with state %s", retrievedState) + tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code")) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed") + return + } + + client := p.conf.Client(context.Background(), tok) + uinfo, err := client.Get(p.infoURL) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info") + return + } + + defer func() { + if e := uinfo.Body.Close(); e != nil { + p.Logf("[WARN] failed to close response body, %s", e) + } + }() + + data, err := ioutil.ReadAll(uinfo.Body) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info") + return + } + + jData := map[string]interface{}{} + if e := json.Unmarshal(data, &jData); e != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info") + return + } + p.Logf("[DEBUG] got raw user info %+v", jData) + + u := p.mapUser(jData, data) + u, err = setAvatar(p.AvatarSaver, u) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save avatar to proxy") + return + } + + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") + return + } + claims := token.Claims{ + User: &u, + StandardClaims: jwt.StandardClaims{ + Issuer: p.Issuer, + Id: cid, + Audience: oauthClaims.Audience, + }, + SessionOnly: oauthClaims.SessionOnly, + } + + if err = p.JwtService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") + return + } + + p.Logf("[DEBUG] user info %+v", u) + + // redirect to back url if presented in login query params + if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" { + http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect) + return + } + rest.RenderJSON(w, r, &u) +} + +// LogoutHandler - GET /logout +func (p Oauth2Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + p.JwtService.Reset(w) +} diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go index cb5ee7db..9474bc7d 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go @@ -14,14 +14,14 @@ import ( ) // NewGoogle makes google oauth2 provider -func NewGoogle(p Params) Service { - return initService(p, Service{ - Name: "google", - Endpoint: google.Endpoint, - RedirectURL: p.URL + "/auth/google/callback", - Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, - InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", - MapUser: func(data userData, _ []byte) token.User { +func NewGoogle(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "google", + endpoint: google.Endpoint, + redirectURL: p.URL + "/auth/google/callback", + scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, + infoURL: "https://www.googleapis.com/oauth2/v3/userinfo", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ // encode email with provider name to avoid collision if same id returned by other provider ID: "google_" + token.HashID(sha1.New(), data.value("sub")), @@ -37,14 +37,14 @@ func NewGoogle(p Params) Service { } // NewGithub makes github oauth2 provider -func NewGithub(p Params) Service { - return initService(p, Service{ - Name: "github", - Endpoint: github.Endpoint, - RedirectURL: p.URL + "/auth/github/callback", - Scopes: []string{}, - InfoURL: "https://api.github.com/user", - MapUser: func(data userData, _ []byte) token.User { +func NewGithub(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "github", + endpoint: github.Endpoint, + redirectURL: p.URL + "/auth/github/callback", + scopes: []string{}, + infoURL: "https://api.github.com/user", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: "github_" + token.HashID(sha1.New(), data.value("login")), Name: data.value("name"), @@ -60,7 +60,7 @@ func NewGithub(p Params) Service { } // NewFacebook makes facebook oauth2 provider -func NewFacebook(p Params) Service { +func NewFacebook(p Params) Oauth2Handler { // response format for fb /me call type uinfo struct { @@ -73,13 +73,13 @@ func NewFacebook(p Params) Service { } `json:"picture"` } - return initService(p, Service{ - Name: "facebook", - Endpoint: facebook.Endpoint, - RedirectURL: p.URL + "/auth/facebook/callback", - Scopes: []string{"public_profile"}, - InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", - MapUser: func(data userData, bdata []byte) token.User { + return initOauth2Handler(p, Oauth2Handler{ + name: "facebook", + endpoint: facebook.Endpoint, + redirectURL: p.URL + "/auth/facebook/callback", + scopes: []string{"public_profile"}, + infoURL: "https://graph.facebook.com/me?fields=id,name,picture", + mapUser: func(data userData, bdata []byte) token.User { userInfo := token.User{ ID: "facebook_" + token.HashID(sha1.New(), data.value("id")), Name: data.value("name"), @@ -98,15 +98,15 @@ func NewFacebook(p Params) Service { } // NewYandex makes yandex oauth2 provider -func NewYandex(p Params) Service { - return initService(p, Service{ - Name: "yandex", - Endpoint: yandex.Endpoint, - RedirectURL: p.URL + "/auth/yandex/callback", - Scopes: []string{}, +func NewYandex(p Params) Oauth2Handler { + return initOauth2Handler(p, Oauth2Handler{ + name: "yandex", + endpoint: yandex.Endpoint, + redirectURL: p.URL + "/auth/yandex/callback", + scopes: []string{}, // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ - InfoURL: "https://login.yandex.ru/info?format=json", - MapUser: func(data userData, _ []byte) token.User { + infoURL: "https://login.yandex.ru/info?format=json", + mapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ ID: "yandex_" + token.HashID(sha1.New(), data.value("id")), Name: data.value("display_name"), // using Display Name by default diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index e6d55d5e..0915e2f1 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -1,47 +1,25 @@ package provider import ( - "context" "crypto/rand" "crypto/sha1" - "encoding/json" "fmt" - "io/ioutil" "net/http" "strings" - "time" - "github.com/go-pkgz/auth/logger" - - jwt "github.com/dgrijalva/jwt-go" - "github.com/go-pkgz/rest" "github.com/pkg/errors" - "golang.org/x/oauth2" "github.com/go-pkgz/auth/token" ) -// Service represents oauth2 provider +// Service represents oauth2 provider. Adds Handler method multiplexing login, auth and logout requests type Service struct { - Params - Name string - RedirectURL string - InfoURL string - Endpoint oauth2.Endpoint - Scopes []string - MapUser func(userData, []byte) token.User // map info from InfoURL to User - conf oauth2.Config + Provider } -// Params to make initialized and ready to use provider -type Params struct { - logger.L - URL string - JwtService TokenService - AvatarSaver AvatarSaver - Cid string - Csecret string - Issuer string +// NewService makes service for given provider +func NewService(p Provider) Service { + return Service{Provider: p} } // AvatarSaver defines minimal interface to save avatar @@ -57,34 +35,12 @@ type TokenService interface { Reset(w http.ResponseWriter) } -type userData map[string]interface{} - -func (u userData) value(key string) string { - // json.Unmarshal converts json "null" value to go's "nil", in this case return empty string - if val, ok := u[key]; ok && val != nil { - return fmt.Sprintf("%v", val) - } - return "" -} - -// initService makes oauth2 service for given provider -func initService(p Params, service Service) Service { - if p.L == nil { - p.L = logger.Func(func(fmt string, args ...interface{}) {}) - } - p.Logf("[INFO] init oauth2 service %s", service.Name) - service.Params = p - service.conf = oauth2.Config{ - ClientID: service.Cid, - ClientSecret: service.Csecret, - RedirectURL: service.RedirectURL, - Scopes: service.Scopes, - Endpoint: service.Endpoint, - } - - p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", - service.Name, service.Cid, service.Endpoint, service.RedirectURL) - return service +// Provider defines interface for auth handler +type Provider interface { + Name() string + LoginHandler(w http.ResponseWriter, r *http.Request) + AuthHandler(w http.ResponseWriter, r *http.Request) + LogoutHandler(w http.ResponseWriter, r *http.Request) } // Handler returns auth routes for given provider @@ -95,11 +51,11 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) { return } if strings.HasSuffix(r.URL.Path, "/login") { - p.loginHandler(w, r) + p.LoginHandler(w, r) return } if strings.HasSuffix(r.URL.Path, "/callback") { - p.authHandler(w, r) + p.AuthHandler(w, r) return } if strings.HasSuffix(r.URL.Path, "/logout") { @@ -109,153 +65,20 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) } -// loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1 -func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { - - p.Logf("[DEBUG] login with %s", p.Name) - // make state (random) and store in session - state, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state") - return - } - - cid, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") - return - } - - claims := token.Claims{ - Handshake: &token.Handshake{ - State: state, - From: r.URL.Query().Get("from"), - }, - SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", - StandardClaims: jwt.StandardClaims{ - Id: cid, - Audience: r.URL.Query().Get("site"), - ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), - NotBefore: time.Now().Add(-1 * time.Minute).Unix(), - }, - } - - if err := p.JwtService.Set(w, claims); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") - return - } - - // return login url - loginURL := p.conf.AuthCodeURL(state) - p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims) - - http.Redirect(w, r, loginURL, http.StatusFound) -} - -// authHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser -// GET /callback -func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { - oauthClaims, _, err := p.JwtService.Get(r) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get token") - return - } - - if oauthClaims.Handshake == nil { - rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token") - return - } - - retrievedState := oauthClaims.Handshake.State - if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { - rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") - return - } - - p.Logf("[DEBUG] token with state %s", retrievedState) - tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code")) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed") - return - } - - client := p.conf.Client(context.Background(), tok) - uinfo, err := client.Get(p.InfoURL) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info") - return - } - - defer func() { - if e := uinfo.Body.Close(); e != nil { - p.Logf("[WARN] failed to close response body, %s", e) - } - }() - - data, err := ioutil.ReadAll(uinfo.Body) - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info") - return - } - - jData := map[string]interface{}{} - if e := json.Unmarshal(data, &jData); e != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info") - return - } - p.Logf("[DEBUG] got raw user info %+v", jData) - - u := p.MapUser(jData, data) - u = p.setAvatar(u) - - cid, err := p.randToken() - if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id") - return - } - claims := token.Claims{ - User: &u, - StandardClaims: jwt.StandardClaims{ - Issuer: p.Issuer, - Id: cid, - Audience: oauthClaims.Audience, - }, - SessionOnly: oauthClaims.SessionOnly, - } - - if err = p.JwtService.Set(w, claims); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") - return - } - - p.Logf("[DEBUG] user info %+v", u) - - // redirect to back url if presented in login query params - if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" { - http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect) - return - } - rest.RenderJSON(w, r, &u) -} - // setAvatar saves avatar and puts proxied URL to u.Picture -func (p Service) setAvatar(u token.User) token.User { - if p.AvatarSaver != nil { - if avatarURL, e := p.AvatarSaver.Put(u); e == nil { - u.Picture = avatarURL - } else { - p.Logf("[WARN] failed to set avatar for %+v, %+v", u, e) +func setAvatar(ava AvatarSaver, u token.User) (token.User, error) { + if ava != nil { + avatarURL, e := ava.Put(u) + if e != nil { + return u, errors.Wrap(e, "failed to save avatar for") } + u.Picture = avatarURL + return u, nil } - return u + return u, nil // empty AvatarSaver ok, just skipped } -// LogoutHandler - GET /logout -func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) { - p.JwtService.Reset(w) -} - -func (p Service) randToken() (string, error) { +func randToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", errors.Wrap(err, "can't get random")