From b62474e2c1631d2e4d5391fc0c9c375916a56ce3 Mon Sep 17 00:00:00 2001 From: Rafal Gawlik Date: Tue, 28 Sep 2021 14:38:57 +0200 Subject: [PATCH 1/4] add zoom provider --- providers/zoom/session.go | 62 ++++++++++++ providers/zoom/session_test.go | 47 +++++++++ providers/zoom/zoom.go | 176 +++++++++++++++++++++++++++++++++ providers/zoom/zoom_test.go | 54 ++++++++++ 4 files changed, 339 insertions(+) create mode 100644 providers/zoom/session.go create mode 100644 providers/zoom/session_test.go create mode 100644 providers/zoom/zoom.go create mode 100644 providers/zoom/zoom_test.go diff --git a/providers/zoom/session.go b/providers/zoom/session.go new file mode 100644 index 000000000..80c4ccce9 --- /dev/null +++ b/providers/zoom/session.go @@ -0,0 +1,62 @@ +package zoom + +import ( + "encoding/json" + "errors" + "github.com/markbates/goth" + "strings" + "time" +) + +// Session stores data during the auth process with Zoom. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +var _ goth.Session = &Session{} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Zoom provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Zoom and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + s := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(s) + return s, err +} diff --git a/providers/zoom/session_test.go b/providers/zoom/session_test.go new file mode 100644 index 000000000..e282b084e --- /dev/null +++ b/providers/zoom/session_test.go @@ -0,0 +1,47 @@ +package zoom_test + +import ( + "github.com/markbates/goth" + "github.com/markbates/goth/providers/zoom" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Implements_Session(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &zoom.Session{} + + a.Implements((*goth.Session)(nil), s) +} + +func Test_GetAuthURL(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &zoom.Session{} + + _, err := s.GetAuthURL() + a.Error(err) + + s.AuthURL = "/foo" + + url, _ := s.GetAuthURL() + a.Equal(url, "/foo") +} + +func Test_ToJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &zoom.Session{} + + data := s.Marshal() + a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`) +} + +func Test_String(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &zoom.Session{} + + a.Equal(s.String(), s.Marshal()) +} diff --git a/providers/zoom/zoom.go b/providers/zoom/zoom.go new file mode 100644 index 000000000..e874d78cd --- /dev/null +++ b/providers/zoom/zoom.go @@ -0,0 +1,176 @@ +// Package zoom implements the OAuth2 protocol for authenticating users through zoo. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package zoom + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/markbates/goth" + "golang.org/x/oauth2" + "io" + "net/http" +) + +var ( + authorizeURL string = "https://zoom.us/oauth/authorize" + tokenURL string = "https://zoom.us/oauth/token" + profileURL string = "https://zoom.us/v2/users/me" +) + +// Provider is the implementation of `goth.Provider` for accessing Zoom. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +type profileResp struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + AvatarURL string `json:"pic_url"` + ID string `json:"id"` +} + +// New creates a new Zoom provider and sets up connection details. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "zoom", + } + p.config = newConfig(p, scopes) + return p +} + +// Name is the name used to retrieve the provider. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the zoom package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth returns zoom authentication endpoint. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// FetchUser makes a request to profileURL and returns zoom user data. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + s := session.(*Session) + user := goth.User{ + AccessToken: s.AccessToken, + Provider: p.Name(), + RefreshToken: s.RefreshToken, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", profileURL, nil) + if err != nil { + return user, err + } + + req.Header.Set("Authorization", "Bearer "+s.AccessToken) + resp, err := p.Client().Do(req) + if err != nil { + return user, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) + } + + err = userFromReader(resp.Body, &user) + return user, err +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authorizeURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + } + + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + var rawData map[string]interface{} + + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(r) + if err != nil { + return err + } + + err = json.Unmarshal(buf.Bytes(), &rawData) + if err != nil { + return err + } + + u := &profileResp{} + err = json.Unmarshal(buf.Bytes(), &u) + if err != nil { + return err + } + + user.Email = u.Email + user.FirstName = u.FirstName + user.LastName = u.LastName + user.Name = fmt.Sprintf("%s %s", u.FirstName, u.LastName) + user.UserID = u.ID + user.AvatarURL = u.AvatarURL + user.RawData = rawData + + return nil +} diff --git a/providers/zoom/zoom_test.go b/providers/zoom/zoom_test.go new file mode 100644 index 000000000..cb33bfb92 --- /dev/null +++ b/providers/zoom/zoom_test.go @@ -0,0 +1,54 @@ +package zoom_test + +import ( + "fmt" + "github.com/markbates/goth" + "github.com/markbates/goth/providers/zoom" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func zoomProvider() *zoom.Provider { + return zoom.New(os.Getenv("ZOOM_KEY"), os.Getenv("ZOOM_SECRET"), "/foo", "basic") +} + +func Test_New(t *testing.T) { + t.Parallel() + a := assert.New(t) + + provider := zoomProvider() + a.Equal(provider.ClientKey, os.Getenv("ZOOM_KEY")) + a.Equal(provider.Secret, os.Getenv("ZOOM_SECRET")) + a.Equal(provider.CallbackURL, "/foo") +} + +func Test_Implements_Provider(t *testing.T) { + t.Parallel() + a := assert.New(t) + a.Implements((*goth.Provider)(nil), zoomProvider()) +} +func Test_BeginAuth(t *testing.T) { + t.Parallel() + a := assert.New(t) + provider := zoomProvider() + session, err := provider.BeginAuth("test_state") + s := session.(*zoom.Session) + a.NoError(err) + a.Contains(s.AuthURL, "https://zoom.us/oauth/authorize") + a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("ZOOM_KEY"))) + a.Contains(s.AuthURL, "state=test_state") +} + +func Test_SessionFromJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + + provider := zoomProvider() + + s, err := provider.UnmarshalSession(`{"AuthURL":"https://app.zoom.io/oauth","AccessToken":"1234567890"}`) + a.NoError(err) + session := s.(*zoom.Session) + a.Equal(session.AuthURL, "https://app.zoom.io/oauth") + a.Equal(session.AccessToken, "1234567890") +} From c072d3b2daf9058d869f3d905b83a9dc8187d717 Mon Sep 17 00:00:00 2001 From: Rafal Gawlik Date: Tue, 28 Sep 2021 14:39:04 +0200 Subject: [PATCH 2/4] update examples and readme --- README.md | 1 + examples/main.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index d3f1088e0..76d739d53 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ $ go get github.com/markbates/goth * Yahoo * Yammer * Yandex +* Zoom ## Examples diff --git a/examples/main.go b/examples/main.go index c56119c0b..8bdbc2483 100644 --- a/examples/main.go +++ b/examples/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/markbates/goth/providers/zoom" "html/template" "net/http" "os" @@ -136,6 +137,7 @@ func main() { strava.New(os.Getenv("STRAVA_KEY"), os.Getenv("STRAVA_SECRET"), "http://localhost:3000/auth/strava/callback"), okta.New(os.Getenv("OKTA_ID"), os.Getenv("OKTA_SECRET"), os.Getenv("OKTA_ORG_URL"), "http://localhost:3000/auth/okta/callback", "openid", "profile", "email"), mastodon.New(os.Getenv("MASTODON_KEY"), os.Getenv("MASTODON_SECRET"), "http://localhost:3000/auth/mastodon/callback", "read:accounts"), + zoom.New(os.Getenv("ZOOM_KEY"), os.Getenv("ZOOM_SECRET"), "http://localhost:3000/auth/zoom/callback", "read:user"), ) // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) @@ -202,6 +204,7 @@ func main() { m["strava"] = "Strava" m["okta"] = "Okta" m["mastodon"] = "Mastodon" + m["zoom"] = "Zoom" var keys []string for k := range m { From 1d899f7c741b1ab2a5c272c398aed6653ae0ccb1 Mon Sep 17 00:00:00 2001 From: Rafal Gawlik Date: Tue, 5 Oct 2021 16:16:13 +0200 Subject: [PATCH 3/4] fix imports order --- examples/main.go | 2 +- providers/zoom/session.go | 3 ++- providers/zoom/session_test.go | 3 ++- providers/zoom/zoom.go | 6 ++++-- providers/zoom/zoom_test.go | 5 +++-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/main.go b/examples/main.go index 8bdbc2483..8321e8487 100644 --- a/examples/main.go +++ b/examples/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "github.com/markbates/goth/providers/zoom" "html/template" "net/http" "os" @@ -69,6 +68,7 @@ import ( "github.com/markbates/goth/providers/yahoo" "github.com/markbates/goth/providers/yammer" "github.com/markbates/goth/providers/yandex" + "github.com/markbates/goth/providers/zoom" ) func main() { diff --git a/providers/zoom/session.go b/providers/zoom/session.go index 80c4ccce9..37bc1d6f7 100644 --- a/providers/zoom/session.go +++ b/providers/zoom/session.go @@ -3,9 +3,10 @@ package zoom import ( "encoding/json" "errors" - "github.com/markbates/goth" "strings" "time" + + "github.com/markbates/goth" ) // Session stores data during the auth process with Zoom. diff --git a/providers/zoom/session_test.go b/providers/zoom/session_test.go index e282b084e..470aa81b7 100644 --- a/providers/zoom/session_test.go +++ b/providers/zoom/session_test.go @@ -1,10 +1,11 @@ package zoom_test import ( + "testing" + "github.com/markbates/goth" "github.com/markbates/goth/providers/zoom" "github.com/stretchr/testify/assert" - "testing" ) func Test_Implements_Session(t *testing.T) { diff --git a/providers/zoom/zoom.go b/providers/zoom/zoom.go index e874d78cd..eff1a9d06 100644 --- a/providers/zoom/zoom.go +++ b/providers/zoom/zoom.go @@ -6,10 +6,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/markbates/goth" - "golang.org/x/oauth2" "io" "net/http" + + "golang.org/x/oauth2" + + "github.com/markbates/goth" ) var ( diff --git a/providers/zoom/zoom_test.go b/providers/zoom/zoom_test.go index cb33bfb92..b90263dfe 100644 --- a/providers/zoom/zoom_test.go +++ b/providers/zoom/zoom_test.go @@ -2,11 +2,12 @@ package zoom_test import ( "fmt" + "os" + "testing" + "github.com/markbates/goth" "github.com/markbates/goth/providers/zoom" "github.com/stretchr/testify/assert" - "os" - "testing" ) func zoomProvider() *zoom.Provider { From bfdd06cfd6d6557646be5cb4a6168b78c0b2f208 Mon Sep 17 00:00:00 2001 From: Rafal Gawlik Date: Wed, 6 Oct 2021 22:29:39 +0200 Subject: [PATCH 4/4] gofmt -s -w ./ --- examples/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/main.go b/examples/main.go index 42f93abc8..7b83879c8 100644 --- a/examples/main.go +++ b/examples/main.go @@ -205,7 +205,7 @@ func main() { m["okta"] = "Okta" m["mastodon"] = "Mastodon" m["wecom"] = "WeCom" - m["zoom"] = "Zoom" + m["zoom"] = "Zoom" var keys []string for k := range m {