From 07fdc5d797adcffdf0787246760f6a5e51d85812 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sat, 13 Nov 2021 21:16:09 +0000 Subject: [PATCH 001/102] feat: initial workos implementation --- README.md | 8 +-- api/external.go | 2 + api/external_workos_test.go | 33 ++++++++++++ api/provider/workos.go | 103 ++++++++++++++++++++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 8 ++- hack/test.env | 4 ++ 9 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 api/external_workos_test.go create mode 100644 api/provider/workos.go diff --git a/README.md b/README.md index 5949d8192b..3e6daefabb 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ The default group to assign all new users to. ### External Authentication Providers -We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `linkedin`, `notion`, `spotify`, `slack`, `twitch` and `twitter` for external authentication. +We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `linkedin`, `notion`, `spotify`, `slack`, `twitch`, `twitter` and `workos` for external authentication. Use the names as the keys underneath `external` to configure each separately. @@ -514,7 +514,9 @@ Returns the publicly available settings for this gotrue instance. "slack": true, "spotify": true, "twitch": true, - "twitter": true + "twitter": true, + "tiktok": true, + "workos": true, }, "disable_signup": false, "autoconfirm": false @@ -922,7 +924,7 @@ Get access_token from external oauth provider query params: ``` -provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | linkedin | notion | slack | spotify | twitch | twitter +provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | linkedin | notion | slack | spotify | twitch | twitter | workos scopes= ``` diff --git a/api/external.go b/api/external.go index 4d4a96c3b7..4b94a519a3 100644 --- a/api/external.go +++ b/api/external.go @@ -416,6 +416,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewTwitchProvider(config.External.Twitch, scopes) case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) + case "workos": + return provider.NewWorkOSProvider(config.External.Twitter, scopes) case "saml": return provider.NewSamlProvider(config.External.Saml, a.db, getInstanceID(ctx)) case "zoom": diff --git a/api/external_workos_test.go b/api/external_workos_test.go new file mode 100644 index 0000000000..04682b8ec6 --- /dev/null +++ b/api/external_workos_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "net/url" + + jwt "github.com/golang-jwt/jwt" +) + +func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=workos", nil) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + ts.Require().Equal(http.StatusFound, w.Code) + u, err := url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + q := u.Query() + ts.Equal(ts.Config.External.Spotify.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.Spotify.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + ts.Equal("", q.Get("scope")) + + claims := ExternalProviderClaims{} + p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(ts.Config.JWT.Secret), nil + }) + ts.Require().NoError(err) + + ts.Equal("workos", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} diff --git a/api/provider/workos.go b/api/provider/workos.go new file mode 100644 index 0000000000..197fe03e5d --- /dev/null +++ b/api/provider/workos.go @@ -0,0 +1,103 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +const ( + defaultWorkOSAPIBase = "https://api.workos.com" +) + +type workosProvider struct { + *oauth2.Config + APIPath string +} + +/* +{ + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "okta", + "email": "todd@foo-corp.com", + "first_name": "Todd", + "idp_id": "00u1a0ufowBJlzPlk357", + "last_name": "Rundgren", + "object": "profile", + "raw_attributes": {...} +} +*/ +type workosUser struct { + ID string `json:"id"` + ConnectionId string `json:"connection_id"` + ConnectionType string `json:"connection_type"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Object string `json:"object"` + IdpId string `json:"idp_id"` + RawAttributes map[string]interface{} `json:"raw_attributes"` +} + +// NewWorkOSProvider creates a WorkOS account provider. +func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.Validate(); err != nil { + return nil, err + } + + apiPath := chooseHost(ext.URL, defaultWorkOSAPIBase) + + oauthScopes := []string{} + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + return &workosProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID, + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: apiPath + "/sso/authorize", + TokenURL: apiPath + "/sso/token", + }, + Scopes: oauthScopes, + RedirectURL: ext.RedirectURI, + }, + APIPath: apiPath, + }, nil +} + +func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + // TODO rework this as the TokenURL returns only an access token and the profile + return g.Exchange(oauth2.NoContext, code) +} + +func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u workosUser + // TODO rework this as the only way to get profile is with TokenURL + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/sso/token", &u); err != nil { + return nil, err + } + + return &UserProvidedData{ + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: u.FirstName, + Email: u.Email, + + // To be deprecated + FullName: fmt.Sprintf("%s %s", u.FirstName, u.LastName), + ProviderId: u.ID, + }, + Emails: []Email{{ + Email: u.Email, + Primary: true, + }}, + }, nil +} diff --git a/api/settings.go b/api/settings.go index 56e760efb8..929593ed4d 100644 --- a/api/settings.go +++ b/api/settings.go @@ -15,6 +15,7 @@ type ProviderSettings struct { Notion bool `json:"notion"` Spotify bool `json:"spotify"` Slack bool `json:"slack"` + WorkOS bool `json:"workos"` Twitch bool `json:"twitch"` Twitter bool `json:"twitter"` Email bool `json:"email"` @@ -55,6 +56,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { Slack: config.External.Slack.Enabled, Twitch: config.External.Twitch.Enabled, Twitter: config.External.Twitter.Enabled, + WorkOS: config.External.WorkOS.Enabled, Email: config.External.Email.Enabled, Phone: config.External.Phone.Enabled, SAML: config.External.Saml.Enabled, diff --git a/api/settings_test.go b/api/settings_test.go index 922b5ff016..4e02e5bf88 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -41,6 +41,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.GitLab) require.True(t, p.SAML) require.True(t, p.Twitch) + require.True(t, p.WorkOS) require.True(t, p.Zoom) } diff --git a/conf/configuration.go b/conf/configuration.go index ee9bc0a5e1..a224d89a73 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -95,6 +95,7 @@ type ProviderConfiguration struct { Slack OAuthProviderConfiguration `json:"slack"` Twitter OAuthProviderConfiguration `json:"twitter"` Twitch OAuthProviderConfiguration `json:"twitch"` + WorkOS OAuthProviderConfiguration `json:"workos"` Email EmailProviderConfiguration `json:"email"` Phone PhoneProviderConfiguration `json:"phone"` Saml SamlProviderConfiguration `json:"saml"` diff --git a/example.env b/example.env index 44cc6899a8..b639e2df80 100644 --- a/example.env +++ b/example.env @@ -137,6 +137,12 @@ GOTRUE_EXTERNAL_SLACK_CLIENT_ID="" GOTRUE_EXTERNAL_SLACK_SECRET="" GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="https://localhost:9999/callback" +# WorkOS OAuth config +GOTRUE_EXTERNAL_WORKOS_ENABLED="true" +GOTRUE_EXTERNAL_WORKOS_CLIENT_ID="" +GOTRUE_EXTERNAL_WORKOS_SECRET="" +GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="https://localhost:9999/callback" + # Zoom OAuth config GOTRUE_EXTERNAL_ZOOM_ENABLED="false" GOTRUE_EXTERNAL_ZOOM_CLIENT_ID="" @@ -191,4 +197,4 @@ GOTRUE_WEBHOOK_EVENTS=validate,signup,login # Cookie config GOTRUE_COOKIE_KEY: "sb" -GOTRUE_COOKIE_DOMAIN: "localhost" \ No newline at end of file +GOTRUE_COOKIE_DOMAIN: "localhost" diff --git a/hack/test.env b/hack/test.env index 9b5d9b89c8..6d7f74ffb2 100644 --- a/hack/test.env +++ b/hack/test.env @@ -61,6 +61,10 @@ GOTRUE_EXTERNAL_SLACK_ENABLED=true GOTRUE_EXTERNAL_SLACK_CLIENT_ID=testclientid GOTRUE_EXTERNAL_SLACK_SECRET=testsecret GOTRUE_EXTERNAL_SLACK_REDIRECT_URI=https://identity.services.netlify.com/callback +GOTRUE_EXTERNAL_WORKOS_ENABLED=true +GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=testclientid +GOTRUE_EXTERNAL_WORKOS_SECRET=testsecret +GOTRUE_EXTERNAL_WORKOS_REDIRECT_URI=https://identity.services.netlify.com/callback GOTRUE_EXTERNAL_TWITCH_ENABLED=true GOTRUE_EXTERNAL_TWITCH_CLIENT_ID=testclientid GOTRUE_EXTERNAL_TWITCH_SECRET=testsecret From 7f93e2f73a4aafa99a3e3b3199d2a750d2124687 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Mon, 15 Nov 2021 15:23:30 +0000 Subject: [PATCH 002/102] Fix trailing spaces in WorkOS name --- api/provider/workos.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 197fe03e5d..04c56a8e5c 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -2,7 +2,6 @@ package provider import ( "context" - "fmt" "strings" "github.com/netlify/gotrue/conf" @@ -92,7 +91,7 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us Email: u.Email, // To be deprecated - FullName: fmt.Sprintf("%s %s", u.FirstName, u.LastName), + FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), ProviderId: u.ID, }, Emails: []Email{{ From 79381df9a4d94788774e1bf2c6774ed1f13d4ac1 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Mon, 17 Jan 2022 13:03:26 +0800 Subject: [PATCH 003/102] fix: correct provider names --- README.md | 1 - api/external.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e6daefabb..cf0980ade4 100644 --- a/README.md +++ b/README.md @@ -515,7 +515,6 @@ Returns the publicly available settings for this gotrue instance. "spotify": true, "twitch": true, "twitter": true, - "tiktok": true, "workos": true, }, "disable_signup": false, diff --git a/api/external.go b/api/external.go index 4b94a519a3..82557c9868 100644 --- a/api/external.go +++ b/api/external.go @@ -417,7 +417,7 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) case "workos": - return provider.NewWorkOSProvider(config.External.Twitter, scopes) + return provider.NewWorkOSProvider(config.External.WorkOS, scopes) case "saml": return provider.NewSamlProvider(config.External.Saml, a.db, getInstanceID(ctx)) case "zoom": From 2e8636449f211b372a5007a0accb9d775ce6fd27 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Tue, 25 Jan 2022 11:15:40 +0800 Subject: [PATCH 004/102] chore: apply latest commits --- api/external_workos_test.go | 4 ++-- api/provider/workos.go | 22 ++-------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/api/external_workos_test.go b/api/external_workos_test.go index 04682b8ec6..d7de03b56d 100644 --- a/api/external_workos_test.go +++ b/api/external_workos_test.go @@ -16,8 +16,8 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { u, err := url.Parse(w.Header().Get("Location")) ts.Require().NoError(err, "redirect url parse failed") q := u.Query() - ts.Equal(ts.Config.External.Spotify.RedirectURI, q.Get("redirect_uri")) - ts.Equal(ts.Config.External.Spotify.ClientID, q.Get("client_id")) + ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.WorkOS.ClientID, q.Get("client_id")) ts.Equal("code", q.Get("response_type")) ts.Equal("", q.Get("scope")) diff --git a/api/provider/workos.go b/api/provider/workos.go index 04c56a8e5c..9279ed5b6c 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -17,19 +17,6 @@ type workosProvider struct { APIPath string } -/* -{ - "id": "prof_01DMC79VCBZ0NY2099737PSVF1", - "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", - "connection_type": "okta", - "email": "todd@foo-corp.com", - "first_name": "Todd", - "idp_id": "00u1a0ufowBJlzPlk357", - "last_name": "Rundgren", - "object": "profile", - "raw_attributes": {...} -} -*/ type workosUser struct { ID string `json:"id"` ConnectionId string `json:"connection_id"` @@ -72,16 +59,11 @@ func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut } func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { - // TODO rework this as the TokenURL returns only an access token and the profile return g.Exchange(oauth2.NoContext, code) } func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { - var u workosUser - // TODO rework this as the only way to get profile is with TokenURL - if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/sso/token", &u); err != nil { - return nil, err - } + u := tok.Extra("profile").(workosUser) return &UserProvidedData{ Metadata: &Claims{ @@ -99,4 +81,4 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us Primary: true, }}, }, nil -} +} \ No newline at end of file From dcd9b6711cdd887405700d21e8921fc1e24108ec Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Tue, 25 Jan 2022 11:17:53 +0800 Subject: [PATCH 005/102] chore: run gofmt --- api/provider/workos.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 9279ed5b6c..86bdcb8593 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -81,4 +81,4 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us Primary: true, }}, }, nil -} \ No newline at end of file +} From 405eebce2716e7ae3242e5b59c34fcb1855c69c2 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 4 Feb 2022 15:49:05 +0800 Subject: [PATCH 006/102] chore: strip https on workos link --- api/provider/workos.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 86bdcb8593..358896db06 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -9,7 +9,7 @@ import ( ) const ( - defaultWorkOSAPIBase = "https://api.workos.com" + defaultWorkOSAPIBase = "api.workos.com" ) type workosProvider struct { @@ -34,7 +34,6 @@ func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut if err := ext.Validate(); err != nil { return nil, err } - apiPath := chooseHost(ext.URL, defaultWorkOSAPIBase) oauthScopes := []string{} From d5fcc82da2f87a86cad22e95dc46f71790aae859 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Fri, 25 Feb 2022 22:19:45 +0800 Subject: [PATCH 007/102] ExternalProviderRedirect: pass query parameters into `Provider` function This allows referencing of query parameters when constructing new provider objects. --- api/external.go | 13 +++++++------ api/external_oauth.go | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/external.go b/api/external.go index 82557c9868..7e5b3c6d6d 100644 --- a/api/external.go +++ b/api/external.go @@ -38,15 +38,16 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e ctx := r.Context() config := a.getConfig(ctx) - providerType := r.URL.Query().Get("provider") - scopes := r.URL.Query().Get("scopes") + query := r.URL.Query() + providerType := query.Get("provider") + scopes := query.Get("scopes") - p, err := a.Provider(ctx, providerType, scopes) + p, err := a.Provider(ctx, providerType, scopes, &query) if err != nil { return badRequestError("Unsupported provider: %+v", err).WithInternalError(err) } - inviteToken := r.URL.Query().Get("invite_token") + inviteToken := query.Get("invite_token") if inviteToken != "" { _, userErr := models.FindUserByConfirmationToken(a.db, inviteToken) if userErr != nil { @@ -57,7 +58,7 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e } } - redirectURL := a.getRedirectURLOrReferrer(r, r.URL.Query().Get("redirect_to")) + redirectURL := a.getRedirectURLOrReferrer(r, query.Get("redirect_to")) log := getLogEntry(r) log.WithField("provider", providerType).Info("Redirecting to external provider") @@ -383,7 +384,7 @@ func (a *API) loadExternalState(ctx context.Context, state string) (context.Cont } // Provider returns a Provider interface for the given name. -func (a *API) Provider(ctx context.Context, name string, scopes string) (provider.Provider, error) { +func (a *API) Provider(ctx context.Context, name string, scopes string, query *url.Values) (provider.Provider, error) { config := a.getConfig(ctx) name = strings.ToLower(name) diff --git a/api/external_oauth.go b/api/external_oauth.go index 3a4899efce..0e7e72a44a 100644 --- a/api/external_oauth.go +++ b/api/external_oauth.go @@ -141,7 +141,7 @@ func (a *API) oAuth1Callback(ctx context.Context, r *http.Request, providerType // OAuthProvider returns the corresponding oauth provider as an OAuthProvider interface func (a *API) OAuthProvider(ctx context.Context, name string) (provider.OAuthProvider, error) { - providerCandidate, err := a.Provider(ctx, name, "") + providerCandidate, err := a.Provider(ctx, name, "", nil) if err != nil { return nil, err } From c15ea634ca9ce9138ed1c40186a8edeb02eaef2f Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 26 Feb 2022 00:13:37 +0800 Subject: [PATCH 008/102] Tweak `example.env` --- example.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example.env b/example.env index b639e2df80..0782f6ec7c 100644 --- a/example.env +++ b/example.env @@ -135,19 +135,19 @@ GOTRUE_EXTERNAL_LINKEDIN_SECRET="" GOTRUE_EXTERNAL_SLACK_ENABLED="false" GOTRUE_EXTERNAL_SLACK_CLIENT_ID="" GOTRUE_EXTERNAL_SLACK_SECRET="" -GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="https://localhost:9999/callback" +GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="http://localhost:9999/callback" # WorkOS OAuth config GOTRUE_EXTERNAL_WORKOS_ENABLED="true" GOTRUE_EXTERNAL_WORKOS_CLIENT_ID="" GOTRUE_EXTERNAL_WORKOS_SECRET="" -GOTRUE_EXTERNAL_SLACK_REDIRECT_URI="https://localhost:9999/callback" +GOTRUE_EXTERNAL_WORKOS_REDIRECT_URI="http://localhost:9999/callback" # Zoom OAuth config GOTRUE_EXTERNAL_ZOOM_ENABLED="false" GOTRUE_EXTERNAL_ZOOM_CLIENT_ID="" GOTRUE_EXTERNAL_ZOOM_SECRET="" -GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI="https://localhost:9999/callback" +GOTRUE_EXTERNAL_ZOOM_REDIRECT_URI="http://localhost:9999/callback" # Phone provider config GOTRUE_SMS_AUTOCONFIRM="false" From 11ffa84a0325c14f81f135edc24c986e8e2540ab Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 26 Feb 2022 00:25:17 +0800 Subject: [PATCH 009/102] WorkOS provider: construct custom authorization URL from request query parameters --- api/external.go | 2 +- api/provider/workos.go | 32 +++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/api/external.go b/api/external.go index 7e5b3c6d6d..9df6b6e360 100644 --- a/api/external.go +++ b/api/external.go @@ -418,7 +418,7 @@ func (a *API) Provider(ctx context.Context, name string, scopes string, query *u case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) case "workos": - return provider.NewWorkOSProvider(config.External.WorkOS, scopes) + return provider.NewWorkOSProvider(config.External.WorkOS, query) case "saml": return provider.NewSamlProvider(config.External.Saml, a.db, getInstanceID(ctx)) case "zoom": diff --git a/api/provider/workos.go b/api/provider/workos.go index 358896db06..bb1212634d 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -2,6 +2,7 @@ package provider import ( "context" + "net/url" "strings" "github.com/netlify/gotrue/conf" @@ -14,7 +15,8 @@ const ( type workosProvider struct { *oauth2.Config - APIPath string + APIPath string + AuthCodeOptions []oauth2.AuthCodeOption } type workosUser struct { @@ -30,16 +32,27 @@ type workosUser struct { } // NewWorkOSProvider creates a WorkOS account provider. -func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { +func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, query *url.Values) (OAuthProvider, error) { if err := ext.Validate(); err != nil { return nil, err } apiPath := chooseHost(ext.URL, defaultWorkOSAPIBase) - oauthScopes := []string{} + // Attach custom query parameters to the WorkOS authorization URL. + // See https://workos.com/docs/reference/sso/authorize/get. + authCodeOptions := make([]oauth2.AuthCodeOption, 0) + if query != nil { + if connection := query.Get("connection"); connection != "" { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("connection", connection)) + } else if organization := query.Get("organization"); organization != "" { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("organization", organization)) + } else if provider := query.Get("provider"); provider != "" { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("provider", provider)) + } - if scopes != "" { - oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + if login_hint := query.Get("login_hint"); login_hint != "" { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("login_hint", login_hint)) + } } return &workosProvider{ @@ -50,13 +63,18 @@ func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut AuthURL: apiPath + "/sso/authorize", TokenURL: apiPath + "/sso/token", }, - Scopes: oauthScopes, RedirectURL: ext.RedirectURI, }, - APIPath: apiPath, + APIPath: apiPath, + AuthCodeOptions: authCodeOptions, }, nil } +func (g workosProvider) AuthCodeURL(state string, args ...oauth2.AuthCodeOption) string { + opts := append(args, g.AuthCodeOptions...) + return g.Config.AuthCodeURL(state, opts...) +} + func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { return g.Exchange(oauth2.NoContext, code) } From 81dabf59fa47fb6c511efd8c066862215620c8e2 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sat, 26 Feb 2022 01:10:24 +0800 Subject: [PATCH 010/102] Go modules: add `github.com/mitchellh/mapstructure` This module can be used to easily convert interfaces to structs without going through the JSON serialization/deserialization dance. --- go.mod | 1 + go.sum | 1 + 2 files changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 1563512c6a..e8130727e6 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/lestrrat-go/jwx v0.9.0 github.com/lib/pq v1.9.0 // indirect github.com/microcosm-cc/bluemonday v1.0.16 // indirect + github.com/mitchellh/mapstructure v1.1.2 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 github.com/netlify/mailme v1.1.1 github.com/opentracing/opentracing-go v1.1.0 diff --git a/go.sum b/go.sum index 6ca10922bd..2fd375e267 100644 --- a/go.sum +++ b/go.sum @@ -420,6 +420,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= From 760b2460e3a443c66b33ca595db4009f76b8f159 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 27 Feb 2022 13:28:43 +0800 Subject: [PATCH 011/102] WorkOS provider: obtain user data from response from authorization endpoint --- api/provider/workos.go | 48 ++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index bb1212634d..669959b767 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -5,6 +5,7 @@ import ( "net/url" "strings" + "github.com/mitchellh/mapstructure" "github.com/netlify/gotrue/conf" "golang.org/x/oauth2" ) @@ -19,16 +20,18 @@ type workosProvider struct { AuthCodeOptions []oauth2.AuthCodeOption } +// See https://workos.com/docs/reference/sso/profile. type workosUser struct { - ID string `json:"id"` - ConnectionId string `json:"connection_id"` - ConnectionType string `json:"connection_type"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Object string `json:"object"` - IdpId string `json:"idp_id"` - RawAttributes map[string]interface{} `json:"raw_attributes"` + ID string `mapstructure:"id"` + ConnectionID string `mapstructure:"connection_id"` + OrganizationID string `mapstructure:"organization_id"` + ConnectionType string `mapstructure:"connection_type"` + Email string `mapstructure:"email"` + FirstName string `mapstructure:"first_name"` + LastName string `mapstructure:"last_name"` + Object string `mapstructure:"object"` + IdpID string `mapstructure:"idp_id"` + RawAttributes map[string]interface{} `mapstructure:"raw_attributes"` } // NewWorkOSProvider creates a WorkOS account provider. @@ -80,22 +83,35 @@ func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { } func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { - u := tok.Extra("profile").(workosUser) + if tok.AccessToken == "" { + return &UserProvidedData{}, nil + } + + // WorkOS API returns the user's profile data along with the OAuth2 token, so + // we can just convert from `map[string]interface{}` to `workosUser` without + // an additional network request. + var u workosUser + err := mapstructure.Decode(tok.Extra("profile"), &u) + if err != nil { + return &UserProvidedData{}, err + } return &UserProvidedData{ Metadata: &Claims{ - Issuer: g.APIPath, - Subject: u.ID, - Name: u.FirstName, - Email: u.Email, + Issuer: g.APIPath, + Subject: u.ID, + Name: strings.TrimSpace(u.FirstName + " " + u.LastName), + Email: u.Email, + EmailVerified: true, // To be deprecated FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), ProviderId: u.ID, }, Emails: []Email{{ - Email: u.Email, - Primary: true, + Email: u.Email, + Verified: true, + Primary: true, }}, }, nil } From 35e518346e18996e123e01b22a5750c330ca336f Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 27 Feb 2022 13:29:04 +0800 Subject: [PATCH 012/102] WorkOS provider: return error when returned profile has no email --- api/provider/workos.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 669959b767..369d818b62 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -2,6 +2,7 @@ package provider import ( "context" + "errors" "net/url" "strings" @@ -93,7 +94,11 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us var u workosUser err := mapstructure.Decode(tok.Extra("profile"), &u) if err != nil { - return &UserProvidedData{}, err + return nil, err + } + + if u.Email == "" { + return nil, errors.New("Unable to find email with WorkOS provider") } return &UserProvidedData{ From 114d87833a4b07d2022f11e5b6a41aab5aa98289 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 27 Feb 2022 02:20:58 +0800 Subject: [PATCH 013/102] WorkOS provider: rename `provider` query parameter The `provider` query provider is already passed to the `/authorize` route, so an alternative query parameter (`workos_provider`) is chosen instead. --- api/provider/workos.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 369d818b62..0241407a85 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -44,13 +44,13 @@ func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, query *url.Values) ( // Attach custom query parameters to the WorkOS authorization URL. // See https://workos.com/docs/reference/sso/authorize/get. - authCodeOptions := make([]oauth2.AuthCodeOption, 0) + var authCodeOptions []oauth2.AuthCodeOption if query != nil { if connection := query.Get("connection"); connection != "" { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("connection", connection)) } else if organization := query.Get("organization"); organization != "" { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("organization", organization)) - } else if provider := query.Get("provider"); provider != "" { + } else if provider := query.Get("workos_provider"); provider != "" { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("provider", provider)) } From 34068bc0c743caa4ebe468271dbf53d4d04d1d06 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 27 Feb 2022 02:11:00 +0800 Subject: [PATCH 014/102] WorkOS provider: add more comprehensive tests --- api/external_workos_test.go | 192 +++++++++++++++++++++++++++++++++++- 1 file changed, 190 insertions(+), 2 deletions(-) diff --git a/api/external_workos_test.go b/api/external_workos_test.go index d7de03b56d..b2e1e599e0 100644 --- a/api/external_workos_test.go +++ b/api/external_workos_test.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -8,8 +9,15 @@ import ( jwt "github.com/golang-jwt/jwt" ) -func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { - req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=workos", nil) +const ( + workosUser string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","email":"workos@example.com","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}` + workosUserWrongEmail string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","email":"other@example.com","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}` + workosUserNoEmail string = `{"id":"test_prof_workos","first_name":"John","last_name":"Doe","connection_id":"test_conn_1","organization_id":"test_org_1","connection_type":"test","idp_id":"test_idp_1","object": "profile","raw_attributes": {}}` +) + +func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithConnection() { + connection := "test_connection_id" + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&connection=%s", connection), nil) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) ts.Require().Equal(http.StatusFound, w.Code) @@ -20,6 +28,7 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { ts.Equal(ts.Config.External.WorkOS.ClientID, q.Get("client_id")) ts.Equal("code", q.Get("response_type")) ts.Equal("", q.Get("scope")) + ts.Equal(connection, q.Get("connection")) claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} @@ -31,3 +40,182 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { ts.Equal("workos", claims.Provider) ts.Equal(ts.Config.SiteURL, claims.SiteURL) } + +func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithOrganization() { + organization := "test_organization_id" + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&organization=%s", organization), nil) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + ts.Require().Equal(http.StatusFound, w.Code) + u, err := url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + q := u.Query() + ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.WorkOS.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + ts.Equal("", q.Get("scope")) + ts.Equal(organization, q.Get("organization")) + + claims := ExternalProviderClaims{} + p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(ts.Config.JWT.Secret), nil + }) + ts.Require().NoError(err) + + ts.Equal("workos", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} + +func (ts *ExternalTestSuite) TestSignupExternalWorkOSWithProvider() { + provider := "test_provider" + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost/authorize?provider=workos&workos_provider=%s", provider), nil) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + ts.Require().Equal(http.StatusFound, w.Code) + u, err := url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + q := u.Query() + ts.Equal(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.WorkOS.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + ts.Equal("", q.Get("scope")) + ts.Equal(provider, q.Get("provider")) + + claims := ExternalProviderClaims{} + p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(ts.Config.JWT.Secret), nil + }) + ts.Require().NoError(err) + + ts.Equal("workos", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} + +func WorkosTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/sso/token": + // WorkOS returns the user data along with the token. + *tokenCount++ + *userCount++ + ts.Equal(code, r.FormValue("code")) + ts.Equal("authorization_code", r.FormValue("grant_type")) + ts.Equal(ts.Config.External.WorkOS.RedirectURI, r.FormValue("redirect_uri")) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, `{"access_token":"workos_token","expires_in":100000,"profile":%s}`, user) + default: + fmt.Printf("%s", r.URL.Path) + w.WriteHeader(500) + ts.Fail("unknown workos oauth call %s", r.URL.Path) + } + })) + + ts.Config.External.WorkOS.URL = server.URL + + return server +} + +func (ts *ExternalTestSuite) TestSignupExternalWorkosAuthorizationCode() { + ts.Config.DisableSignup = false + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "") +} + +func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenNoUser() { + ts.Config.DisableSignup = true + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "") + + assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "workos@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenEmptyEmail() { + ts.Config.DisableSignup = true + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUserNoEmail) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "") + + assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "workos@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupSuccessWithPrimaryEmail() { + ts.Config.DisableSignup = true + + ts.createUser("test_prof_workos", "workos@example.com", "John Doe", "http://example.com/avatar", "") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosSuccessWhenMatchingToken() { + ts.createUser("test_prof_workos", "workos@example.com", "", "http://example.com/avatar", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "invite_token") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "workos@example.com", "John Doe", "test_prof_workos", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenNoMatchingToken() { + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + w := performAuthorizationRequest(ts, "workos", "invite_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenWrongToken() { + ts.createUser("test_prof_workos", "workos@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUser) + defer server.Close() + + w := performAuthorizationRequest(ts, "workos", "wrong_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalWorkosErrorWhenEmailDoesntMatch() { + ts.createUser("test_prof_workos", "workos@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := WorkosTestSignupSetup(ts, &tokenCount, &userCount, code, workosUserWrongEmail) + defer server.Close() + + u := performAuthorization(ts, "workos", code, "invite_token") + + assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") +} From 20c18c34ea47ff1c0ac6a6027c61b1fcaaee7435 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Sun, 27 Feb 2022 04:09:34 +0800 Subject: [PATCH 015/102] Tests: tweak `assertAuthorizationSuccess` to accept empty strings for `avatar` Certain providers (eg. WorkOS, Azure) do not return an avatar URL in their user profile data. Hence, it is possible for `user.UserMetaData["avatar_url"]` to be `nil`. However, the `assertAuthorizationSuccess` function only accepts a `string` to compare against the stored avatar URL in a test, causing certain tests to fail when a This PR tweaks the test for avatar URL to check for `nil` if the supplied expected input is `""` (the empty string). Various tests are also modified for the Azure provider, which previously had workarounds including already creating a user before the signup occurred, which meant the initial login flow wasn't thoroughly tested. The alternative would be to change the type of `avatar` to `*string`, which is a lot more troublesome to write for in the common case of having an avatar URL, since you can't use the `&` referencing syntax for string constants. --- api/external_azure_test.go | 11 +++++------ api/external_test.go | 6 +++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/external_azure_test.go b/api/external_azure_test.go index b0599c45fe..4e8c874bf5 100644 --- a/api/external_azure_test.go +++ b/api/external_azure_test.go @@ -67,7 +67,6 @@ func AzureTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int func (ts *ExternalTestSuite) TestSignupExternalAzure_AuthorizationCode() { ts.Config.DisableSignup = false - ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "") tokenCount, userCount := 0, 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) @@ -106,7 +105,7 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmai func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "") + ts.createUser("azuretestid", "azure@example.com", "Azure Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" @@ -115,12 +114,12 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrim u := performAuthorization(ts, "azure", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToken() { - // name and avatar should be populated from Azure API - ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") + // name should be populated from Azure API + ts.createUser("azuretestid", "azure@example.com", "", "http://example.com/avatar", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" @@ -129,7 +128,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToke u := performAuthorization(ts, "azure", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToken() { diff --git a/api/external_test.go b/api/external_test.go index 9aee2c30c4..d6260e24c4 100644 --- a/api/external_test.go +++ b/api/external_test.go @@ -120,7 +120,11 @@ func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount in ts.Require().NoError(err) ts.Equal(providerId, user.UserMetaData["provider_id"]) ts.Equal(name, user.UserMetaData["full_name"]) - ts.Equal(avatar, user.UserMetaData["avatar_url"]) + if avatar == "" { + ts.Equal(nil, user.UserMetaData["avatar_url"]) + } else { + ts.Equal(avatar, user.UserMetaData["avatar_url"]) + } } func assertAuthorizationFailure(ts *ExternalTestSuite, u *url.URL, errorDescription string, errorType string, email string) { From 1cddd51316b52b9135348b894896782330b874a5 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Mon, 28 Feb 2022 23:32:58 +0800 Subject: [PATCH 016/102] WorkOS provider: do not mark emails as verified --- api/provider/workos.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 0241407a85..1c6f460d43 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -103,20 +103,18 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return &UserProvidedData{ Metadata: &Claims{ - Issuer: g.APIPath, - Subject: u.ID, - Name: strings.TrimSpace(u.FirstName + " " + u.LastName), - Email: u.Email, - EmailVerified: true, + Issuer: g.APIPath, + Subject: u.ID, + Name: strings.TrimSpace(u.FirstName + " " + u.LastName), + Email: u.Email, // To be deprecated FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), ProviderId: u.ID, }, Emails: []Email{{ - Email: u.Email, - Verified: true, - Primary: true, + Email: u.Email, + Primary: true, }}, }, nil } From 638b5d02f64fcad64f5fbeace50f3c0ee909e143 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Mon, 28 Feb 2022 23:45:42 +0800 Subject: [PATCH 017/102] WorkOS provider: fix tests by enabling autoconfirm --- api/external_workos_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/external_workos_test.go b/api/external_workos_test.go index b2e1e599e0..0f217c60ae 100644 --- a/api/external_workos_test.go +++ b/api/external_workos_test.go @@ -120,6 +120,8 @@ func WorkosTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *in func (ts *ExternalTestSuite) TestSignupExternalWorkosAuthorizationCode() { ts.Config.DisableSignup = false + // Enable autoconfirm since emails from WorkOS are not verified. + ts.Config.Mailer.Autoconfirm = true tokenCount, userCount := 0, 0 code := "authcode" @@ -159,6 +161,8 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true + // Enable autoconfirm since emails from WorkOS are not verified. + ts.Config.Mailer.Autoconfirm = true ts.createUser("test_prof_workos", "workos@example.com", "John Doe", "http://example.com/avatar", "") From bcb1d5a745b2493c807f28186da5a58df33445f2 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Wed, 2 Mar 2022 14:16:29 +0800 Subject: [PATCH 018/102] Revert "WorkOS provider: fix tests by enabling autoconfirm" This reverts commit 638b5d02f64fcad64f5fbeace50f3c0ee909e143. --- api/external_workos_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/external_workos_test.go b/api/external_workos_test.go index 0f217c60ae..b2e1e599e0 100644 --- a/api/external_workos_test.go +++ b/api/external_workos_test.go @@ -120,8 +120,6 @@ func WorkosTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *in func (ts *ExternalTestSuite) TestSignupExternalWorkosAuthorizationCode() { ts.Config.DisableSignup = false - // Enable autoconfirm since emails from WorkOS are not verified. - ts.Config.Mailer.Autoconfirm = true tokenCount, userCount := 0, 0 code := "authcode" @@ -161,8 +159,6 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalWorkosDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - // Enable autoconfirm since emails from WorkOS are not verified. - ts.Config.Mailer.Autoconfirm = true ts.createUser("test_prof_workos", "workos@example.com", "John Doe", "http://example.com/avatar", "") From 6807ae5542377e75533b2197aba5a4d115204c4e Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Wed, 2 Mar 2022 14:17:11 +0800 Subject: [PATCH 019/102] Revert "WorkOS provider: do not mark emails as verified" This reverts commit 1cddd51316b52b9135348b894896782330b874a5. --- api/provider/workos.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 1c6f460d43..0241407a85 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -103,18 +103,20 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return &UserProvidedData{ Metadata: &Claims{ - Issuer: g.APIPath, - Subject: u.ID, - Name: strings.TrimSpace(u.FirstName + " " + u.LastName), - Email: u.Email, + Issuer: g.APIPath, + Subject: u.ID, + Name: strings.TrimSpace(u.FirstName + " " + u.LastName), + Email: u.Email, + EmailVerified: true, // To be deprecated FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), ProviderId: u.ID, }, Emails: []Email{{ - Email: u.Email, - Primary: true, + Email: u.Email, + Verified: true, + Primary: true, }}, }, nil } From fa4b7e6435d4e88b19575a5e8b681fd6e6ef1794 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 4 Mar 2022 11:48:55 +0800 Subject: [PATCH 020/102] fix: azure api_url config (#407) --- conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/configuration.go b/conf/configuration.go index 9b00b4c7be..f030ac371f 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -19,7 +19,7 @@ type OAuthProviderConfiguration struct { Secret string `json:"secret"` RedirectURI string `json:"redirect_uri" split_words:"true"` URL string `json:"url"` - ApiURL string `json:"api_url"` + ApiURL string `json:"api_url" split_words:"true"` Enabled bool `json:"enabled"` } From 6fce18548655f33c8f6150bd4837be1ca9ec1755 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Fri, 4 Mar 2022 14:51:26 +0800 Subject: [PATCH 021/102] WorkOS provider: add `connection_id` and `organization_id` custom claims --- api/provider/workos.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/provider/workos.go b/api/provider/workos.go index 0241407a85..91ff8dd332 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -108,6 +108,10 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us Name: strings.TrimSpace(u.FirstName + " " + u.LastName), Email: u.Email, EmailVerified: true, + CustomClaims: map[string]interface{}{ + "connection_id": u.ConnectionID, + "organization_id": u.OrganizationID, + }, // To be deprecated FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), From 3e92e8d6f196101b50a8afe01c6c71be960ed1bb Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 7 Mar 2022 09:21:21 +0800 Subject: [PATCH 022/102] fix: allow max db pool size to be configurable (#409) --- README.md | 4 ++++ conf/configuration.go | 7 +++++-- storage/dial.go | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9343f03134..31fc6bb571 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,10 @@ Chooses what dialect of database you want. Must be `mysql`. Connection string for the database. +`GOTRUE_DB_MAX_POOL_SIZE` - `int` + +Sets the maximum number of open connections to the database. Defaults to 0 which is equivalent to an "unlimited" number of connections. + `DB_NAMESPACE` - `string` Adds a prefix to all table names. diff --git a/conf/configuration.go b/conf/configuration.go index 77f210ce9f..dbea9bebe4 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -38,8 +38,11 @@ type SamlProviderConfiguration struct { // DBConfiguration holds all the database related configuration. type DBConfiguration struct { - Driver string `json:"driver" required:"true"` - URL string `json:"url" envconfig:"DATABASE_URL" required:"true"` + Driver string `json:"driver" required:"true"` + URL string `json:"url" envconfig:"DATABASE_URL" required:"true"` + + // MaxPoolSize defaults to 0 (unlimited). + MaxPoolSize int `json:"max_pool_size" split_words:"true"` MigrationsPath string `json:"migrations_path" split_words:"true" default:"./migrations"` } diff --git a/storage/dial.go b/storage/dial.go index e176851eaa..5761981941 100644 --- a/storage/dial.go +++ b/storage/dial.go @@ -31,6 +31,7 @@ func Dial(config *conf.GlobalConfiguration) (*Connection, error) { db, err := pop.NewConnection(&pop.ConnectionDetails{ Dialect: config.DB.Driver, URL: config.DB.URL, + Pool: config.DB.MaxPoolSize, }) if err != nil { return nil, errors.Wrap(err, "opening database connection") From 32b0802b9da30639fca1611738216c258203b71d Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 8 Mar 2022 09:45:41 +0800 Subject: [PATCH 023/102] fix: update user email should not fail when current email doesn't exist (#408) * fix: check if current email exists before sending * refactor: create MailClient interface to mock mail method * refactor: merge sendEmailChange & sendSecureEmailChange methods * fix: allow single confirmation if current email doesn't exist * test: add update user email tests * add test for UserGet --- api/mail.go | 28 ++++----------- api/user.go | 10 ++---- api/user_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++- api/verify.go | 2 +- mailer/mailer.go | 23 ++++++------ mailer/noop.go | 16 ++++++++- mailer/template.go | 12 ++++--- 7 files changed, 133 insertions(+), 47 deletions(-) diff --git a/api/mail.go b/api/mail.go index edf8b0f675..5d9ea1401d 100644 --- a/api/mail.go +++ b/api/mail.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/mailer" "github.com/netlify/gotrue/models" @@ -229,30 +230,12 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery") } -// sendSecureEmailChange sends out an email change token each to the old and new emails. -func (a *API) sendSecureEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error { - u.EmailChangeTokenCurrent, u.EmailChangeTokenNew = crypto.SecureToken(), crypto.SecureToken() - u.EmailChange = email - u.EmailChangeConfirmStatus = zeroConfirmation - now := time.Now() - if err := mailer.EmailChangeMail(u, referrerURL); err != nil { - return err - } - - u.EmailChangeSentAt = &now - return errors.Wrap(tx.UpdateOnly( - u, - "email_change_token_current", - "email_change_token_new", - "email_change", - "email_change_sent_at", - "email_change_confirm_status", - ), "Database error updating user for email change") -} - // sendEmailChange sends out an email change token to the new email. -func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error { +func (a *API) sendEmailChange(tx *storage.Connection, config *conf.Configuration, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error { u.EmailChangeTokenNew = crypto.SecureToken() + if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" { + u.EmailChangeTokenCurrent = crypto.SecureToken() + } u.EmailChange = email u.EmailChangeConfirmStatus = zeroConfirmation now := time.Now() @@ -263,6 +246,7 @@ func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mai u.EmailChangeSentAt = &now return errors.Wrap(tx.UpdateOnly( u, + "email_change_token_current", "email_change_token_new", "email_change", "email_change_sent_at", diff --git a/api/user.go b/api/user.go index 4ef51525a3..56296378e5 100644 --- a/api/user.go +++ b/api/user.go @@ -119,14 +119,8 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if config.Mailer.SecureEmailChangeEnabled { - if terr = a.sendSecureEmailChange(tx, user, mailer, params.Email, referrer); terr != nil { - return internalServerError("Error sending change email").WithInternalError(terr) - } - } else { - if terr = a.sendEmailChange(tx, user, mailer, params.Email, referrer); terr != nil { - return internalServerError("Error sending change email").WithInternalError(terr) - } + if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer); terr != nil { + return internalServerError("Error sending change email").WithInternalError(terr) } } diff --git a/api/user_test.go b/api/user_test.go index baae1f35ba..8d8566b1e7 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -47,7 +47,94 @@ func (ts *UserTestSuite) SetupTest() { require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } -func (ts *UserTestSuite) TestUser_UpdatePassword() { +func (ts *UserTestSuite) TestUserGet() { + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err, "Error finding user") + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err, "Error generating access token") + + req := httptest.NewRequest(http.MethodGet, "http://localhost/user", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) +} + +func (ts *UserTestSuite) TestUserUpdateEmail() { + cases := []struct { + desc string + userData map[string]string + isSecureEmailChangeEnabled bool + expectedCode int + }{ + { + "User doesn't have an existing email", + map[string]string{ + "email": "", + "phone": "123456789", + }, + false, + http.StatusOK, + }, + { + "User doesn't have an existing email and double email confirmation required", + map[string]string{ + "email": "", + "phone": "234567890", + }, + true, + http.StatusOK, + }, + { + "User has an existing email", + map[string]string{ + "email": "foo@example.com", + "phone": "", + }, + false, + http.StatusOK, + }, + { + "User has an existing email and double email confirmation required", + map[string]string{ + "email": "bar@example.com", + "phone": "", + }, + true, + http.StatusOK, + }, + } + + for _, c := range cases { + ts.Run(c.desc, func() { + u, err := models.NewUser(ts.instanceID, "", "", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user") + require.NoError(ts.T(), u.SetEmail(ts.API.db, c.userData["email"]), "Error setting user email") + require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"]), "Error setting user phone") + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err, "Error generating access token") + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": "new@example.com", + })) + req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.Config.Mailer.SecureEmailChangeEnabled = c.isSecureEmailChangeEnabled + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), c.expectedCode, w.Code) + }) + } + +} + +func (ts *UserTestSuite) TestUserUpdatePassword() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) diff --git a/api/verify.go b/api/verify.go index c6fa4cc406..06bfe05a2a 100644 --- a/api/verify.go +++ b/api/verify.go @@ -280,7 +280,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p instanceID := getInstanceID(ctx) config := a.getConfig(ctx) - if config.Mailer.SecureEmailChangeEnabled && user.EmailChangeConfirmStatus == zeroConfirmation { + if config.Mailer.SecureEmailChangeEnabled && user.EmailChangeConfirmStatus == zeroConfirmation && user.GetEmail() != "" { err := a.db.Transaction(func(tx *storage.Connection) error { user.EmailChangeConfirmStatus = singleConfirmation if params.Token == user.EmailChangeTokenCurrent { diff --git a/mailer/mailer.go b/mailer/mailer.go index 496572760d..28fbea68ee 100644 --- a/mailer/mailer.go +++ b/mailer/mailer.go @@ -25,18 +25,15 @@ type Mailer interface { // NewMailer returns a new gotrue mailer func NewMailer(instanceConfig *conf.Configuration) Mailer { - if instanceConfig.SMTP.Host == "" { - logrus.Infof("Noop mailer being used for %v", instanceConfig.SiteURL) - return &noopMailer{} - } - mail := gomail.NewMessage() from := mail.FormatAddress(instanceConfig.SMTP.AdminEmail, instanceConfig.SMTP.SenderName) - return &TemplateMailer{ - SiteURL: instanceConfig.SiteURL, - Config: instanceConfig, - Mailer: &mailme.Mailer{ + var mailClient MailClient + if instanceConfig.SMTP.Host == "" { + logrus.Infof("Noop mail client being used for %v", instanceConfig.SiteURL) + mailClient = &noopMailClient{} + } else { + mailClient = &mailme.Mailer{ Host: instanceConfig.SMTP.Host, Port: instanceConfig.SMTP.Port, User: instanceConfig.SMTP.User, @@ -44,7 +41,13 @@ func NewMailer(instanceConfig *conf.Configuration) Mailer { From: from, BaseURL: instanceConfig.SiteURL, Logger: logrus.New(), - }, + } + } + + return &TemplateMailer{ + SiteURL: instanceConfig.SiteURL, + Config: instanceConfig, + Mailer: mailClient, } } diff --git a/mailer/noop.go b/mailer/noop.go index 9c486fc4e6..b35a7af1ef 100644 --- a/mailer/noop.go +++ b/mailer/noop.go @@ -1,8 +1,13 @@ package mailer -import "github.com/netlify/gotrue/models" +import ( + "errors" + + "github.com/netlify/gotrue/models" +) type noopMailer struct { + Mailer MailClient } func (m noopMailer) ValidateEmail(email string) error { @@ -36,3 +41,12 @@ func (m noopMailer) Send(user *models.User, subject, body string, data map[strin func (m noopMailer) GetEmailActionLink(user *models.User, actionType, referrerURL string) (string, error) { return "", nil } + +type noopMailClient struct{} + +func (m *noopMailClient) Mail(to, subjectTemplate, templateURL, defaultTemplate string, templateData map[string]interface{}) error { + if to == "" { + return errors.New("to field cannot be empty") + } + return nil +} diff --git a/mailer/template.go b/mailer/template.go index 634eb4f8a8..bda1ab5b3a 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -6,14 +6,17 @@ import ( "github.com/badoux/checkmail" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" - "github.com/netlify/mailme" ) +type MailClient interface { + Mail(string, string, string, string, map[string]interface{}) error +} + // TemplateMailer will send mail and use templates from the site for easy mail styling type TemplateMailer struct { SiteURL string Config *conf.Configuration - Mailer *mailme.Mailer + Mailer MailClient } var configFile = "" @@ -132,9 +135,10 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) }, } - if m.Config.Mailer.SecureEmailChangeEnabled { + currentEmail := user.GetEmail() + if m.Config.Mailer.SecureEmailChangeEnabled && currentEmail != "" { emails = append(emails, Email{ - Address: user.GetEmail(), + Address: currentEmail, Token: user.EmailChangeTokenCurrent, Subject: string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")), Template: m.Config.Mailer.Templates.EmailChange, From 6de5ec1e65f9a904207d4ae054db4d677da4158b Mon Sep 17 00:00:00 2001 From: Frank Spijkerman Date: Tue, 8 Mar 2022 06:18:46 -0800 Subject: [PATCH 024/102] fix: Keycloak OAuth Provider (#371) * Keycloak OAuth Provider * Update README.md * Removed the usage of chooseHost to keep things clear * Allow to use the Keyloak provider in an ID Token grant flow * fix tests Co-authored-by: Kang Ming --- README.md | 8 +- api/external.go | 2 + api/external_keycloak_test.go | 182 ++++++++++++++++++++++++++++++++++ api/provider/keycloak.go | 98 ++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + api/token.go | 4 + conf/configuration.go | 1 + example.env | 7 ++ hack/test.env | 5 + 10 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 api/external_keycloak_test.go create mode 100644 api/provider/keycloak.go diff --git a/README.md b/README.md index 31fc6bb571..0c924249b6 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ The default group to assign all new users to. ### External Authentication Providers -We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `linkedin`, `notion`, `spotify`, `slack`, `twitch`, `twitter` and `workos` for external authentication. +We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `spotify`, `slack`, `twitch`, `twitter` and `workos` for external authentication. Use the names as the keys underneath `external` to configure each separately. @@ -228,7 +228,7 @@ The URI a OAuth2 provider will redirect to with the `code` and `state` values. `EXTERNAL_X_URL` - `string` -The base URL used for constructing the URLs to request authorization and access tokens. Used by `gitlab` only. Defaults to `https://gitlab.com`. +The base URL used for constructing the URLs to request authorization and access tokens. Used by `gitlab` and `keycloak`. For `gitlab` it defaults to `https://gitlab.com`. For `keycloak` you need to set this to your instance, for example: `https://keycloak.example.com/auth/realms/myrealm` #### Apple OAuth @@ -513,6 +513,7 @@ Returns the publicly available settings for this gotrue instance. "github": true, "gitlab": true, "google": true, + "keycloak": true, "linkedin": true, "notion": true, "slack": true, @@ -927,7 +928,8 @@ Get access_token from external oauth provider query params: ``` -provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | linkedin | notion | slack | spotify | twitch | twitter | workos +provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | keycloak | linkedin | notion | slack | spotify | twitch | twitter | workos + scopes= ``` diff --git a/api/external.go b/api/external.go index 9df6b6e360..b2f2d35a00 100644 --- a/api/external.go +++ b/api/external.go @@ -403,6 +403,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string, query *u return provider.NewGitlabProvider(config.External.Gitlab, scopes) case "google": return provider.NewGoogleProvider(config.External.Google, scopes) + case "keycloak": + return provider.NewKeycloakProvider(config.External.Keycloak, scopes) case "linkedin": return provider.NewLinkedinProvider(config.External.Linkedin, scopes) case "facebook": diff --git a/api/external_keycloak_test.go b/api/external_keycloak_test.go new file mode 100644 index 0000000000..81072049f8 --- /dev/null +++ b/api/external_keycloak_test.go @@ -0,0 +1,182 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + + jwt "github.com/golang-jwt/jwt" +) + +const ( + keycloakUser string = `{"sub": "keycloaktestid", "name": "Keycloak Test", "email": "keycloak@example.com", "preferred_username": "keycloak", "email_verified": true}` + keycloakUserNoEmail string = `{"sub": "keycloaktestid", "name": "Keycloak Test", "preferred_username": "keycloak", "email_verified": false}` +) + +func (ts *ExternalTestSuite) TestSignupExternalKeycloak() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=keycloak", nil) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + ts.Require().Equal(http.StatusFound, w.Code) + u, err := url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + q := u.Query() + ts.Equal(ts.Config.External.Keycloak.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.Keycloak.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + ts.Equal("profile email", q.Get("scope")) + + claims := ExternalProviderClaims{} + p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(ts.Config.JWT.Secret), nil + }) + ts.Require().NoError(err) + + ts.Equal("keycloak", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} + +func KeycloakTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/protocol/openid-connect/token": + *tokenCount++ + ts.Equal(code, r.FormValue("code")) + ts.Equal("authorization_code", r.FormValue("grant_type")) + ts.Equal(ts.Config.External.Keycloak.RedirectURI, r.FormValue("redirect_uri")) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, `{"access_token":"keycloak_token","expires_in":100000}`) + case "/protocol/openid-connect/userinfo": + *userCount++ + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, user) + default: + w.WriteHeader(500) + ts.Fail("unknown keycloak oauth call %s", r.URL.Path) + } + })) + + ts.Config.External.Keycloak.URL = server.URL + + return server +} + +func (ts *ExternalTestSuite) TestSignupExternalKeycloakWithoutURLSetup() { + ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "", "") + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + ts.Config.External.Keycloak.URL = "" + defer server.Close() + + w := performAuthorizationRequest(ts, "keycloak", code) + ts.Equal(w.Code, http.StatusBadRequest) +} + +func (ts *ExternalTestSuite) TestSignupExternalKeycloak_AuthorizationCode() { + ts.Config.DisableSignup = false + ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "http://example.com/avatar", "") + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupErrorWhenNoUser() { + ts.Config.DisableSignup = true + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "") + + assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "keycloak@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupErrorWhenNoEmail() { + ts.Config.DisableSignup = true + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUserNoEmail) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "") + + assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "keycloak@example.com") + +} + +func (ts *ExternalTestSuite) TestSignupExternalKeycloakDisableSignupSuccessWithPrimaryEmail() { + ts.Config.DisableSignup = true + + ts.createUser("keycloaktestid", "keycloak@example.com", "Keycloak Test", "http://example.com/avatar", "") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakSuccessWhenMatchingToken() { + // name and avatar should be populated from Keycloak API + ts.createUser("keycloaktestid", "keycloak@example.com", "", "http://example.com/avatar", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "invite_token") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "keycloak@example.com", "Keycloak Test", "keycloaktestid", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenNoMatchingToken() { + tokenCount, userCount := 0, 0 + code := "authcode" + keycloakUser := `{"name":"Keycloak Test","avatar":{"href":"http://example.com/avatar"}}` + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + w := performAuthorizationRequest(ts, "keycloak", "invite_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenWrongToken() { + ts.createUser("keycloaktestid", "keycloak@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + keycloakUser := `{"name":"Keycloak Test","avatar":{"href":"http://example.com/avatar"}}` + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + w := performAuthorizationRequest(ts, "keycloak", "wrong_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKeycloakErrorWhenEmailDoesntMatch() { + ts.createUser("keycloaktestid", "keycloak@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + keycloakUser := `{"name":"Keycloak Test", "email":"other@example.com", "avatar":{"href":"http://example.com/avatar"}}` + server := KeycloakTestSignupSetup(ts, &tokenCount, &userCount, code, keycloakUser) + defer server.Close() + + u := performAuthorization(ts, "keycloak", code, "invite_token") + + assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") +} diff --git a/api/provider/keycloak.go b/api/provider/keycloak.go new file mode 100644 index 0000000000..997fc73c99 --- /dev/null +++ b/api/provider/keycloak.go @@ -0,0 +1,98 @@ +package provider + +import ( + "context" + "errors" + "strings" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +// Keycloak +type keycloakProvider struct { + *oauth2.Config + Host string +} + +type keycloakUser struct { + Name string `json:"name"` + Sub string `json:"sub"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` +} + +// NewKeycloakProvider creates a Keycloak account provider. +func NewKeycloakProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.Validate(); err != nil { + return nil, err + } + + oauthScopes := []string{ + "profile", + "email", + } + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + if ext.URL == "" { + return nil, errors.New("Unable to find URL for the Keycloak provider") + } + + extURLlen := len(ext.URL) + if ext.URL[extURLlen-1] == '/' { + ext.URL = ext.URL[:extURLlen-1] + } + + return &keycloakProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID, + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: ext.URL + "/protocol/openid-connect/auth", + TokenURL: ext.URL + "/protocol/openid-connect/token", + }, + RedirectURL: ext.RedirectURI, + Scopes: oauthScopes, + }, + Host: ext.URL, + }, nil +} + +func (g keycloakProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + return g.Exchange(oauth2.NoContext, code) +} + +func (g keycloakProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u keycloakUser + + if err := makeRequest(ctx, tok, g.Config, g.Host+"/protocol/openid-connect/userinfo", &u); err != nil { + return nil, err + } + + if u.Email == "" { + return nil, errors.New("Unable to find email with Keycloak provider") + } + + return &UserProvidedData{ + Metadata: &Claims{ + Issuer: g.Host, + Subject: u.Sub, + Name: u.Name, + Email: u.Email, + EmailVerified: u.EmailVerified, + + // To be deprecated + FullName: u.Name, + ProviderId: u.Sub, + }, + Emails: []Email{{ + Email: u.Email, + Verified: u.EmailVerified, + Primary: true, + }}, + }, nil + +} diff --git a/api/settings.go b/api/settings.go index 929593ed4d..df76dd1d13 100644 --- a/api/settings.go +++ b/api/settings.go @@ -9,6 +9,7 @@ type ProviderSettings struct { Discord bool `json:"discord"` GitHub bool `json:"github"` GitLab bool `json:"gitlab"` + Keycloak bool `json:"keycloak"` Google bool `json:"google"` Linkedin bool `json:"linkedin"` Facebook bool `json:"facebook"` @@ -49,6 +50,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { GitHub: config.External.Github.Enabled, GitLab: config.External.Gitlab.Enabled, Google: config.External.Google.Enabled, + Keycloak: config.External.Keycloak.Enabled, Linkedin: config.External.Linkedin.Enabled, Facebook: config.External.Facebook.Enabled, Notion: config.External.Notion.Enabled, diff --git a/api/settings_test.go b/api/settings_test.go index 4e02e5bf88..ebadc3d5f9 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -36,6 +36,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.Spotify) require.True(t, p.Slack) require.True(t, p.Google) + require.True(t, p.Keycloak) require.True(t, p.Linkedin) require.True(t, p.GitHub) require.True(t, p.GitLab) diff --git a/api/token.go b/api/token.go index f2853f171e..a20321825e 100644 --- a/api/token.go +++ b/api/token.go @@ -90,6 +90,10 @@ func (p *IdTokenGrantParams) getVerifier(ctx context.Context) (*oidc.IDTokenVeri oAuthProvider = config.External.Google oAuthProviderClientId = oAuthProvider.ClientID provider, err = oidc.NewProvider(ctx, "https://accounts.google.com") + case "keycloak": + oAuthProvider = config.External.Keycloak + oAuthProviderClientId = oAuthProvider.ClientID + provider, err = oidc.NewProvider(ctx, oAuthProvider.URL) default: return nil, fmt.Errorf("Provider %s doesn't support the id_token grant flow", p.Provider) } diff --git a/conf/configuration.go b/conf/configuration.go index dbea9bebe4..7b713e646d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -95,6 +95,7 @@ type ProviderConfiguration struct { Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` Notion OAuthProviderConfiguration `json:"notion"` + Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` Spotify OAuthProviderConfiguration `json:"spotify"` Slack OAuthProviderConfiguration `json:"slack"` diff --git a/example.env b/example.env index 0782f6ec7c..e38ef11a44 100644 --- a/example.env +++ b/example.env @@ -126,6 +126,13 @@ GOTRUE_EXTERNAL_SPOTIFY_CLIENT_ID="" GOTRUE_EXTERNAL_SPOTIFY_SECRET="" GOTRUE_EXTERNAL_SPOTIFY_REDIRECT_URI="http://localhost:9999/callback" +# Keycloak OAuth config +GOTRUE_EXTERNAL_KEYCLOAK_ENABLED="false" +GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID="" +GOTRUE_EXTERNAL_KEYCLOAK_SECRET="" +GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI="http://localhost:9999/callback" +GOTRUE_EXTERNAL_KEYCLOAK_URL="https://keycloak.example.com/auth/realms/myrealm" + # Linkedin OAuth config GOTRUE_EXTERNAL_LINKEDIN_ENABLED="true" GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID="" diff --git a/hack/test.env b/hack/test.env index 6d7f74ffb2..80bab4105d 100644 --- a/hack/test.env +++ b/hack/test.env @@ -37,6 +37,11 @@ GOTRUE_EXTERNAL_GITHUB_ENABLED=true GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=testclientid GOTRUE_EXTERNAL_GITHUB_SECRET=testsecret GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI=https://identity.services.netlify.com/callback +GOTRUE_EXTERNAL_KEYCLOAK_ENABLED=true +GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID=testclientid +GOTRUE_EXTERNAL_KEYCLOAK_SECRET=testsecret +GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI=https://identity.services.netlify.com/callback +GOTRUE_EXTERNAL_KEYCLOAK_URL=https://keycloak.example.com/auth/realms/myrealm GOTRUE_EXTERNAL_LINKEDIN_ENABLED=true GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID=testclientid GOTRUE_EXTERNAL_LINKEDIN_SECRET=testsecret From 9489d7e6356d2141f4eb41398a4780ff7fa2afaf Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Sun, 20 Mar 2022 15:35:33 +0100 Subject: [PATCH 025/102] fix: set idle_in_transaction_session_timeout to 5min (#418) --- migrations/20220320143831_set_idle_transaction_timeout.up.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 migrations/20220320143831_set_idle_transaction_timeout.up.sql diff --git a/migrations/20220320143831_set_idle_transaction_timeout.up.sql b/migrations/20220320143831_set_idle_transaction_timeout.up.sql new file mode 100644 index 0000000000..5adedf1e82 --- /dev/null +++ b/migrations/20220320143831_set_idle_transaction_timeout.up.sql @@ -0,0 +1,3 @@ +-- set idle_in_transaction_session_timeout to 5min + +ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO 300000; \ No newline at end of file From fefed991098465a60ec98a09ae35f8520b260337 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Mon, 21 Mar 2022 09:23:34 +0100 Subject: [PATCH 026/102] fix: no longer hardcode username for migration (#419) * fix: no longer hardcode username for migration The username is configurable via the connstring, and as such we can't rely on it being supabase_auth_admin * update idle transaction timeout time Co-authored-by: Kang Ming --- migrations/20220320143831_set_idle_transaction_timeout.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20220320143831_set_idle_transaction_timeout.up.sql b/migrations/20220320143831_set_idle_transaction_timeout.up.sql index 5adedf1e82..9d257dbd8f 100644 --- a/migrations/20220320143831_set_idle_transaction_timeout.up.sql +++ b/migrations/20220320143831_set_idle_transaction_timeout.up.sql @@ -1,3 +1,3 @@ --- set idle_in_transaction_session_timeout to 5min +-- set idle_in_transaction_session_timeout to 1min -ALTER ROLE supabase_auth_admin SET idle_in_transaction_session_timeout TO 300000; \ No newline at end of file +ALTER ROLE current_user SET idle_in_transaction_session_timeout TO 60000; From b611f1ac2e65ac914bd502b570b16b39f0f83296 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 22 Mar 2022 20:12:35 +0100 Subject: [PATCH 027/102] fix: allow user to update phone number (#421) * refactor: sendPhoneConfirmation should accept an otpType and smsProvider * fix: allow update phone for users endpoint * fix: add phone change verification method --- api/errors.go | 1 + api/otp.go | 8 +++- api/phone.go | 45 ++++++++++++++------- api/phone_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++ api/signup.go | 7 +++- api/user.go | 25 ++++++++++++ api/user_test.go | 52 +++++++++++++++++++++++- api/verify.go | 24 +++++++---- api/verify_test.go | 30 ++++++++++++-- go.sum | 1 + 10 files changed, 263 insertions(+), 29 deletions(-) create mode 100644 api/phone_test.go diff --git a/api/errors.go b/api/errors.go index f839357def..be77a7eea5 100644 --- a/api/errors.go +++ b/api/errors.go @@ -14,6 +14,7 @@ import ( // Common error messages during signup flow var ( DuplicateEmailMsg = "A user with this email address has already been registered" + DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") ) diff --git a/api/otp.go b/api/otp.go index 6b8a86b812..ad2955b415 100644 --- a/api/otp.go +++ b/api/otp.go @@ -7,6 +7,7 @@ import ( "net/http" "strings" + "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "github.com/sethvargo/go-password/password" @@ -103,8 +104,11 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { if err := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); err != nil { return err } - - if err := a.sendPhoneConfirmation(ctx, tx, user, params.Phone); err != nil { + smsProvider, err := sms_provider.GetSmsProvider(*config) + if err != nil { + return err + } + if err := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneConfirmationOtp, smsProvider); err != nil { return badRequestError("Error sending sms otp: %v", err) } return nil diff --git a/api/phone.go b/api/phone.go index c363973319..6c107fcc11 100644 --- a/api/phone.go +++ b/api/phone.go @@ -17,6 +17,11 @@ import ( const e164Format = `^[1-9]\d{1,14}$` const defaultSmsMessage = "Your code is %v" +const ( + phoneChangeOtp = "phone_change" + phoneConfirmationOtp = "confirmation" +) + // validateE165Format checks if phone number follows the E.164 format func (a *API) validateE164Format(phone string) bool { // match should never fail as long as regexp is valid @@ -29,39 +34,51 @@ func (a *API) formatPhoneNumber(phone string) string { return strings.ReplaceAll(strings.Trim(phone, "+"), " ", "") } -func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, user *models.User, phone string) error { +// sendPhoneConfirmation sends an otp to the user's phone number +func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, user *models.User, phone, otpType string, smsProvider sms_provider.SmsProvider) error { config := a.getConfig(ctx) - if user.ConfirmationSentAt != nil && !user.ConfirmationSentAt.Add(config.Sms.MaxFrequency).Before(time.Now()) { + var token *string + var sentAt *time.Time + var tokenDbField, sentAtDbField string + + if otpType == phoneConfirmationOtp { + token = &user.ConfirmationToken + sentAt = user.ConfirmationSentAt + tokenDbField, sentAtDbField = "confirmation_token", "confirmation_sent_at" + } else if otpType == phoneChangeOtp { + token = &user.PhoneChangeToken + sentAt = user.PhoneChangeSentAt + tokenDbField, sentAtDbField = "phone_change_token", "phone_change_sent_at" + } else { + return internalServerError("invalid otp type") + } + + if sentAt != nil && !sentAt.Add(config.Sms.MaxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } - oldToken := user.ConfirmationToken + oldToken := *token otp, err := crypto.GenerateOtp(config.Sms.OtpLength) if err != nil { return internalServerError("error generating otp").WithInternalError(err) } - user.ConfirmationToken = otp - - smsProvider, err := sms_provider.GetSmsProvider(*config) - if err != nil { - return err - } + *token = otp var message string if config.Sms.Template == "" { - message = fmt.Sprintf(defaultSmsMessage, user.ConfirmationToken) + message = fmt.Sprintf(defaultSmsMessage, *token) } else { - message = strings.Replace(config.Sms.Template, "{{ .Code }}", user.ConfirmationToken, -1) + message = strings.Replace(config.Sms.Template, "{{ .Code }}", *token, -1) } if serr := smsProvider.SendSms(phone, message); serr != nil { - user.ConfirmationToken = oldToken + *token = oldToken return serr } now := time.Now() - user.ConfirmationSentAt = &now + sentAt = &now - return errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation") + return errors.Wrap(tx.UpdateOnly(user, tokenDbField, sentAtDbField), "Database error updating user for confirmation") } diff --git a/api/phone_test.go b/api/phone_test.go new file mode 100644 index 0000000000..3e959b981c --- /dev/null +++ b/api/phone_test.go @@ -0,0 +1,99 @@ +package api + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type PhoneTestSuite struct { + suite.Suite + API *API + Config *conf.Configuration + + instanceID uuid.UUID +} + +type TestSmsProvider struct { + mock.Mock +} + +func (t TestSmsProvider) SendSms(phone string, message string) error { + return nil +} + +func TestPhone(t *testing.T) { + api, config, instanceID, err := setupAPIForTestForInstance() + require.NoError(t, err) + + ts := &PhoneTestSuite{ + API: api, + Config: config, + instanceID: instanceID, + } + defer api.db.Close() + + suite.Run(t, ts) +} + +func (ts *PhoneTestSuite) SetupTest() { + models.TruncateAll(ts.API.db) + + // Create user + u, err := models.NewUser(ts.instanceID, "", "password", ts.Config.JWT.Aud, nil) + u.Phone = "123456789" + require.NoError(ts.T(), err, "Error creating test user model") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") +} + +func (ts *PhoneTestSuite) TestValidateE164Format() { + isValid := ts.API.validateE164Format("0123456789") + assert.Equal(ts.T(), false, isValid) +} + +func (ts *PhoneTestSuite) TestFormatPhoneNumber() { + actual := ts.API.formatPhoneNumber("+1 23456789 ") + assert.Equal(ts.T(), "123456789", actual) +} + +func (ts *PhoneTestSuite) TestSendPhoneConfirmation() { + u, err := models.FindUserByPhoneAndAudience(ts.API.db, ts.instanceID, "123456789", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + ctx, err := WithInstanceConfig(context.Background(), ts.Config, ts.instanceID) + require.NoError(ts.T(), err) + cases := []struct { + desc string + otpType string + expected error + }{ + { + "send confirmation otp", + phoneConfirmationOtp, + nil, + }, + { + "send phone_change otp", + phoneChangeOtp, + nil, + }, + { + "send invalid otp type ", + "invalid otp type", + internalServerError("invalid otp type"), + }, + } + + for _, c := range cases { + ts.Run(c.desc, func() { + err = ts.API.sendPhoneConfirmation(ctx, ts.API.db, u, "123456789", c.otpType, TestSmsProvider{}) + require.Equal(ts.T(), c.expected, err) + }) + } +} diff --git a/api/signup.go b/api/signup.go index a35d1b716f..270f6f9489 100644 --- a/api/signup.go +++ b/api/signup.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/metering" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" @@ -159,7 +160,11 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { }); terr != nil { return terr } - if terr = a.sendPhoneConfirmation(ctx, tx, user, params.Phone); terr != nil { + smsProvider, err := sms_provider.GetSmsProvider(*config) + if err != nil { + return err + } + if terr = a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneConfirmationOtp, smsProvider); terr != nil { return badRequestError("Error sending confirmation sms: %v", terr) } } diff --git a/api/user.go b/api/user.go index 56296378e5..53fe2527e5 100644 --- a/api/user.go +++ b/api/user.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/gofrs/uuid" + "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" ) @@ -124,6 +125,30 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } } + if params.Phone != "" { + params.Phone = a.formatPhoneNumber(params.Phone) + if isValid := a.validateE164Format(params.Phone); !isValid { + return unprocessableEntityError("Invalid phone number format") + } + var exists bool + if exists, terr = models.IsDuplicatedPhone(tx, instanceID, params.Phone, user.Aud); terr != nil { + return internalServerError("Database error checking phone").WithInternalError(terr) + } else if exists { + return unprocessableEntityError(DuplicatePhoneMsg) + } + if config.Sms.Autoconfirm { + return user.UpdatePhone(tx, params.Phone) + } else { + smsProvider, terr := sms_provider.GetSmsProvider(*config) + if terr != nil { + return terr + } + if terr := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneChangeOtp, smsProvider); terr != nil { + return internalServerError("Error sending phone change otp").WithInternalError(terr) + } + } + } + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, nil); terr != nil { return internalServerError("Error recording audit log entry").WithInternalError(terr) } diff --git a/api/user_test.go b/api/user_test.go index 8d8566b1e7..b14d511544 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -43,6 +43,7 @@ func (ts *UserTestSuite) SetupTest() { // Create user u, err := models.NewUser(ts.instanceID, "test@example.com", "password", ts.Config.JWT.Aud, nil) + u.Phone = "123456789" require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } @@ -72,7 +73,7 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { "User doesn't have an existing email", map[string]string{ "email": "", - "phone": "123456789", + "phone": "", }, false, http.StatusOK, @@ -110,9 +111,9 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { ts.Run(c.desc, func() { u, err := models.NewUser(ts.instanceID, "", "", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") - require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user") require.NoError(ts.T(), u.SetEmail(ts.API.db, c.userData["email"]), "Error setting user email") require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"]), "Error setting user phone") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user") token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") @@ -132,6 +133,53 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { }) } +} +func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + cases := []struct { + desc string + userData map[string]string + expectedCode int + }{ + { + "New phone number is the same as current phone number", + map[string]string{ + "phone": "123456789", + }, + http.StatusUnprocessableEntity, + }, + { + "New phone number is different from current phone number", + map[string]string{ + "phone": "234567890", + }, + http.StatusOK, + }, + } + + ts.Config.Sms.Autoconfirm = true + + for _, c := range cases { + ts.Run(c.desc, func() { + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err, "Error generating access token") + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "phone": c.userData["phone"], + })) + req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), c.expectedCode, w.Code) + }) + } + } func (ts *UserTestSuite) TestUserUpdatePassword() { diff --git a/api/verify.go b/api/verify.go index 06bfe05a2a..5150cde953 100644 --- a/api/verify.go +++ b/api/verify.go @@ -26,6 +26,7 @@ const ( magicLinkVerification = "magiclink" emailChangeVerification = "email_change" smsVerification = "sms" + phoneChangeVerification = "phone_change" ) const ( @@ -98,8 +99,8 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { http.Redirect(w, r, rurl, http.StatusSeeOther) return nil } - case smsVerification: - user, terr = a.smsVerify(ctx, tx, user) + case smsVerification, phoneChangeVerification: + user, terr = a.smsVerify(ctx, tx, user, params.Type) default: return unprocessableEntityError("Verify requires a verification type") } @@ -231,7 +232,7 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, user return user, nil } -func (a *API) smsVerify(ctx context.Context, conn *storage.Connection, user *models.User) (*models.User, error) { +func (a *API) smsVerify(ctx context.Context, conn *storage.Connection, user *models.User, otpType string) (*models.User, error) { instanceID := getInstanceID(ctx) config := a.getConfig(ctx) @@ -245,8 +246,14 @@ func (a *API) smsVerify(ctx context.Context, conn *storage.Connection, user *mod return terr } - if terr = user.ConfirmPhone(tx); terr != nil { - return internalServerError("Error confirming user").WithInternalError(terr) + if otpType == smsVerification { + if terr = user.ConfirmPhone(tx); terr != nil { + return internalServerError("Error confirming user").WithInternalError(terr) + } + } else if otpType == phoneChangeVerification { + if terr = user.ConfirmPhoneChange(tx); terr != nil { + return internalServerError("Error confirming user").WithInternalError(terr) + } } return nil }) @@ -340,7 +347,7 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, case emailChangeVerification: user, err = models.FindUserByEmailChangeToken(conn, params.Token) } - } else if params.Type == smsVerification { + } else if params.Type == smsVerification || params.Type == phoneChangeVerification { if params.Phone == "" { return nil, unprocessableEntityError("Sms Verification requires a phone number") } @@ -383,6 +390,8 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, user.EmailChangeConfirmStatus = zeroConfirmation err = conn.UpdateOnly(user, "email_change_confirm_status") } + case phoneChangeVerification: + isValid = isOtpValid(params.Token, user.PhoneChangeToken, user.PhoneChangeSentAt.Add(smsOtpExpiresAt)) case smsVerification: isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt.Add(smsOtpExpiresAt)) } @@ -399,5 +408,6 @@ func isOtpValid(actual, expected string, expiresAt time.Time) bool { } func isUrlVerification(params *VerifyParams) bool { - return params.Type != smsVerification && params.Email == "" + isPhoneVerification := params.Type == smsVerification || params.Type == phoneChangeVerification + return !isPhoneVerification && params.Email == "" } diff --git a/api/verify_test.go b/api/verify_test.go index 65c4dc7fd9..597fbd3b6d 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -193,8 +193,10 @@ func (ts *VerifyTestSuite) TestInvalidOtp() { u, err := models.FindUserByPhoneAndAudience(ts.API.db, ts.instanceID, "12345678", ts.Config.JWT.Aud) require.NoError(ts.T(), err) u.ConfirmationToken = "123456" + u.PhoneChangeToken = "123456" sentTime := time.Now().Add(-48 * time.Hour) u.ConfirmationSentAt = &sentTime + u.PhoneChangeSentAt = &sentTime require.NoError(ts.T(), ts.API.db.Update(u)) type ResponseBody struct { @@ -233,6 +235,16 @@ func (ts *VerifyTestSuite) TestInvalidOtp() { }, expected: expectedResponse, }, + { + desc: "Invalid Phone Change OTP", + sentTime: time.Now(), + body: map[string]interface{}{ + "type": phoneChangeVerification, + "token": "invalid_otp", + "phone": u.GetPhone(), + }, + expected: expectedResponse, + }, { desc: "Invalid Email OTP", sentTime: time.Now(), @@ -588,6 +600,16 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { code: http.StatusSeeOther, }, }, + { + desc: "Valid Phone Change OTP", + sentTime: time.Now(), + body: map[string]interface{}{ + "type": phoneChangeVerification, + "token": "123456", + "phone": "12345678", + }, + expected: expectedResponse, + }, } for _, c := range cases { @@ -596,9 +618,11 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { u.ConfirmationSentAt = &c.sentTime u.RecoverySentAt = &c.sentTime u.EmailChangeSentAt = &c.sentTime - u.ConfirmationToken, _ = c.body["token"].(string) - u.RecoveryToken, _ = c.body["token"].(string) - u.EmailChangeTokenCurrent, _ = c.body["token"].(string) + u.PhoneChangeSentAt = &c.sentTime + u.ConfirmationToken = c.body["token"].(string) + u.RecoveryToken = c.body["token"].(string) + u.EmailChangeTokenCurrent = c.body["token"].(string) + u.PhoneChangeToken = c.body["token"].(string) require.NoError(ts.T(), ts.API.db.Update(u)) var buffer bytes.Buffer diff --git a/go.sum b/go.sum index 2fd375e267..f248244695 100644 --- a/go.sum +++ b/go.sum @@ -539,6 +539,7 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= From 3356266fe5688ce689b203f4157c668196fdd3a0 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Wed, 23 Mar 2022 17:41:26 +0800 Subject: [PATCH 028/102] fix: add `UserSignedUpAction` to the audit log when the user is unconfirmed (#423) This fixes a leftover bug following PRs #395 and #396. --- api/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/verify.go b/api/verify.go index 5150cde953..80de7785c1 100644 --- a/api/verify.go +++ b/api/verify.go @@ -205,7 +205,7 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, user return terr } if !user.IsConfirmed() { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil { return terr } From 460b31bea71b575783371ca10d3e78d142e1e137 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Wed, 23 Mar 2022 17:44:31 +0800 Subject: [PATCH 029/102] fix: re-use existing connection's transaction in `emailChangeVerify` (#424) This aligns the connection handling behaviour with the rest of the functions in `api/verify.go`. This looks like it might have been a leftover bug during refactoring in PR #379. --- api/verify.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/verify.go b/api/verify.go index 80de7785c1..574418af12 100644 --- a/api/verify.go +++ b/api/verify.go @@ -288,7 +288,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p config := a.getConfig(ctx) if config.Mailer.SecureEmailChangeEnabled && user.EmailChangeConfirmStatus == zeroConfirmation && user.GetEmail() != "" { - err := a.db.Transaction(func(tx *storage.Connection) error { + err := conn.Transaction(func(tx *storage.Connection) error { user.EmailChangeConfirmStatus = singleConfirmation if params.Token == user.EmailChangeTokenCurrent { user.EmailChangeTokenCurrent = "" @@ -307,7 +307,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p } // one email is confirmed at this point - err := a.db.Transaction(func(tx *storage.Connection) error { + err := conn.Transaction(func(tx *storage.Connection) error { var terr error if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, nil); terr != nil { From 18a8a5f6c9c3bb723e4b3bd8aee338e0e1b416e6 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 23 Mar 2022 13:21:20 +0100 Subject: [PATCH 030/102] fix: default nil interface to empty byte slice (#422) --- models/json_map.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/json_map.go b/models/json_map.go index 6db3c998b4..a343a100f0 100644 --- a/models/json_map.go +++ b/models/json_map.go @@ -23,6 +23,8 @@ func (j JSONMap) Scan(src interface{}) error { source = []byte(v) case []byte: source = v + case nil: + source = []byte("") default: return errors.New("Invalid data type for JSONMap") } From 95fa5f6884fbc068e5b336f2b8fe343b91dcfac9 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 24 Mar 2022 13:55:03 +0100 Subject: [PATCH 031/102] fix: ensure confirmation & phone change sent at is saved (#425) --- api/admin.go | 6 +++--- api/otp.go | 10 +++++----- api/phone.go | 14 +++++++++++++- api/phone_test.go | 16 ++++++++++++++-- api/signup.go | 6 +++--- api/sms_provider/messagebird.go | 2 +- api/sms_provider/textlocal.go | 4 ++-- api/sms_provider/twilio.go | 2 +- api/sms_provider/vonage.go | 2 +- api/user.go | 6 +++--- api/verify.go | 6 +++--- 11 files changed, 49 insertions(+), 25 deletions(-) diff --git a/api/admin.go b/api/admin.go index 8022cda8f7..c2d94e93a6 100644 --- a/api/admin.go +++ b/api/admin.go @@ -231,9 +231,9 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error { } if params.Phone != "" { - params.Phone = a.formatPhoneNumber(params.Phone) - if isValid := a.validateE164Format(params.Phone); !isValid { - return unprocessableEntityError("Invalid phone format") + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return err } if exists, err := models.IsDuplicatedPhone(a.db, instanceID, params.Phone, aud); err != nil { return internalServerError("Database error checking phone").WithInternalError(err) diff --git a/api/otp.go b/api/otp.go index ad2955b415..071bfe6063 100644 --- a/api/otp.go +++ b/api/otp.go @@ -70,10 +70,10 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read sms otp params: %v", err) } - params.Phone = a.formatPhoneNumber(params.Phone) - - if isValid := a.validateE164Format(params.Phone); !isValid { - return badRequestError("Invalid format: Phone number should follow the E.164 format") + var err error + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return err } aud := a.requestAud(ctx, r) @@ -100,7 +100,7 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error finding user").WithInternalError(uerr) } - err := a.db.Transaction(func(tx *storage.Connection) error { + err = a.db.Transaction(func(tx *storage.Connection) error { if err := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); err != nil { return err } diff --git a/api/phone.go b/api/phone.go index 6c107fcc11..2dcb1338de 100644 --- a/api/phone.go +++ b/api/phone.go @@ -22,6 +22,14 @@ const ( phoneConfirmationOtp = "confirmation" ) +func (a *API) validatePhone(phone string) (string, error) { + phone = a.formatPhoneNumber(phone) + if isValid := a.validateE164Format(phone); !isValid { + return "", unprocessableEntityError("Invalid phone number format") + } + return phone, nil +} + // validateE165Format checks if phone number follows the E.164 format func (a *API) validateE164Format(phone string) bool { // match should never fail as long as regexp is valid @@ -78,7 +86,11 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, } now := time.Now() - sentAt = &now + if otpType == phoneConfirmationOtp { + user.ConfirmationSentAt = &now + } else if otpType == phoneChangeOtp { + user.PhoneChangeSentAt = &now + } return errors.Wrap(tx.UpdateOnly(user, tokenDbField, sentAtDbField), "Database error updating user for confirmation") } diff --git a/api/phone_test.go b/api/phone_test.go index 3e959b981c..9bc4f9c5ae 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -25,7 +25,7 @@ type TestSmsProvider struct { mock.Mock } -func (t TestSmsProvider) SendSms(phone string, message string) error { +func (t *TestSmsProvider) SendSms(phone string, message string) error { return nil } @@ -92,8 +92,20 @@ func (ts *PhoneTestSuite) TestSendPhoneConfirmation() { for _, c := range cases { ts.Run(c.desc, func() { - err = ts.API.sendPhoneConfirmation(ctx, ts.API.db, u, "123456789", c.otpType, TestSmsProvider{}) + err = ts.API.sendPhoneConfirmation(ctx, ts.API.db, u, "123456789", c.otpType, &TestSmsProvider{}) require.Equal(ts.T(), c.expected, err) + u, err = models.FindUserByPhoneAndAudience(ts.API.db, ts.instanceID, "123456789", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + switch c.otpType { + case phoneConfirmationOtp: + require.NotEmpty(ts.T(), u.ConfirmationToken) + require.NotEmpty(ts.T(), u.ConfirmationSentAt) + case phoneChangeOtp: + require.NotEmpty(ts.T(), u.PhoneChangeToken) + require.NotEmpty(ts.T(), u.PhoneChangeSentAt) + default: + } }) } } diff --git a/api/signup.go b/api/signup.go index 270f6f9489..462f9e7cd9 100644 --- a/api/signup.go +++ b/api/signup.go @@ -76,9 +76,9 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { if !config.External.Phone.Enabled { return badRequestError("Phone signups are disabled") } - params.Phone = a.formatPhoneNumber(params.Phone) - if isValid := a.validateE164Format(params.Phone); !isValid { - return unprocessableEntityError("Invalid phone number format") + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return err } user, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, params.Aud) default: diff --git a/api/sms_provider/messagebird.go b/api/sms_provider/messagebird.go index 0055bad286..88f61df4c9 100644 --- a/api/sms_provider/messagebird.go +++ b/api/sms_provider/messagebird.go @@ -55,7 +55,7 @@ func NewMessagebirdProvider(config conf.MessagebirdProviderConfiguration) (SmsPr } // Send an SMS containing the OTP with Messagebird's API -func (t MessagebirdProvider) SendSms(phone string, message string) error { +func (t *MessagebirdProvider) SendSms(phone string, message string) error { body := url.Values{ "originator": {t.Config.Originator}, "body": {message}, diff --git a/api/sms_provider/textlocal.go b/api/sms_provider/textlocal.go index c45cab7034..09e30f234b 100644 --- a/api/sms_provider/textlocal.go +++ b/api/sms_provider/textlocal.go @@ -44,7 +44,7 @@ func NewTextlocalProvider(config conf.TextlocalProviderConfiguration) (SmsProvid } // Send an SMS containing the OTP with Textlocal's API -func (t TextlocalProvider) SendSms(phone string, message string) error { +func (t *TextlocalProvider) SendSms(phone string, message string) error { body := url.Values{ "sender": {t.Config.Sender}, "apikey": {t.Config.ApiKey}, @@ -70,7 +70,7 @@ func (t TextlocalProvider) SendSms(phone string, message string) error { if derr != nil { return derr } - + if len(resp.Errors) == 0 { return errors.New("Textlocal error: Internal Error") } diff --git a/api/sms_provider/twilio.go b/api/sms_provider/twilio.go index c9f4cc6fed..a77d9c1072 100644 --- a/api/sms_provider/twilio.go +++ b/api/sms_provider/twilio.go @@ -54,7 +54,7 @@ func NewTwilioProvider(config conf.TwilioProviderConfiguration) (SmsProvider, er } // Send an SMS containing the OTP with Twilio's API -func (t TwilioProvider) SendSms(phone string, message string) error { +func (t *TwilioProvider) SendSms(phone string, message string) error { body := url.Values{ "To": {"+" + phone}, // twilio api requires "+" extension to be included "Channel": {"sms"}, diff --git a/api/sms_provider/vonage.go b/api/sms_provider/vonage.go index e266dbcc55..fdc3752584 100644 --- a/api/sms_provider/vonage.go +++ b/api/sms_provider/vonage.go @@ -43,7 +43,7 @@ func NewVonageProvider(config conf.VonageProviderConfiguration) (SmsProvider, er } // Send an SMS containing the OTP with Vonage's API -func (t VonageProvider) SendSms(phone string, message string) error { +func (t *VonageProvider) SendSms(phone string, message string) error { body := url.Values{ "from": {t.Config.From}, "to": {phone}, diff --git a/api/user.go b/api/user.go index 53fe2527e5..ba99dfeb79 100644 --- a/api/user.go +++ b/api/user.go @@ -126,9 +126,9 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } if params.Phone != "" { - params.Phone = a.formatPhoneNumber(params.Phone) - if isValid := a.validateE164Format(params.Phone); !isValid { - return unprocessableEntityError("Invalid phone number format") + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return err } var exists bool if exists, terr = models.IsDuplicatedPhone(tx, instanceID, params.Phone, user.Aud); terr != nil { diff --git a/api/verify.go b/api/verify.go index 574418af12..655a2c55f0 100644 --- a/api/verify.go +++ b/api/verify.go @@ -351,9 +351,9 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, if params.Phone == "" { return nil, unprocessableEntityError("Sms Verification requires a phone number") } - params.Phone = a.formatPhoneNumber(params.Phone) - if ok := a.validateE164Format(params.Phone); !ok { - return nil, unprocessableEntityError("Invalid phone number format") + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return nil, err } user, err = models.FindUserByPhoneAndAudience(conn, instanceID, params.Phone, aud) } else if params.Email != "" { From 924a8a5900b660d94ab6fd1461a8ed50db460b7b Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 24 Mar 2022 14:14:33 +0100 Subject: [PATCH 032/102] fix: SmsOtp should still send otp the first time when sms autoconfirm is true (#426) --- api/otp.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/otp.go b/api/otp.go index 071bfe6063..d001764b51 100644 --- a/api/otp.go +++ b/api/otp.go @@ -92,6 +92,17 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { fakeResponse := &responseStub{} + if config.Sms.Autoconfirm { + // signups are autoconfirmed, send otp after signup + if err := a.Signup(fakeResponse, r); err != nil { + return err + } + newBodyContent := `{"phone":"` + params.Phone + `"}` + r.Body = ioutil.NopCloser(strings.NewReader(newBodyContent)) + r.ContentLength = int64(len(newBodyContent)) + return a.SmsOtp(w, r) + } + if err := a.Signup(fakeResponse, r); err != nil { return err } From 5b08af368e5340004900cf0eb690f9ad48e80570 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 25 Mar 2022 23:46:13 +0100 Subject: [PATCH 033/102] fix: allow enforcing of reauthentication when user updates password (#427) * use pointer receiever over value receiver * fix: ensure confirmation and phone change sent at time is passed correctly * fix: add flag to toggle whether update password requires reauth * fix: add logic for update password reauth * add test for update password w reauthentication * refactor phone number validation * fix: add recovery as an otp type * fix: add phone otp as reauth mechanism * fix: verify should allow recovery via phone otp * remove original reauth logic * fix: add config for reauthentication template * fix: update reauthentication logic * fix: add GET /reauthenticate * fix: rename secret to nonce * update user test * return empty json body for reauthenticate * refactor verify reauthentication logic * refactor user update password test * add test cases for reauthentication * docs: add reauthenticate endpoint --- README.md | 25 +++++ api/api.go | 5 + api/mail.go | 16 +++ api/phone.go | 29 +++-- api/phone_test.go | 12 +- api/reauthenticate.go | 98 +++++++++++++++++ api/recover.go | 37 +++++-- api/user.go | 18 ++- api/user_test.go | 103 +++++++++++++++--- api/verify.go | 24 +++- conf/configuration.go | 16 +-- mailer/mailer.go | 1 + mailer/template.go | 22 ++++ ...323170000_add_user_reauthentication.up.sql | 5 + models/audit_log_entry.go | 1 + models/user.go | 12 ++ 16 files changed, 372 insertions(+), 52 deletions(-) create mode 100644 api/reauthenticate.go create mode 100644 migrations/20220323170000_add_user_reauthentication.up.sql diff --git a/README.md b/README.md index 0c924249b6..2189750349 100644 --- a/README.md +++ b/README.md @@ -494,6 +494,12 @@ for now the only option supported is: `hcaptcha` Retrieve from hcaptcha account +### Reauthentication + +`SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION` - `bool` + +Enforce reauthentication on password update. + ## Endpoints GoTrue exposes the following endpoints: @@ -914,6 +920,25 @@ Returns: } ``` +If `GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION` is enabled, the user will need to reauthenticate first. + +```json +{ + "password": "new-password", + "nonce": "123456", +} +``` + +### **GET /reauthenticate** + +Sends a nonce to the user's email (preferred) or phone. This endpoint requires the user to be logged in / authenticated first. The user needs to have either an email or phone number for the nonce to be sent successfully. + +```json +headers: { + "Authorization" : "Bearer eyJhbGciOiJI...M3A90LCkxxtX9oNP9KZO" +} +``` + ### **POST /logout** Logout a user (Requires authentication). diff --git a/api/api.go b/api/api.go index 6ea1978563..ebe1f6b1c8 100644 --- a/api/api.go +++ b/api/api.go @@ -141,6 +141,11 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.With(api.requireAuthentication).Post("/logout", api.Logout) + r.Route("/reauthenticate", func(r *router) { + r.Use(api.requireAuthentication) + r.Get("/", api.Reauthenticate) + }) + r.Route("/user", func(r *router) { r.Use(api.requireAuthentication) r.Get("/", api.UserGet) diff --git a/api/mail.go b/api/mail.go index 5d9ea1401d..c8ab003ccc 100644 --- a/api/mail.go +++ b/api/mail.go @@ -212,6 +212,22 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery") } +func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration) error { + if u.ReauthenticationSentAt != nil && !u.ReauthenticationSentAt.Add(maxFrequency).Before(time.Now()) { + return MaxFrequencyLimitError + } + + oldToken := u.ReauthenticationToken + u.ReauthenticationToken = crypto.SecureToken() + now := time.Now() + if err := mailer.ReauthenticateMail(u); err != nil { + u.ReauthenticationToken = oldToken + return errors.Wrap(err, "Error sending reauthentication email") + } + u.ReauthenticationSentAt = &now + return errors.Wrap(tx.UpdateOnly(u, "reauthentication_token", "reauthentication_sent_at"), "Database error updating user for reauthentication") +} + func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { // since Magic Link is just a recovery with a different template and behaviour // around new users we will reuse the recovery db timer to prevent potential abuse diff --git a/api/phone.go b/api/phone.go index 2dcb1338de..aa6833f16f 100644 --- a/api/phone.go +++ b/api/phone.go @@ -18,8 +18,8 @@ const e164Format = `^[1-9]\d{1,14}$` const defaultSmsMessage = "Your code is %v" const ( - phoneChangeOtp = "phone_change" - phoneConfirmationOtp = "confirmation" + phoneConfirmationOtp = "confirmation" + phoneReauthenticationOtp = "reauthentication" ) func (a *API) validatePhone(phone string) (string, error) { @@ -50,15 +50,20 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, var sentAt *time.Time var tokenDbField, sentAtDbField string - if otpType == phoneConfirmationOtp { - token = &user.ConfirmationToken - sentAt = user.ConfirmationSentAt - tokenDbField, sentAtDbField = "confirmation_token", "confirmation_sent_at" - } else if otpType == phoneChangeOtp { + switch otpType { + case phoneChangeVerification: token = &user.PhoneChangeToken sentAt = user.PhoneChangeSentAt tokenDbField, sentAtDbField = "phone_change_token", "phone_change_sent_at" - } else { + case phoneConfirmationOtp: + token = &user.ConfirmationToken + sentAt = user.ConfirmationSentAt + tokenDbField, sentAtDbField = "confirmation_token", "confirmation_sent_at" + case phoneReauthenticationOtp: + token = &user.ReauthenticationToken + sentAt = user.ReauthenticationSentAt + tokenDbField, sentAtDbField = "reauthentication_token", "reauthentication_sent_at" + default: return internalServerError("invalid otp type") } @@ -86,10 +91,14 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, } now := time.Now() - if otpType == phoneConfirmationOtp { + + switch otpType { + case phoneConfirmationOtp: user.ConfirmationSentAt = &now - } else if otpType == phoneChangeOtp { + case phoneChangeVerification: user.PhoneChangeSentAt = &now + case phoneReauthenticationOtp: + user.ReauthenticationSentAt = &now } return errors.Wrap(tx.UpdateOnly(user, tokenDbField, sentAtDbField), "Database error updating user for confirmation") diff --git a/api/phone_test.go b/api/phone_test.go index 9bc4f9c5ae..9eda8027ca 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -80,7 +80,12 @@ func (ts *PhoneTestSuite) TestSendPhoneConfirmation() { }, { "send phone_change otp", - phoneChangeOtp, + phoneChangeVerification, + nil, + }, + { + "send recovery otp", + phoneReauthenticationOtp, nil, }, { @@ -101,9 +106,12 @@ func (ts *PhoneTestSuite) TestSendPhoneConfirmation() { case phoneConfirmationOtp: require.NotEmpty(ts.T(), u.ConfirmationToken) require.NotEmpty(ts.T(), u.ConfirmationSentAt) - case phoneChangeOtp: + case phoneChangeVerification: require.NotEmpty(ts.T(), u.PhoneChangeToken) require.NotEmpty(ts.T(), u.PhoneChangeSentAt) + case phoneReauthenticationOtp: + require.NotEmpty(ts.T(), u.ReauthenticationToken) + require.NotEmpty(ts.T(), u.ReauthenticationSentAt) default: } }) diff --git a/api/reauthenticate.go b/api/reauthenticate.go new file mode 100644 index 0000000000..307b8b159c --- /dev/null +++ b/api/reauthenticate.go @@ -0,0 +1,98 @@ +package api + +import ( + "errors" + "net/http" + "time" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/api/sms_provider" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" + "github.com/netlify/gotrue/storage" +) + +// Reauthenticate sends a reauthentication otp to either the user's email or phone +func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.getConfig(ctx) + instanceID := getInstanceID(ctx) + + claims := getClaims(ctx) + userID, err := uuid.FromString(claims.Subject) + if err != nil { + return badRequestError("Could not read User ID claim") + } + user, err := models.FindUserByID(a.db, userID) + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding user").WithInternalError(err) + } + + email, phone := user.GetEmail(), user.GetPhone() + + if email == "" && phone == "" { + return unprocessableEntityError("Reauthentication requires the user to have an email or a phone number") + } + + if email != "" { + if !user.IsConfirmed() { + return badRequestError("Please verify your email first.") + } + } else if phone != "" { + if !user.IsPhoneConfirmed() { + return badRequestError("Please verify your phone first.") + } + } + + err = a.db.Transaction(func(tx *storage.Connection) error { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserReauthenticateAction, nil); terr != nil { + return terr + } + if email != "" { + mailer := a.Mailer(ctx) + return a.sendReauthenticationOtp(tx, user, mailer, config.SMTP.MaxFrequency) + } else if phone != "" { + smsProvider, err := sms_provider.GetSmsProvider(*config) + if err != nil { + return err + } + return a.sendPhoneConfirmation(ctx, tx, user, phone, recoveryVerification, smsProvider) + } + return nil + }) + if err != nil { + if errors.Is(err, MaxFrequencyLimitError) { + return tooManyRequestsError("For security purposes, you can only request this once every 60 seconds") + } + return internalServerError("Reauthentication failed.").WithInternalError(err) + } + + return sendJSON(w, http.StatusOK, make(map[string]string)) +} + +// verifyReauthentication checks if the nonce provided is valid +func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, config *conf.Configuration, user *models.User) error { + if user.ReauthenticationToken == "" || user.ReauthenticationSentAt == nil { + return unauthorizedError("Requires reauthentication") + } + var isValid bool + if user.GetEmail() != "" { + mailerOtpExpiresAt := time.Second * time.Duration(config.Mailer.OtpExp) + isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt.Add(mailerOtpExpiresAt)) + } else if user.GetPhone() != "" { + smsOtpExpiresAt := time.Second * time.Duration(config.Sms.OtpExp) + isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt.Add(smsOtpExpiresAt)) + } else { + return unprocessableEntityError("Reauthentication requires an email or a phone number") + } + if !isValid { + return badRequestError("Nonce has expired or is invalid") + } + if err := user.ConfirmReauthentication(tx); err != nil { + return internalServerError("Error during reauthentication").WithInternalError(err) + } + return nil +} diff --git a/api/recover.go b/api/recover.go index 2853e75427..94de22ffe8 100644 --- a/api/recover.go +++ b/api/recover.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" + "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" ) @@ -12,6 +13,7 @@ import ( // RecoverParams holds the parameters for a password recovery request type RecoverParams struct { Email string `json:"email"` + Phone string `json:"phone"` } // Recover sends a recovery email @@ -26,13 +28,26 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read verification params: %v", err) } - if params.Email == "" { - return unprocessableEntityError("Password recovery requires an email") + if params.Email == "" && params.Phone == "" { + return unprocessableEntityError("Password recovery requires an email or a phone number") } + var user *models.User aud := a.requestAud(ctx, r) - user, err := models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud) recoverErrorMessage := "If a user exists, you will receive an email with instructions on how to reset your password." + if params.Email != "" { + if err := a.validateEmail(ctx, params.Email); err != nil { + return err + } + user, err = models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud) + } else if params.Phone != "" { + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return err + } + user, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, aud) + } + if err != nil { if models.IsNotFoundError(err) { return notFoundError(err.Error()) @@ -44,10 +59,18 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil { return terr } - - mailer := a.Mailer(ctx) - referrer := a.getReferrer(r) - return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) + if params.Email != "" { + mailer := a.Mailer(ctx) + referrer := a.getReferrer(r) + return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) + } else if params.Phone != "" { + smsProvider, err := sms_provider.GetSmsProvider(*config) + if err != nil { + return err + } + return a.sendPhoneConfirmation(ctx, tx, user, params.Phone, recoveryVerification, smsProvider) + } + return nil }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/api/user.go b/api/user.go index ba99dfeb79..c7ab19e45f 100644 --- a/api/user.go +++ b/api/user.go @@ -14,6 +14,7 @@ import ( type UserUpdateParams struct { Email string `json:"email"` Password *string `json:"password"` + Nonce string `json:"nonce"` Data map[string]interface{} `json:"data"` AppData map[string]interface{} `json:"app_metadata,omitempty"` Phone string `json:"phone"` @@ -85,8 +86,19 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { return invalidPasswordLengthError(config) } - if terr = user.UpdatePassword(tx, *params.Password); terr != nil { - return internalServerError("Error during password storage").WithInternalError(terr) + if !config.Security.UpdatePasswordRequireReauthentication { + if terr = user.UpdatePassword(tx, *params.Password); terr != nil { + return internalServerError("Error during password storage").WithInternalError(terr) + } + } else if params.Nonce == "" { + return unauthorizedError("Password update requires reauthentication.") + } else { + if terr = a.verifyReauthentication(params.Nonce, tx, config, user); terr != nil { + return terr + } + if terr = user.UpdatePassword(tx, *params.Password); terr != nil { + return internalServerError("Error during password storage").WithInternalError(terr) + } } } @@ -143,7 +155,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { if terr != nil { return terr } - if terr := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneChangeOtp, smsProvider); terr != nil { + if terr := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneChangeVerification, smsProvider); terr != nil { return internalServerError("Error sending phone change otp").WithInternalError(terr) } } diff --git a/api/user_test.go b/api/user_test.go index b14d511544..585799c736 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -186,34 +186,53 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - var cases = []struct { - desc string - update map[string]interface{} - expectedCode int + type expected struct { + code int isAuthenticated bool + } + + var cases = []struct { + desc string + newPassword string + nonce string + requireReauthentication bool + expected expected }{ { "Valid password length", - map[string]interface{}{ - "password": "newpass", - }, - http.StatusOK, - true, + "newpassword", + "", + false, + expected{code: http.StatusOK, isAuthenticated: true}, }, { "Invalid password length", - map[string]interface{}{ - "password": "", - }, - http.StatusUnprocessableEntity, + "", + "", false, + expected{code: http.StatusUnprocessableEntity, isAuthenticated: false}, + }, + { + "No reauthentication provided", + "newpassword123", + "", + true, + expected{code: http.StatusUnauthorized, isAuthenticated: false}, + }, + { + "Invalid nonce", + "newpassword123", + "123456", + true, + expected{code: http.StatusUnauthorized, isAuthenticated: false}, }, } for _, c := range cases { ts.Run(c.desc, func() { + ts.Config.Security.UpdatePasswordRequireReauthentication = c.requireReauthentication var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.update)) + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"password": c.newPassword, "nonce": c.nonce})) req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) req.Header.Set("Content-Type", "application/json") @@ -225,14 +244,64 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { // Setup response recorder w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), w.Code, c.expectedCode) + require.Equal(ts.T(), w.Code, c.expected.code) // Request body u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - passwordUpdate, _ := c.update["password"].(string) - require.Equal(ts.T(), c.isAuthenticated, u.Authenticate(passwordUpdate)) + require.Equal(ts.T(), c.expected.isAuthenticated, u.Authenticate(c.newPassword)) }) } } + +func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() { + ts.Config.Security.UpdatePasswordRequireReauthentication = true + + // create a confirmed user + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + now := time.Now() + u.EmailConfirmedAt = &now + require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + // request for reauthentication nonce + req := httptest.NewRequest(http.MethodGet, "http://localhost/reauthenticate", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), w.Code, http.StatusOK) + + u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), u.ReauthenticationToken) + require.NotEmpty(ts.T(), u.ReauthenticationSentAt) + + // update password with reauthentication token + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "password": "newpass", + "nonce": u.ReauthenticationToken, + })) + + req = httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) + req.Header.Set("Content-Type", "application/json") + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w = httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), w.Code, http.StatusOK) + + // Request body + u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + require.True(ts.T(), u.Authenticate("newpass")) + require.Empty(ts.T(), u.ReauthenticationToken) + require.NotEmpty(ts.T(), u.ReauthenticationSentAt) +} diff --git a/api/verify.go b/api/verify.go index 655a2c55f0..844ef85bee 100644 --- a/api/verify.go +++ b/api/verify.go @@ -338,7 +338,7 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, var user *models.User var err error - if isUrlVerification(params) { + if isUrlLinkVerification(params) { switch params.Type { case signupVerification, inviteVerification: user, err = models.FindUserByConfirmationToken(conn, params.Token) @@ -347,7 +347,7 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, case emailChangeVerification: user, err = models.FindUserByEmailChangeToken(conn, params.Token) } - } else if params.Type == smsVerification || params.Type == phoneChangeVerification { + } else if isPhoneOtpVerification(params) { if params.Phone == "" { return nil, unprocessableEntityError("Sms Verification requires a phone number") } @@ -356,11 +356,13 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, return nil, err } user, err = models.FindUserByPhoneAndAudience(conn, instanceID, params.Phone, aud) - } else if params.Email != "" { + } else if isEmailOtpVerification(params) { if err := a.validateEmail(ctx, params.Email); err != nil { return nil, unprocessableEntityError("Invalid email format").WithInternalError(err) } user, err = models.FindUserByEmailAndAudience(conn, instanceID, params.Email, aud) + } else { + return nil, badRequestError("Only an email address or phone number should be provided on verify, not both.") } if err != nil { @@ -407,7 +409,17 @@ func isOtpValid(actual, expected string, expiresAt time.Time) bool { return time.Now().Before(expiresAt) && (actual == expected) } -func isUrlVerification(params *VerifyParams) bool { - isPhoneVerification := params.Type == smsVerification || params.Type == phoneChangeVerification - return !isPhoneVerification && params.Email == "" +// isUrlLinkVerification checks if the verification came from clicking an email link which wouldn't contain the email field in the params +func isUrlLinkVerification(params *VerifyParams) bool { + return params.Phone == "" && params.Email == "" +} + +// isPhoneOtpVerification checks if the verification came from a phone otp +func isPhoneOtpVerification(params *VerifyParams) bool { + return params.Phone != "" && params.Email == "" +} + +// isEmailOtpVerification checks if the verification came from an email otp +func isEmailOtpVerification(params *VerifyParams) bool { + return params.Phone == "" && params.Email != "" } diff --git a/conf/configuration.go b/conf/configuration.go index 7b713e646d..23aacf3207 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -78,11 +78,12 @@ type GlobalConfiguration struct { // EmailContentConfiguration holds the configuration for emails, both subjects and template URLs. type EmailContentConfiguration struct { - Invite string `json:"invite"` - Confirmation string `json:"confirmation"` - Recovery string `json:"recovery"` - EmailChange string `json:"email_change" split_words:"true"` - MagicLink string `json:"magic_link" split_words:"true"` + Invite string `json:"invite"` + Confirmation string `json:"confirmation"` + Recovery string `json:"recovery"` + EmailChange string `json:"email_change" split_words:"true"` + MagicLink string `json:"magic_link" split_words:"true"` + Reauthentication string `json:"reauthentication"` } type ProviderConfiguration struct { @@ -175,8 +176,9 @@ type CaptchaConfiguration struct { } type SecurityConfiguration struct { - Captcha CaptchaConfiguration `json:"captcha"` - RefreshTokenRotationEnabled bool `json:"refresh_token_rotation_enabled" split_words:"true" default:"true"` + Captcha CaptchaConfiguration `json:"captcha"` + RefreshTokenRotationEnabled bool `json:"refresh_token_rotation_enabled" split_words:"true" default:"true"` + UpdatePasswordRequireReauthentication bool `json:"update_password_require_reauthentication" split_words:"true"` } // Configuration holds all the per-instance configuration. diff --git a/mailer/mailer.go b/mailer/mailer.go index 28fbea68ee..3441eb5a47 100644 --- a/mailer/mailer.go +++ b/mailer/mailer.go @@ -19,6 +19,7 @@ type Mailer interface { RecoveryMail(user *models.User, referrerURL string) error MagicLinkMail(user *models.User, referrerURL string) error EmailChangeMail(user *models.User, referrerURL string) error + ReauthenticateMail(user *models.User) error ValidateEmail(email string) error GetEmailActionLink(user *models.User, actionType, referrerURL string) (string, error) } diff --git a/mailer/template.go b/mailer/template.go index bda1ab5b3a..64673edb5a 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -52,6 +52,10 @@ const defaultEmailChangeMail = `

Confirm email address change

Change email address

Alternatively, enter the code: {{ .Token }}

` +const defaultReauthenticateMail = `

Confirm reauthentication

+ +

Enter the code: {{ .Token }}

` + // ValidateEmail returns nil if the email is valid, // otherwise an error indicating the reason it is invalid func (m TemplateMailer) ValidateEmail(email string) error { @@ -118,6 +122,24 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) ) } +// ReauthenticateMail sends a reauthentication mail to an authenticated user +func (m *TemplateMailer) ReauthenticateMail(user *models.User) error { + data := map[string]interface{}{ + "SiteURL": m.Config.SiteURL, + "Email": user.Email, + "Token": user.ReauthenticationToken, + "Data": user.UserMetaData, + } + + return m.Mailer.Mail( + user.GetEmail(), + string(withDefault(m.Config.Mailer.Subjects.Reauthentication, "Confirm reauthentication")), + m.Config.Mailer.Templates.Reauthentication, + defaultReauthenticateMail, + data, + ) +} + // EmailChangeMail sends an email change confirmation mail to a user func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) error { type Email struct { diff --git a/migrations/20220323170000_add_user_reauthentication.up.sql b/migrations/20220323170000_add_user_reauthentication.up.sql new file mode 100644 index 0000000000..3b6e60b7c3 --- /dev/null +++ b/migrations/20220323170000_add_user_reauthentication.up.sql @@ -0,0 +1,5 @@ +-- adds reauthentication_token and reauthentication_sent_at + +ALTER TABLE auth.users +ADD COLUMN IF NOT EXISTS reauthentication_token varchar(255) null default '', +ADD COLUMN IF NOT EXISTS reauthentication_sent_at timestamptz null default null; diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 3aa39b7424..72bf6e9770 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -22,6 +22,7 @@ const ( UserDeletedAction AuditAction = "user_deleted" UserModifiedAction AuditAction = "user_modified" UserRecoveryRequestedAction AuditAction = "user_recovery_requested" + UserReauthenticateAction AuditAction = "user_reauthenticate_requested" UserConfirmationRequestedAction AuditAction = "user_confirmation_requested" UserRepeatedSignUpAction AuditAction = "user_repeated_signup" TokenRevokedAction AuditAction = "token_revoked" diff --git a/models/user.go b/models/user.go index d2f7083e1c..09de86ecb3 100644 --- a/models/user.go +++ b/models/user.go @@ -50,6 +50,9 @@ type User struct { PhoneChange string `json:"new_phone,omitempty" db:"phone_change"` PhoneChangeSentAt *time.Time `json:"phone_change_sent_at,omitempty" db:"phone_change_sent_at"` + ReauthenticationToken string `json:"-" db:"reauthentication_token"` + ReauthenticationSentAt *time.Time `json:"reauthentication_sent_at,omitempty" db:"reauthentication_sent_at"` + LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"` AppMetaData JSONMap `json:"app_metadata" db:"raw_app_meta_data"` @@ -145,6 +148,9 @@ func (u *User) BeforeSave(tx *pop.Connection) error { if u.PhoneChangeSentAt != nil && u.PhoneChangeSentAt.IsZero() { u.PhoneChangeSentAt = nil } + if u.ReauthenticationSentAt != nil && u.ReauthenticationSentAt.IsZero() { + u.ReauthenticationSentAt = nil + } if u.LastSignInAt != nil && u.LastSignInAt.IsZero() { u.LastSignInAt = nil } @@ -275,6 +281,12 @@ func (u *User) Authenticate(password string) bool { return err == nil } +// ConfirmReauthentication resets the reauthentication token +func (u *User) ConfirmReauthentication(tx *storage.Connection) error { + u.ReauthenticationToken = "" + return tx.UpdateOnly(u, "reauthentication_token") +} + // Confirm resets the confimation token and sets the confirm timestamp func (u *User) Confirm(tx *storage.Connection) error { u.ConfirmationToken = "" From 17587e5349ac185c4892f95104b5469f44100dc2 Mon Sep 17 00:00:00 2001 From: Div Arora Date: Sun, 27 Mar 2022 16:11:53 +0200 Subject: [PATCH 034/102] fix: remove migration that requires elevated privileges (#428) --- migrations/20220320143831_set_idle_transaction_timeout.up.sql | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 migrations/20220320143831_set_idle_transaction_timeout.up.sql diff --git a/migrations/20220320143831_set_idle_transaction_timeout.up.sql b/migrations/20220320143831_set_idle_transaction_timeout.up.sql deleted file mode 100644 index 9d257dbd8f..0000000000 --- a/migrations/20220320143831_set_idle_transaction_timeout.up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- set idle_in_transaction_session_timeout to 1min - -ALTER ROLE current_user SET idle_in_transaction_session_timeout TO 60000; From 1cde8810ba89246e3cc92b0de2196471c989bebf Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Sun, 27 Mar 2022 23:34:49 +0200 Subject: [PATCH 035/102] fix: wrap error returned by GetSmsProvider (#429) --- api/otp.go | 6 +++--- api/reauthenticate.go | 6 +++--- api/recover.go | 6 +++--- api/signup.go | 6 +++--- api/user.go | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/otp.go b/api/otp.go index d001764b51..9f1f54e10b 100644 --- a/api/otp.go +++ b/api/otp.go @@ -115,9 +115,9 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { if err := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); err != nil { return err } - smsProvider, err := sms_provider.GetSmsProvider(*config) - if err != nil { - return err + smsProvider, terr := sms_provider.GetSmsProvider(*config) + if terr != nil { + return badRequestError("Error sending sms: %v", terr) } if err := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneConfirmationOtp, smsProvider); err != nil { return badRequestError("Error sending sms otp: %v", err) diff --git a/api/reauthenticate.go b/api/reauthenticate.go index 307b8b159c..9310e82724 100644 --- a/api/reauthenticate.go +++ b/api/reauthenticate.go @@ -55,9 +55,9 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { mailer := a.Mailer(ctx) return a.sendReauthenticationOtp(tx, user, mailer, config.SMTP.MaxFrequency) } else if phone != "" { - smsProvider, err := sms_provider.GetSmsProvider(*config) - if err != nil { - return err + smsProvider, terr := sms_provider.GetSmsProvider(*config) + if terr != nil { + return badRequestError("Error sending sms: %v", terr) } return a.sendPhoneConfirmation(ctx, tx, user, phone, recoveryVerification, smsProvider) } diff --git a/api/recover.go b/api/recover.go index 94de22ffe8..4122f4673c 100644 --- a/api/recover.go +++ b/api/recover.go @@ -64,9 +64,9 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { referrer := a.getReferrer(r) return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) } else if params.Phone != "" { - smsProvider, err := sms_provider.GetSmsProvider(*config) - if err != nil { - return err + smsProvider, terr := sms_provider.GetSmsProvider(*config) + if terr != nil { + return badRequestError("Error sending sms: %v", terr) } return a.sendPhoneConfirmation(ctx, tx, user, params.Phone, recoveryVerification, smsProvider) } diff --git a/api/signup.go b/api/signup.go index 462f9e7cd9..73e6b80781 100644 --- a/api/signup.go +++ b/api/signup.go @@ -160,9 +160,9 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { }); terr != nil { return terr } - smsProvider, err := sms_provider.GetSmsProvider(*config) - if err != nil { - return err + smsProvider, terr := sms_provider.GetSmsProvider(*config) + if terr != nil { + return badRequestError("Error sending confirmation sms: %v", terr) } if terr = a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneConfirmationOtp, smsProvider); terr != nil { return badRequestError("Error sending confirmation sms: %v", terr) diff --git a/api/user.go b/api/user.go index c7ab19e45f..1385acb15d 100644 --- a/api/user.go +++ b/api/user.go @@ -153,7 +153,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } else { smsProvider, terr := sms_provider.GetSmsProvider(*config) if terr != nil { - return terr + return badRequestError("Error sending sms: %v", terr) } if terr := a.sendPhoneConfirmation(ctx, tx, user, params.Phone, phoneChangeVerification, smsProvider); terr != nil { return internalServerError("Error sending phone change otp").WithInternalError(terr) From b2968496a67a72203e0e722e74505c9fd595e0fb Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 30 Mar 2022 01:39:16 +0200 Subject: [PATCH 036/102] fix: reauthenticate bugs (#431) --- api/phone_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++ api/reauthenticate.go | 10 ++-- api/recover.go | 35 ++++---------- api/user_test.go | 4 +- 4 files changed, 123 insertions(+), 33 deletions(-) diff --git a/api/phone_test.go b/api/phone_test.go index 9eda8027ca..878e6256ef 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -1,8 +1,15 @@ package api import ( + "bytes" "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" "testing" + "time" "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" @@ -117,3 +124,103 @@ func (ts *PhoneTestSuite) TestSendPhoneConfirmation() { }) } } + +func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() { + u, err := models.FindUserByPhoneAndAudience(ts.API.db, ts.instanceID, "123456789", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + now := time.Now() + u.PhoneConfirmedAt = &now + require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + cases := []struct { + desc string + endpoint string + method string + header string + body map[string]string + expected map[string]interface{} + }{ + { + "Signup", + "/signup", + http.MethodPost, + "", + map[string]string{ + "phone": "1234567890", + "password": "testpassword", + }, + map[string]interface{}{ + "code": http.StatusBadRequest, + "message": "Error sending confirmation sms:", + }, + }, + { + "Sms OTP", + "/otp", + http.MethodPost, + "", + map[string]string{ + "phone": "123456789", + }, + map[string]interface{}{ + "code": http.StatusBadRequest, + "message": "Error sending sms:", + }, + }, + { + "Phone change", + "/user", + http.MethodPut, + token, + map[string]string{ + "phone": "111111111", + }, + map[string]interface{}{ + "code": http.StatusBadRequest, + "message": "Error sending sms:", + }, + }, + { + "Reauthenticate", + "/reauthenticate", + http.MethodGet, + "", + nil, + map[string]interface{}{ + "code": http.StatusBadRequest, + "message": "Error sending sms:", + }, + }, + } + + smsProviders := []string{"twilio", "messagebird", "textlocal", "vonage"} + ts.Config.External.Phone.Enabled = true + ts.Config.Sms.Twilio.AccountSid = "" + ts.Config.Sms.Messagebird.AccessKey = "" + ts.Config.Sms.Textlocal.ApiKey = "" + ts.Config.Sms.Vonage.ApiKey = "" + for _, c := range cases { + for _, provider := range smsProviders { + ts.Config.Sms.Provider = provider + desc := fmt.Sprintf("[%v] %v", provider, c.desc) + ts.Run(desc, func() { + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body)) + + req := httptest.NewRequest(c.method, "http://localhost"+c.endpoint, &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), c.expected["code"], w.Code) + + body := w.Body.String() + require.True(ts.T(), strings.Contains(body, c.expected["message"].(string))) + }) + } + } +} diff --git a/api/reauthenticate.go b/api/reauthenticate.go index 9310e82724..c75360aa16 100644 --- a/api/reauthenticate.go +++ b/api/reauthenticate.go @@ -12,6 +12,8 @@ import ( "github.com/netlify/gotrue/storage" ) +const InvalidNonceMessage = "Nonce has expired or is invalid" + // Reauthenticate sends a reauthentication otp to either the user's email or phone func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() @@ -59,7 +61,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { if terr != nil { return badRequestError("Error sending sms: %v", terr) } - return a.sendPhoneConfirmation(ctx, tx, user, phone, recoveryVerification, smsProvider) + return a.sendPhoneConfirmation(ctx, tx, user, phone, phoneReauthenticationOtp, smsProvider) } return nil }) @@ -67,7 +69,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { if errors.Is(err, MaxFrequencyLimitError) { return tooManyRequestsError("For security purposes, you can only request this once every 60 seconds") } - return internalServerError("Reauthentication failed.").WithInternalError(err) + return err } return sendJSON(w, http.StatusOK, make(map[string]string)) @@ -76,7 +78,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { // verifyReauthentication checks if the nonce provided is valid func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, config *conf.Configuration, user *models.User) error { if user.ReauthenticationToken == "" || user.ReauthenticationSentAt == nil { - return unauthorizedError("Requires reauthentication") + return badRequestError(InvalidNonceMessage) } var isValid bool if user.GetEmail() != "" { @@ -89,7 +91,7 @@ func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, confi return unprocessableEntityError("Reauthentication requires an email or a phone number") } if !isValid { - return badRequestError("Nonce has expired or is invalid") + return badRequestError(InvalidNonceMessage) } if err := user.ConfirmReauthentication(tx); err != nil { return internalServerError("Error during reauthentication").WithInternalError(err) diff --git a/api/recover.go b/api/recover.go index 4122f4673c..1701f5b2d4 100644 --- a/api/recover.go +++ b/api/recover.go @@ -5,7 +5,6 @@ import ( "errors" "net/http" - "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" ) @@ -13,7 +12,6 @@ import ( // RecoverParams holds the parameters for a password recovery request type RecoverParams struct { Email string `json:"email"` - Phone string `json:"phone"` } // Recover sends a recovery email @@ -28,25 +26,17 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read verification params: %v", err) } - if params.Email == "" && params.Phone == "" { - return unprocessableEntityError("Password recovery requires an email or a phone number") + if params.Email == "" { + return unprocessableEntityError("Password recovery requires an email") } var user *models.User aud := a.requestAud(ctx, r) recoverErrorMessage := "If a user exists, you will receive an email with instructions on how to reset your password." - if params.Email != "" { - if err := a.validateEmail(ctx, params.Email); err != nil { - return err - } - user, err = models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud) - } else if params.Phone != "" { - params.Phone, err = a.validatePhone(params.Phone) - if err != nil { - return err - } - user, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, aud) + if err := a.validateEmail(ctx, params.Email); err != nil { + return err } + user, err = models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud) if err != nil { if models.IsNotFoundError(err) { @@ -59,18 +49,9 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil { return terr } - if params.Email != "" { - mailer := a.Mailer(ctx) - referrer := a.getReferrer(r) - return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) - } else if params.Phone != "" { - smsProvider, terr := sms_provider.GetSmsProvider(*config) - if terr != nil { - return badRequestError("Error sending sms: %v", terr) - } - return a.sendPhoneConfirmation(ctx, tx, user, params.Phone, recoveryVerification, smsProvider) - } - return nil + mailer := a.Mailer(ctx) + referrer := a.getReferrer(r) + return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/api/user_test.go b/api/user_test.go index 585799c736..f54d7799cd 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -224,7 +224,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { "newpassword123", "123456", true, - expected{code: http.StatusUnauthorized, isAuthenticated: false}, + expected{code: http.StatusBadRequest, isAuthenticated: false}, }, } @@ -244,7 +244,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { // Setup response recorder w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), w.Code, c.expected.code) + require.Equal(ts.T(), c.expected.code, w.Code) // Request body u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) From b4d9ca3f23b216569389b9bc360bd4a67e9d9359 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 31 Mar 2022 01:07:46 +0200 Subject: [PATCH 037/102] fix: user email & phone update (#432) * fix: update phone should send otp to new phone * fix: check if phone update is the same as current * fix: handle email change via otp * fix invalid phone change otp test * add test for new phone number exists already --- api/phone.go | 11 ++++++----- api/reauthenticate.go | 7 ++----- api/user.go | 2 +- api/user_test.go | 12 ++++++++++++ api/verify.go | 44 +++++++++++++++++++++++++++++++------------ api/verify_test.go | 12 ++++++++---- models/user.go | 36 ++++++++++++++++++++++++++++++++--- 7 files changed, 94 insertions(+), 30 deletions(-) diff --git a/api/phone.go b/api/phone.go index aa6833f16f..98bcf9f833 100644 --- a/api/phone.go +++ b/api/phone.go @@ -48,21 +48,22 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, var token *string var sentAt *time.Time - var tokenDbField, sentAtDbField string + includeFields := []string{} switch otpType { case phoneChangeVerification: token = &user.PhoneChangeToken sentAt = user.PhoneChangeSentAt - tokenDbField, sentAtDbField = "phone_change_token", "phone_change_sent_at" + user.PhoneChange = phone + includeFields = append(includeFields, "phone_change", "phone_change_token", "phone_change_sent_at") case phoneConfirmationOtp: token = &user.ConfirmationToken sentAt = user.ConfirmationSentAt - tokenDbField, sentAtDbField = "confirmation_token", "confirmation_sent_at" + includeFields = append(includeFields, "confirmation_token", "confirmation_sent_at") case phoneReauthenticationOtp: token = &user.ReauthenticationToken sentAt = user.ReauthenticationSentAt - tokenDbField, sentAtDbField = "reauthentication_token", "reauthentication_sent_at" + includeFields = append(includeFields, "reauthentication_token", "reauthentication_sent_at") default: return internalServerError("invalid otp type") } @@ -101,5 +102,5 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, user.ReauthenticationSentAt = &now } - return errors.Wrap(tx.UpdateOnly(user, tokenDbField, sentAtDbField), "Database error updating user for confirmation") + return errors.Wrap(tx.UpdateOnly(user, includeFields...), "Database error updating user for confirmation") } diff --git a/api/reauthenticate.go b/api/reauthenticate.go index c75360aa16..3d8743c99c 100644 --- a/api/reauthenticate.go +++ b/api/reauthenticate.go @@ -3,7 +3,6 @@ package api import ( "errors" "net/http" - "time" "github.com/gofrs/uuid" "github.com/netlify/gotrue/api/sms_provider" @@ -82,11 +81,9 @@ func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, confi } var isValid bool if user.GetEmail() != "" { - mailerOtpExpiresAt := time.Second * time.Duration(config.Mailer.OtpExp) - isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt.Add(mailerOtpExpiresAt)) + isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Mailer.OtpExp) } else if user.GetPhone() != "" { - smsOtpExpiresAt := time.Second * time.Duration(config.Sms.OtpExp) - isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt.Add(smsOtpExpiresAt)) + isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Sms.OtpExp) } else { return unprocessableEntityError("Reauthentication requires an email or a phone number") } diff --git a/api/user.go b/api/user.go index 1385acb15d..389414b4d5 100644 --- a/api/user.go +++ b/api/user.go @@ -137,7 +137,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if params.Phone != "" { + if params.Phone != "" && params.Phone != user.GetPhone() { params.Phone, err = a.validatePhone(params.Phone) if err != nil { return err diff --git a/api/user_test.go b/api/user_test.go index f54d7799cd..8acc6b6a45 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -138,6 +138,11 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) + existingUser, err := models.NewUser(ts.instanceID, "", "", ts.Config.JWT.Aud, nil) + existingUser.Phone = "22222222" + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Create(existingUser)) + cases := []struct { desc string userData map[string]string @@ -148,6 +153,13 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { map[string]string{ "phone": "123456789", }, + http.StatusOK, + }, + { + "New phone number exists already", + map[string]string{ + "phone": "22222222", + }, http.StatusUnprocessableEntity, }, { diff --git a/api/verify.go b/api/verify.go index 844ef85bee..d3e1bf1698 100644 --- a/api/verify.go +++ b/api/verify.go @@ -34,6 +34,9 @@ const ( singleConfirmation ) +// Only applicable when SECURE_EMAIL_CHANGE_ENABLED +const singleConfirmationAccepted = "Confirmation link accepted. Please proceed to confirm link sent to the other email" + // VerifyParams are the parameters the Verify endpoint accepts type VerifyParams struct { Type string `json:"type"` @@ -95,7 +98,7 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { user, terr = a.emailChangeVerify(ctx, tx, params, user) if user == nil && terr == nil { // when double confirmation is required - rurl := a.prepRedirectURL("Confirmation link accepted. Please proceed to confirm link sent to the other email", params.RedirectTo) + rurl := a.prepRedirectURL(singleConfirmationAccepted, params.RedirectTo) http.Redirect(w, r, rurl, http.StatusSeeOther) return nil } @@ -150,6 +153,12 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { } http.Redirect(w, r, rurl, http.StatusSeeOther) case "POST": + if token == nil && params.Type == emailChangeVerification { + return sendJSON(w, http.StatusOK, map[string]string{ + "msg": singleConfirmationAccepted, + "code": strconv.Itoa(http.StatusOK), + }) + } return sendJSON(w, http.StatusOK, token) } @@ -355,12 +364,22 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, if err != nil { return nil, err } - user, err = models.FindUserByPhoneAndAudience(conn, instanceID, params.Phone, aud) + switch params.Type { + case phoneChangeVerification: + user, err = models.FindUserByPhoneChangeAndAudience(conn, instanceID, params.Phone, aud) + case smsVerification: + user, err = models.FindUserByPhoneAndAudience(conn, instanceID, params.Phone, aud) + } } else if isEmailOtpVerification(params) { if err := a.validateEmail(ctx, params.Email); err != nil { return nil, unprocessableEntityError("Invalid email format").WithInternalError(err) } - user, err = models.FindUserByEmailAndAudience(conn, instanceID, params.Email, aud) + switch params.Type { + case emailChangeVerification: + user, err = models.FindUserForEmailChange(conn, instanceID, params.Email, params.Token, aud, config.Mailer.SecureEmailChangeEnabled) + default: + user, err = models.FindUserByEmailAndAudience(conn, instanceID, params.Email, aud) + } } else { return nil, badRequestError("Only an email address or phone number should be provided on verify, not both.") } @@ -377,25 +396,22 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, } var isValid bool - mailerOtpExpiresAt := time.Second * time.Duration(config.Mailer.OtpExp) - smsOtpExpiresAt := time.Second * time.Duration(config.Sms.OtpExp) switch params.Type { case signupVerification, inviteVerification: - isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt.Add(mailerOtpExpiresAt)) + isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt, config.Mailer.OtpExp) case recoveryVerification, magicLinkVerification: - isValid = isOtpValid(params.Token, user.RecoveryToken, user.RecoverySentAt.Add(mailerOtpExpiresAt)) + isValid = isOtpValid(params.Token, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp) case emailChangeVerification: - expiresAt := user.EmailChangeSentAt.Add(mailerOtpExpiresAt) - isValid = isOtpValid(params.Token, user.EmailChangeTokenCurrent, expiresAt) || isOtpValid(params.Token, user.EmailChangeTokenNew, expiresAt) + isValid = isOtpValid(params.Token, user.EmailChangeTokenCurrent, user.EmailChangeSentAt, config.Mailer.OtpExp) || isOtpValid(params.Token, user.EmailChangeTokenNew, user.EmailChangeSentAt, config.Mailer.OtpExp) if !isValid { // reset email confirmation status user.EmailChangeConfirmStatus = zeroConfirmation err = conn.UpdateOnly(user, "email_change_confirm_status") } case phoneChangeVerification: - isValid = isOtpValid(params.Token, user.PhoneChangeToken, user.PhoneChangeSentAt.Add(smsOtpExpiresAt)) + isValid = isOtpValid(params.Token, user.PhoneChangeToken, user.PhoneChangeSentAt, config.Sms.OtpExp) case smsVerification: - isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt.Add(smsOtpExpiresAt)) + isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt, config.Sms.OtpExp) } if !isValid || err != nil { @@ -405,7 +421,11 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, } // isOtpValid checks the actual otp sent against the expected otp and ensures that it's within the valid window -func isOtpValid(actual, expected string, expiresAt time.Time) bool { +func isOtpValid(actual, expected string, sentAt *time.Time, otpExp uint) bool { + if expected == "" || sentAt == nil { + return false + } + expiresAt := sentAt.Add(time.Second * time.Duration(otpExp)) return time.Now().Before(expiresAt) && (actual == expected) } diff --git a/api/verify_test.go b/api/verify_test.go index 597fbd3b6d..cdb69e1d54 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -192,10 +192,11 @@ func (ts *VerifyTestSuite) TestExpiredConfirmationToken() { func (ts *VerifyTestSuite) TestInvalidOtp() { u, err := models.FindUserByPhoneAndAudience(ts.API.db, ts.instanceID, "12345678", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - u.ConfirmationToken = "123456" - u.PhoneChangeToken = "123456" sentTime := time.Now().Add(-48 * time.Hour) + u.ConfirmationToken = "123456" u.ConfirmationSentAt = &sentTime + u.PhoneChange = "22222222" + u.PhoneChangeToken = "123456" u.PhoneChangeSentAt = &sentTime require.NoError(ts.T(), ts.API.db.Update(u)) @@ -241,7 +242,7 @@ func (ts *VerifyTestSuite) TestInvalidOtp() { body: map[string]interface{}{ "type": phoneChangeVerification, "token": "invalid_otp", - "phone": u.GetPhone(), + "phone": u.PhoneChange, }, expected: expectedResponse, }, @@ -544,6 +545,9 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) + u.PhoneChange = "1234567890" + require.NoError(ts.T(), ts.API.db.Update(u)) + type expected struct { code int } @@ -606,7 +610,7 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { body: map[string]interface{}{ "type": phoneChangeVerification, "token": "123456", - "phone": "12345678", + "phone": u.PhoneChange, }, expected: expectedResponse, }, diff --git a/models/user.go b/models/user.go index 09de86ecb3..7af1e4d0e2 100644 --- a/models/user.go +++ b/models/user.go @@ -444,9 +444,39 @@ func FindUsersInAudience(tx *storage.Connection, instanceID uuid.UUID, aud strin return users, err } -// FindUserWithPhoneAndPhoneChangeToken finds a user with the matching phone and phone change token -func FindUserWithPhoneAndPhoneChangeToken(tx *storage.Connection, phone, token string) (*User, error) { - return findUser(tx, "phone = ? and phone_change_token = ?", phone, token) +// FindUserByEmailChangeCurrentAndAudience finds a user with the matching email change and audience. +func FindUserByEmailChangeCurrentAndAudience(tx *storage.Connection, instanceID uuid.UUID, email, token, aud string) (*User, error) { + return findUser( + tx, + "instance_id = ? and LOWER(email) = ? and email_change_token_current = ? and aud = ?", + instanceID, strings.ToLower(email), token, aud, + ) +} + +// FindUserByEmailChangeNewAndAudience finds a user with the matching email change and audience. +func FindUserByEmailChangeNewAndAudience(tx *storage.Connection, instanceID uuid.UUID, email, token, aud string) (*User, error) { + return findUser( + tx, + "instance_id = ? and LOWER(email_change) = ? and email_change_token_new = ? and aud = ?", + instanceID, strings.ToLower(email), token, aud, + ) +} + +// FindUserForEmailChange finds a user requesting for an email change +func FindUserForEmailChange(tx *storage.Connection, instanceID uuid.UUID, email, token, aud string, secureEmailChangeEnabled bool) (*User, error) { + if secureEmailChangeEnabled { + if user, err := FindUserByEmailChangeCurrentAndAudience(tx, instanceID, email, token, aud); err == nil { + return user, err + } else if !IsNotFoundError(err) { + return nil, err + } + } + return FindUserByEmailChangeNewAndAudience(tx, instanceID, email, token, aud) +} + +// FindUserByPhoneChangeAndAudience finds a user with the matching phone change and audience. +func FindUserByPhoneChangeAndAudience(tx *storage.Connection, instanceID uuid.UUID, phone, aud string) (*User, error) { + return findUser(tx, "instance_id = ? and phone_change = ? and aud = ?", instanceID, phone, aud) } // IsDuplicatedEmail returns whether a user exists with a matching email and audience. From c83d01e583649a18d7eca962a3585febd34f6eac Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 31 Mar 2022 15:46:39 +0200 Subject: [PATCH 038/102] fix: use email change template for current and new (#433) --- mailer/template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailer/template.go b/mailer/template.go index 64673edb5a..3380ced721 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -153,7 +153,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) Address: user.EmailChange, Token: user.EmailChangeTokenNew, Subject: string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")), - Template: m.Config.Mailer.Templates.Confirmation, + Template: m.Config.Mailer.Templates.EmailChange, }, } From 51313fc17627c30ffd8c6e4053666cfb930ecc88 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 31 Mar 2022 22:47:00 +0200 Subject: [PATCH 039/102] fix bugs in verify --- api/verify.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/verify.go b/api/verify.go index d3e1bf1698..9d34afeed4 100644 --- a/api/verify.go +++ b/api/verify.go @@ -61,11 +61,19 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { params.Password = "" params.Type = r.FormValue("type") params.RedirectTo = a.getRedirectURLOrReferrer(r, r.FormValue("redirect_to")) + if params.Type == smsVerification || params.Type == phoneChangeVerification { + rurl := a.prepErrorRedirectURL(badRequestError("GET /verify does not support sms or phone_change as verification types"), r, params.RedirectTo) + http.Redirect(w, r, rurl, http.StatusSeeOther) + return nil + } case "POST": jsonDecoder := json.NewDecoder(r.Body) if err := jsonDecoder.Decode(params); err != nil { return badRequestError("Could not read verification params: %v", err) } + if params.Email == "" && params.Phone == "" { + return badRequestError("POST /verify requires either an email or phone") + } params.RedirectTo = a.getRedirectURLOrReferrer(r, params.RedirectTo) default: unprocessableEntityError("Only GET and POST methods are supported.") @@ -355,6 +363,8 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, user, err = models.FindUserByRecoveryToken(conn, params.Token) case emailChangeVerification: user, err = models.FindUserByEmailChangeToken(conn, params.Token) + default: + return nil, badRequestError("Invalid email verification type") } } else if isPhoneOtpVerification(params) { if params.Phone == "" { @@ -369,6 +379,8 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, user, err = models.FindUserByPhoneChangeAndAudience(conn, instanceID, params.Phone, aud) case smsVerification: user, err = models.FindUserByPhoneAndAudience(conn, instanceID, params.Phone, aud) + default: + return nil, badRequestError("Invalid sms verification type") } } else if isEmailOtpVerification(params) { if err := a.validateEmail(ctx, params.Email); err != nil { From 1548ccd46cefe54e670ea3ac11b6fcb0ebce698b Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 1 Apr 2022 12:17:45 +0200 Subject: [PATCH 040/102] fix: add rate limit env vars --- api/api.go | 8 ++++---- conf/configuration.go | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/api.go b/api/api.go index ebe1f6b1c8..5a0bbf83f7 100644 --- a/api/api.go +++ b/api/api.go @@ -123,15 +123,15 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.With(sharedLimiter).With(api.verifyCaptcha).Post("/otp", api.Otp) r.With(api.limitHandler( - // Allow requests at a rate of 30 per 5 minutes. - tollbooth.NewLimiter(30.0/(60*5), &limiter.ExpirableOptions{ + // Allow requests at the specified rate per 5 minutes. + tollbooth.NewLimiter(api.config.RateLimitToken/(60*5), &limiter.ExpirableOptions{ DefaultExpirationTTL: time.Hour, }).SetBurst(30), )).Post("/token", api.Token) r.With(api.limitHandler( - // Allow requests at a rate of 30 per 5 minutes. - tollbooth.NewLimiter(30.0/(60*5), &limiter.ExpirableOptions{ + // Allow requests at the specified rate per 5 minutes. + tollbooth.NewLimiter(api.config.RateLimitVerify/(60*5), &limiter.ExpirableOptions{ DefaultExpirationTTL: time.Hour, }).SetBurst(30), )).Route("/verify", func(r *router) { diff --git a/conf/configuration.go b/conf/configuration.go index 23aacf3207..938d0a2216 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -74,6 +74,8 @@ type GlobalConfiguration struct { SMTP SMTPConfiguration RateLimitHeader string `split_words:"true"` RateLimitEmailSent float64 `split_words:"true" default:"30"` + RateLimitVerify float64 `split_words:"true" default:"30"` + RateLimitToken float64 `split_words:"true" default:"30"` } // EmailContentConfiguration holds the configuration for emails, both subjects and template URLs. From a47bfadaa47cc517e00901d10632cffdd8ded3fc Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 1 Apr 2022 12:18:17 +0200 Subject: [PATCH 041/102] change expiredTokenError to return unauthorized --- api/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/errors.go b/api/errors.go index be77a7eea5..26dd37f022 100644 --- a/api/errors.go +++ b/api/errors.go @@ -101,7 +101,7 @@ func acceptedTokenError(fmtString string, args ...interface{}) *HTTPError { } func expiredTokenError(fmtString string, args ...interface{}) *HTTPError { - return httpError(http.StatusGone, fmtString, args...) + return httpError(http.StatusUnauthorized, fmtString, args...) } func unauthorizedError(fmtString string, args ...interface{}) *HTTPError { From 03d1708bc86e6560b10f877ebe7a642948e306be Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 1 Apr 2022 12:18:56 +0200 Subject: [PATCH 042/102] refactor verify into verifyGet and verifyPost --- api/verify.go | 174 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 70 deletions(-) diff --git a/api/verify.go b/api/verify.go index 9d34afeed4..bf1ba333e0 100644 --- a/api/verify.go +++ b/api/verify.go @@ -41,7 +41,6 @@ const singleConfirmationAccepted = "Confirmation link accepted. Please proceed t type VerifyParams struct { Type string `json:"type"` Token string `json:"token"` - Password string `json:"password"` Email string `json:"email"` Phone string `json:"phone"` RedirectTo string `json:"redirect_to"` @@ -49,39 +48,23 @@ type VerifyParams struct { // Verify exchanges a confirmation or recovery token to a refresh token func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - config := a.getConfig(ctx) - - params := &VerifyParams{} - switch r.Method { - // GET only supports signup type - case "GET": - params.Token = r.FormValue("token") - params.Password = "" - params.Type = r.FormValue("type") - params.RedirectTo = a.getRedirectURLOrReferrer(r, r.FormValue("redirect_to")) - if params.Type == smsVerification || params.Type == phoneChangeVerification { - rurl := a.prepErrorRedirectURL(badRequestError("GET /verify does not support sms or phone_change as verification types"), r, params.RedirectTo) - http.Redirect(w, r, rurl, http.StatusSeeOther) - return nil - } - case "POST": - jsonDecoder := json.NewDecoder(r.Body) - if err := jsonDecoder.Decode(params); err != nil { - return badRequestError("Could not read verification params: %v", err) - } - if params.Email == "" && params.Phone == "" { - return badRequestError("POST /verify requires either an email or phone") - } - params.RedirectTo = a.getRedirectURLOrReferrer(r, params.RedirectTo) + case http.MethodGet: + return a.verifyGet(w, r) + case http.MethodPost: + return a.verifyPost(w, r) default: - unprocessableEntityError("Only GET and POST methods are supported.") + return unprocessableEntityError("Only GET and POST methods are supported.") } +} - if params.Token == "" { - return unprocessableEntityError("Verify requires a token") - } +func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.getConfig(ctx) + params := &VerifyParams{} + params.Token = r.FormValue("token") + params.Type = r.FormValue("type") + params.RedirectTo = a.getRedirectURLOrReferrer(r, r.FormValue("redirect_to")) var ( user *models.User @@ -91,6 +74,12 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { err = a.db.Transaction(func(tx *storage.Connection) error { var terr error + if params.Token == "" { + return badRequestError("Verify requires a token") + } + if params.Type == "" { + return badRequestError("Verify requires a verification type") + } aud := a.requestAud(ctx, r) user, terr = a.verifyUserAndToken(ctx, tx, params, aud) if terr != nil { @@ -110,10 +99,8 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { http.Redirect(w, r, rurl, http.StatusSeeOther) return nil } - case smsVerification, phoneChangeVerification: - user, terr = a.smsVerify(ctx, tx, user, params.Type) default: - return unprocessableEntityError("Verify requires a verification type") + return unprocessableEntityError("Unsupported verification type") } if terr != nil { @@ -132,47 +119,99 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { }) if err != nil { - if r.Method == http.MethodPost { - // do not redirect POST requests - return err - } var herr *HTTPError if errors.As(err, &herr) { - if errors.Is(herr.InternalError, redirectWithQueryError) { - rurl := a.prepErrorRedirectURL(herr, r, params.RedirectTo) - http.Redirect(w, r, rurl, http.StatusSeeOther) - return nil - } + rurl := a.prepErrorRedirectURL(herr, r, params.RedirectTo) + http.Redirect(w, r, rurl, http.StatusSeeOther) + return nil } } - // GET requests should return to the app site after confirmation - switch r.Method { - case "GET": - rurl := params.RedirectTo - if token != nil { - q := url.Values{} - q.Set("access_token", token.Token) - q.Set("token_type", token.TokenType) - q.Set("expires_in", strconv.Itoa(token.ExpiresIn)) - q.Set("refresh_token", token.RefreshToken) - q.Set("type", params.Type) - rurl += "#" + q.Encode() - } - http.Redirect(w, r, rurl, http.StatusSeeOther) - case "POST": - if token == nil && params.Type == emailChangeVerification { - return sendJSON(w, http.StatusOK, map[string]string{ - "msg": singleConfirmationAccepted, - "code": strconv.Itoa(http.StatusOK), - }) - } - return sendJSON(w, http.StatusOK, token) + rurl := params.RedirectTo + if token != nil { + q := url.Values{} + q.Set("access_token", token.Token) + q.Set("token_type", token.TokenType) + q.Set("expires_in", strconv.Itoa(token.ExpiresIn)) + q.Set("refresh_token", token.RefreshToken) + q.Set("type", params.Type) + rurl += "#" + q.Encode() } - + http.Redirect(w, r, rurl, http.StatusSeeOther) return nil } +func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.getConfig(ctx) + params := &VerifyParams{} + jsonDecoder := json.NewDecoder(r.Body) + if err := jsonDecoder.Decode(params); err != nil { + return badRequestError("Could not read verification params: %v", err) + } + + if params.Token == "" { + return badRequestError("Verify requires a token") + } + + if params.Type == "" { + return badRequestError("Verify requires a verification type") + } + + var ( + user *models.User + err error + token *AccessTokenResponse + ) + + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + aud := a.requestAud(ctx, r) + user, terr = a.verifyUserAndToken(ctx, tx, params, aud) + if terr != nil { + return terr + } + + switch params.Type { + case signupVerification, inviteVerification: + user, terr = a.signupVerify(ctx, tx, user) + case recoveryVerification, magicLinkVerification: + user, terr = a.recoverVerify(ctx, tx, user) + case emailChangeVerification: + user, terr = a.emailChangeVerify(ctx, tx, params, user) + if user == nil && terr == nil { + return sendJSON(w, http.StatusOK, map[string]string{ + "msg": singleConfirmationAccepted, + "code": strconv.Itoa(http.StatusOK), + }) + } + case smsVerification, phoneChangeVerification: + user, terr = a.smsVerify(ctx, tx, user, params.Type) + default: + return unprocessableEntityError("Unsupported verification type") + } + + if terr != nil { + return terr + } + + token, terr = a.issueRefreshToken(ctx, tx, user) + if terr != nil { + return terr + } + + if terr = a.setCookieTokens(config, token, false, w); terr != nil { + return internalServerError("Failed to set JWT cookie. %s", terr) + } + return nil + }) + + if err != nil { + return err + } + return sendJSON(w, http.StatusOK, token) +} + func (a *API) signupVerify(ctx context.Context, conn *storage.Connection, user *models.User) (*models.User, error) { instanceID := getInstanceID(ctx) config := a.getConfig(ctx) @@ -415,11 +454,6 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, isValid = isOtpValid(params.Token, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp) case emailChangeVerification: isValid = isOtpValid(params.Token, user.EmailChangeTokenCurrent, user.EmailChangeSentAt, config.Mailer.OtpExp) || isOtpValid(params.Token, user.EmailChangeTokenNew, user.EmailChangeSentAt, config.Mailer.OtpExp) - if !isValid { - // reset email confirmation status - user.EmailChangeConfirmStatus = zeroConfirmation - err = conn.UpdateOnly(user, "email_change_confirm_status") - } case phoneChangeVerification: isValid = isOtpValid(params.Token, user.PhoneChangeToken, user.PhoneChangeSentAt, config.Sms.OtpExp) case smsVerification: From 5cd1dd81d6659efcdac85996bbad27b1b8df34e4 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 1 Apr 2022 12:19:08 +0200 Subject: [PATCH 043/102] fix verify tests --- api/verify_test.go | 52 +++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/api/verify_test.go b/api/verify_test.go index cdb69e1d54..b8104e1a78 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -141,7 +141,11 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { w = httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusSeeOther, w.Code) + + data := make(map[string]interface{}) + require.Equal(ts.T(), http.StatusOK, w.Code) + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.Equal(ts.T(), singleConfirmationAccepted, data["msg"]) u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) @@ -158,12 +162,18 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { w = httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusOK, w.Code) + data = make(map[string]interface{}) + require.Equal(ts.T(), http.StatusOK, w.Code) + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.NotNil(ts.T(), data["access_token"]) + require.NotNil(ts.T(), data["expires_in"]) + require.NotNil(ts.T(), data["refresh_token"]) + require.NotNil(ts.T(), data["user"]) // user's email should've been updated to new@example.com u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "new@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - assert.Equal(ts.T(), zeroConfirmation, u.EmailChangeConfirmStatus) + require.Equal(ts.T(), zeroConfirmation, u.EmailChangeConfirmStatus) } func (ts *VerifyTestSuite) TestExpiredConfirmationToken() { @@ -184,9 +194,15 @@ func (ts *VerifyTestSuite) TestExpiredConfirmationToken() { ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusSeeOther, w.Code) - url, err := w.Result().Location() + rurl, err := url.Parse(w.Header().Get("Location")) + require.NoError(ts.T(), err, "redirect url parse failed") + + f, err := url.ParseQuery(rurl.Fragment) require.NoError(ts.T(), err) - assert.Equal(ts.T(), "error_code=410&error_description=Token+has+expired+or+is+invalid", url.Fragment) + fmt.Println(f) + assert.Equal(ts.T(), "401", f.Get("error_code")) + assert.Equal(ts.T(), "Token has expired or is invalid", f.Get("error_description")) + assert.Equal(ts.T(), "unauthorized_client", f.Get("error")) } func (ts *VerifyTestSuite) TestInvalidOtp() { @@ -206,7 +222,7 @@ func (ts *VerifyTestSuite) TestInvalidOtp() { } expectedResponse := ResponseBody{ - Code: http.StatusGone, + Code: http.StatusUnauthorized, Msg: "Token has expired or is invalid", } @@ -491,14 +507,6 @@ func (ts *VerifyTestSuite) TestVerifyBannedUser() { Token: u.ConfirmationToken, }, }, - { - "Verify banned phone user on sms", - &VerifyParams{ - Type: "sms", - Token: u.ConfirmationToken, - Phone: u.GetPhone(), - }, - }, { "Verify banned user on recover", &VerifyParams{ @@ -527,16 +535,20 @@ func (ts *VerifyTestSuite) TestVerifyBannedUser() { var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.payload)) - req := httptest.NewRequest(http.MethodPost, "http://localhost/verify", &buffer) + requestUrl := fmt.Sprintf("http://localhost/verify?type=%v&token=%v", c.payload.Type, c.payload.Token) + req := httptest.NewRequest(http.MethodGet, requestUrl, &buffer) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusUnauthorized, w.Code) + assert.Equal(ts.T(), http.StatusSeeOther, w.Code) - b, err := ioutil.ReadAll(w.Body) + rurl, err := url.Parse(w.Header().Get("Location")) + require.NoError(ts.T(), err, "redirect url parse failed") + + f, err := url.ParseQuery(rurl.Fragment) require.NoError(ts.T(), err) - assert.Equal(ts.T(), "{\"code\":401,\"msg\":\"Error confirming user\"}", string(b)) + assert.Equal(ts.T(), "401", f.Get("error_code")) }) } } @@ -600,9 +612,7 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { "token": "123456", "email": u.GetEmail(), }, - expected: expected{ - code: http.StatusSeeOther, - }, + expected: expectedResponse, }, { desc: "Valid Phone Change OTP", From 722ababbc8e89719c57e124336e95d95a103bf5a Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 1 Apr 2022 12:21:17 +0200 Subject: [PATCH 044/102] increase rate limit on verify endpoint for test env --- hack/test.env | 1 + 1 file changed, 1 insertion(+) diff --git a/hack/test.env b/hack/test.env index 80bab4105d..d0137c9c2b 100644 --- a/hack/test.env +++ b/hack/test.env @@ -86,6 +86,7 @@ GOTRUE_EXTERNAL_SAML_ENABLED=true GOTRUE_EXTERNAL_SAML_METADATA_URL= GOTRUE_EXTERNAL_SAML_API_BASE=http://localhost GOTRUE_EXTERNAL_SAML_NAME=TestSamlName +GOTRUE_RATE_LIMIT_VERIFY="1000" GOTRUE_TRACING_ENABLED=false GOTRUE_TRACING_HOST=127.0.0.1 GOTRUE_TRACING_PORT=8126 From 0f5091f63c212b2a0a3f4e9fa5e5129a7d38ad77 Mon Sep 17 00:00:00 2001 From: Milan van Schaik Date: Sat, 16 Apr 2022 00:51:33 +0200 Subject: [PATCH 045/102] fix: adds support for wildcards in redirect URIs (#334) * feat: adds support for wildcards in redirect URIs * docs(readme): adds wildcard redirect URI info * fix: allow wildcard matching for weblinks only * fix: add separator to glob pattern * fix: move glob compilation to load config step Co-authored-by: Kang Ming --- README.md | 2 +- api/helpers.go | 10 +++++-- api/verify_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++ conf/configuration.go | 14 ++++++++-- go.mod | 1 + go.sum | 2 ++ 6 files changed, 87 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2189750349..322b8ef52c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The base URL your site is located at. Currently used in combination with other s `URI_ALLOW_LIST` - `string` -A comma separated list of URIs (e.g. "https://supabase.io/welcome,io.supabase.gotruedemo://logincallback") which are permitted as valid `redirect_to` destinations, in addition to SITE_URL. Defaults to []. +A comma separated list of URIs (e.g. "https://supabase.io/welcome,io.supabase.gotruedemo://logincallback") which are permitted as valid `redirect_to` destinations, in addition to SITE_URL. Defaults to []. Supports wildcard matching trough globbing. e.g. add `*.mydomain.com/welcome` -> `x.mydomain.com/welcome` and `y.mydomain.com/welcome` would be accepted. `OPERATOR_TOKEN` - `string` _Multi-instance mode only_ diff --git a/api/helpers.go b/api/helpers.go index c8cfc2f353..d626287107 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptrace" "net/url" + "strings" "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" @@ -124,8 +125,13 @@ func isRedirectURLValid(config *conf.Configuration, redirectURL string) bool { } // For case when user came from mobile app or other permitted resource - redirect back - for _, uri := range config.URIAllowList { - if redirectURL == uri { + for uri, g := range config.URIAllowListMap { + // Only allow wildcard matching if url scheme is http(s) + if strings.HasPrefix(uri, "http") || strings.HasPrefix(uri, "https") { + if g.Match(redirectURL) { + return true + } + } else if redirectURL == uri { return true } } diff --git a/api/verify_test.go b/api/verify_test.go index b8104e1a78..896a0f1be7 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -440,6 +440,69 @@ func (ts *VerifyTestSuite) TestVerifySignupWithredirectURLContainedPath() { requestredirectURL: "http://localhost:3000/docs", expectedredirectURL: "https://someapp-something.codemagic.app/#/", }, + { + desc: "same wildcard site url and redirect url in allow list", + siteURL: "http://sub.test.dev:3000/#/", + uriAllowList: []string{"http://*.test.dev:3000"}, + requestredirectURL: "http://sub.test.dev:3000/#/", + expectedredirectURL: "http://sub.test.dev:3000/#/", + }, + { + desc: "different wildcard site url and redirect url in allow list", + siteURL: "http://sub.test.dev/#/", + uriAllowList: []string{"http://*.other.dev:3000"}, + requestredirectURL: "http://sub.other.dev:3000", + expectedredirectURL: "http://sub.other.dev:3000", + }, + { + desc: "different wildcard site url and redirect url not in allow list", + siteURL: "http://test.dev:3000/#/", + uriAllowList: []string{"http://*.allowed.dev:3000"}, + requestredirectURL: "http://sub.test.dev:3000/#/", + expectedredirectURL: "http://test.dev:3000/#/", + }, + { + desc: "exact mobile deep link redirect url in allow list", + siteURL: "http://test.dev:3000/#/", + uriAllowList: []string{"twitter://timeline"}, + requestredirectURL: "twitter://timeline", + expectedredirectURL: "twitter://timeline", + }, + { + desc: "wildcard mobile deep link redirect url in allow list", + siteURL: "http://test.dev:3000/#/", + uriAllowList: []string{"com.mobile.*"}, + requestredirectURL: "com.mobile.app", + expectedredirectURL: "http://test.dev:3000/#/", + }, + { + desc: "redirect respects . separator", + siteURL: "http://localhost:3000", + uriAllowList: []string{"http://*.*.dev:3000"}, + requestredirectURL: "http://foo.bar.dev:3000", + expectedredirectURL: "http://foo.bar.dev:3000", + }, + { + desc: "redirect does not respect . separator", + siteURL: "http://localhost:3000", + uriAllowList: []string{"http://*.dev:3000"}, + requestredirectURL: "http://foo.bar.dev:3000", + expectedredirectURL: "http://localhost:3000", + }, + { + desc: "redirect respects / separator in url subdirectory", + siteURL: "http://localhost:3000", + uriAllowList: []string{"http://test.dev:3000/*/*"}, + requestredirectURL: "http://test.dev:3000/bar/foo", + expectedredirectURL: "http://test.dev:3000/bar/foo", + }, + { + desc: "redirect does not respect / separator in url subdirectory", + siteURL: "http://localhost:3000", + uriAllowList: []string{"http://test.dev:3000/*"}, + requestredirectURL: "http://test.dev:3000/bar/foo", + expectedredirectURL: "http://localhost:3000", + }, } for _, tC := range testCases { @@ -448,6 +511,7 @@ func (ts *VerifyTestSuite) TestVerifySignupWithredirectURLContainedPath() { ts.Config.SiteURL = tC.siteURL redirectURL := tC.requestredirectURL ts.Config.URIAllowList = tC.uriAllowList + ts.Config.ApplyDefaults() // set verify token to user as it actual do in magic link method u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) diff --git a/conf/configuration.go b/conf/configuration.go index 938d0a2216..c99e840e61 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/gobwas/glob" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" ) @@ -185,8 +186,9 @@ type SecurityConfiguration struct { // Configuration holds all the per-instance configuration. type Configuration struct { - SiteURL string `json:"site_url" split_words:"true" required:"true"` - URIAllowList []string `json:"uri_allow_list" split_words:"true"` + SiteURL string `json:"site_url" split_words:"true" required:"true"` + URIAllowList []string `json:"uri_allow_list" split_words:"true"` + URIAllowListMap map[string]glob.Glob PasswordMinLength int `json:"password_min_length" split_words:"true"` JWT JWTConfiguration `json:"jwt"` SMTP SMTPConfiguration `json:"smtp"` @@ -341,7 +343,13 @@ func (config *Configuration) ApplyDefaults() { if config.URIAllowList == nil { config.URIAllowList = []string{} } - + if config.URIAllowList != nil { + config.URIAllowListMap = make(map[string]glob.Glob) + for _, uri := range config.URIAllowList { + g := glob.MustCompile(uri, '.', '/') + config.URIAllowListMap[uri] = g + } + } if config.PasswordMinLength < defaultMinPasswordLength { config.PasswordMinLength = defaultMinPasswordLength } diff --git a/go.mod b/go.mod index e8130727e6..c4c055d103 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/gobuffalo/plush/v4 v4.1.0 // indirect github.com/gobuffalo/pop/v5 v5.3.3 github.com/gobuffalo/validate/v3 v3.3.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/uuid v4.0.0+incompatible github.com/golang-jwt/jwt v3.2.1+incompatible github.com/gorilla/securecookie v1.1.1 diff --git a/go.sum b/go.sum index f248244695..9e930f3db4 100644 --- a/go.sum +++ b/go.sum @@ -169,6 +169,8 @@ github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwy github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= github.com/gobuffalo/validate/v3 v3.3.0 h1:j++FFx9gtjTmIQeI9xlaIDZ0nV4x8YQZz4RJAlZNUxg= github.com/gobuffalo/validate/v3 v3.3.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= From a67a77dc300d6701511fea4190226c28b9c37477 Mon Sep 17 00:00:00 2001 From: Daniel Mossaband Date: Fri, 15 Apr 2022 16:32:38 -0700 Subject: [PATCH 046/102] fix: Only require nonce in id_token when also passed in body (#430) --- api/token.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/api/token.go b/api/token.go index a20321825e..16f0d396ce 100644 --- a/api/token.go +++ b/api/token.go @@ -11,7 +11,7 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - jwt "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/metering" "github.com/netlify/gotrue/models" @@ -348,8 +348,8 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return badRequestError("Could not read id token grant params: %v", err) } - if params.IdToken == "" || params.Nonce == "" { - return oauthError("invalid request", "id_token and nonce required") + if params.IdToken == "" { + return oauthError("invalid request", "id_token required") } if params.Provider == "" && (params.ClientID == "" || params.Issuer == "") { @@ -379,14 +379,17 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return err } - // verify nonce to mitigate replay attacks hashedNonce, ok := claims["nonce"] - if !ok { - return oauthError("invalid request", "missing nonce in id_token") + if (!ok && params.Nonce != "") || (ok && params.Nonce == "") { + return oauthError("invalid request", "Passed nonce and nonce in id_token should either both exist or not.") } - hash := fmt.Sprintf("%x", sha256.Sum256([]byte(params.Nonce))) - if hash != hashedNonce.(string) { - return oauthError("invalid nonce", "").WithInternalMessage("Possible abuse attempt: %v", r) + + if ok && params.Nonce != "" { + // verify nonce to mitigate replay attacks + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(params.Nonce))) + if hash != hashedNonce.(string) { + return oauthError("invalid nonce", "").WithInternalMessage("Possible abuse attempt: %v", r) + } } sub, ok := claims["sub"].(string) From 359c66dcb9fbfe28fe58cc51eb38f61ade18e989 Mon Sep 17 00:00:00 2001 From: Nick Neisen Date: Sun, 17 Apr 2022 11:55:23 -0600 Subject: [PATCH 047/102] Add dev containers with hot reload --- Dockerfile.dev | 22 ++++++++++++++++++++++ Makefile | 22 +++++++++++++++++++++- docker-compose-dev.yml | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.dev create mode 100644 docker-compose-dev.yml diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000000..a21af29314 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,22 @@ +FROM golang:1.17-alpine +ENV GO111MODULE=on +ENV CGO_ENABLED=0 +ENV GOOS=linux + +RUN apk add --no-cache make git bash + +WORKDIR /go/src/github.com/netlify/gotrue + +# Pulling dependencies +COPY ./Makefile ./go.* ./ + +# Production dependencies +RUN make deps + +# Development dependences +RUN go get -u github.com/Thatooine/go-test-html-report +RUN go get github.com/githubnemo/CompileDaemon +RUN go install github.com/githubnemo/CompileDaemon + +RUN adduser -D -u 1000 netlify +USER netlify diff --git a/Makefile b/Makefile index c59f4fb4b9..1089e95c94 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ build: ## Build the binary. GOOS=linux GOARCH=arm64 go build $(FLAGS) -o gotrue-arm64 deps: ## Install dependencies. - @go install github.com/gobuffalo/pop/soda@latest + @go install github.com/gobuffalo/pop/soda@latest @go install golang.org/x/lint/golint@latest @go mod download @@ -33,3 +33,23 @@ test: ## Run tests. vet: # Vet the code go vet $(CHECK_FILES) + + +# Run the development containers +dev: + docker-compose -f docker-compose-dev.yml up + +# Run the tests using the development containers +docker-test: + docker-compose -f docker-compose-dev.yml up -d postgres + docker-compose -f docker-compose-dev.yml run gotrue sh -c "./hack/migrate.sh postgres" + docker-compose -f docker-compose-dev.yml run gotrue sh -c "make test" + docker-compose -f docker-compose-dev.yml down -v + +# Remove the development containers and volumes +docker-build: + docker-compose -f docker-compose-dev.yml build --no-cache + +# Remove the development containers and volumes +docker-clean: + docker-compose -f docker-compose-dev.yml rm -fsv diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000000..1d8f140d88 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,32 @@ +version: "3.9" +services: + gotrue: + container_name: gotrue + depends_on: + - postgres + build: + context: ./ + dockerfile: Dockerfile.dev + # Not sure why ports aren't working. Using host as a hack + # ports: + # - "9999:9999" + network_mode: "host" + environment: + - GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/netlify/gotrue/migrations + volumes: + - ./:/go/src/github.com/netlify/gotrue + command: CompileDaemon --build="make build" --directory=/go/src/github.com/netlify/gotrue --recursive=true -pattern=(.+\.go|.+\.env) -exclude=gotrue -exclude=gotrue-arm64 --command=/go/src/github.com/netlify/gotrue/gotrue + postgres: + image: postgres:13 + container_name: postgres + network_mode: "host" + volumes: + - postgres_data:/var/lib/postgresql/data + - ${PWD}/hack/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=root + - POSTGRES_DB=postgres + +volumes: + postgres_data: From dd54189cbca01ad106f0e135000e37818aab1e39 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 18 Apr 2022 09:08:23 -0700 Subject: [PATCH 048/102] fix: validate email & phone number in shouldCreateUser (#448) --- api/otp.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api/otp.go b/api/otp.go index 9f1f54e10b..395baa95f1 100644 --- a/api/otp.go +++ b/api/otp.go @@ -31,6 +31,9 @@ func (a *API) Otp(w http.ResponseWriter, r *http.Request) error { CreateUser: true, } body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } jsonDecoder := json.NewDecoder(bytes.NewReader(body)) if err = jsonDecoder.Decode(params); err != nil { return badRequestError("Could not read verification params: %v", err) @@ -41,8 +44,10 @@ func (a *API) Otp(w http.ResponseWriter, r *http.Request) error { r.Body = ioutil.NopCloser(strings.NewReader(string(body))) - if !a.shouldCreateUser(r, params) { + if ok, err := a.shouldCreateUser(r, params); !ok { return badRequestError("Signups not allowed for otp") + } else if err != nil { + return err } if params.Email != "" { @@ -132,21 +137,28 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, make(map[string]string)) } -func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) bool { +func (a *API) shouldCreateUser(r *http.Request, params *OtpParams) (bool, error) { if !params.CreateUser { ctx := r.Context() instanceID := getInstanceID(ctx) aud := a.requestAud(ctx, r) var err error if params.Email != "" { + if err := a.validateEmail(ctx, params.Email); err != nil { + return false, err + } _, err = models.FindUserByEmailAndAudience(a.db, instanceID, params.Email, aud) } else if params.Phone != "" { + params.Phone, err = a.validatePhone(params.Phone) + if err != nil { + return false, err + } _, err = models.FindUserByPhoneAndAudience(a.db, instanceID, params.Phone, aud) } if err != nil && models.IsNotFoundError(err) { - return false + return false, nil } } - return true + return true, nil } From ecc55ab7bf734593a0f913f61c1c8590596f717d Mon Sep 17 00:00:00 2001 From: Nick Neisen Date: Mon, 18 Apr 2022 21:57:37 -0600 Subject: [PATCH 049/102] Add docs and cleanup --- CONTRIBUTING.md | 42 ++++++++++++++++++++++++++++++++++++++++++ Makefile | 43 +++++++++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3003d492c4..0421ac48dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,48 @@ Docs aren't perfect and so we're here to help. If you're stuck on setup for more Please help us keep all our projects open and inclusive. Kindly follow our [Code of Conduct](<(CODE_OF_CONDUCT.md)>) to keep the ecosystem healthy and friendly for all. +## Quick Start + +GoTrue has a development container setup that makes it easy to get started contributing. This setup only requires that [Docker](https://www.docker.com/get-started) is setup on your system. The development container setup includes a PostgreSQL container with migrations already applied and a container running GoTrue that will perform a hot reload when changes to the source code are detected. + +If you would like to run GoTrue locally or learn more about what these containers are doing for you, continue reading the [Setup and Tooling](#setup-and-tooling) section below. Otherwise, you can skip ahead to the [How To Verify that GoTrue is Available](#how-to-verify-that-gotrue-is-available) section to learn about working with and developing GoTrue. + +Before using the containers, you will need to make sure an `.env` file exists by making a copy of `example.env` and configuring it for your needs. A required change for the containers is that the `DATABASE_URL` be changed to `postgres://supabase_auth_admin:root@localhost:5432/postgres` where the address of the Postgres container is changed to `localhost`. + +The following are some basic commands. A full and up to date list of commands can be found in the project's `Makefile` or by running `make help`. + +### Starting the containers + +Start the containers as described above in an attached state with log output. + +``` bash +make dev +``` + +### Running tests in the containers + +Start the containers with a fresh database and run the project's tests. + +``` bash +make docker-test +``` + +### Removing the containers + +Remove both containers and their volumes. This removes any data associated with the containers. + +``` bash +make docker-clean +``` + +### Rebuild the containers + +Fully rebuild the containers without using any cached layers. + +``` bash +make docker-build +``` + ## Setup and Tooling GoTrue -- as the name implies -- is a user registration and authentication API developed in [Go](https://go.dev). diff --git a/Makefile b/Makefile index 1089e95c94..519ec4374d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: all build deps image lint migrate test vet CHECK_FILES?=$$(go list ./... | grep -v /vendor/) FLAGS?=-ldflags "-X github.com/netlify/gotrue/cmd.Version=`git describe --tags`" +DEV_DOCKER_COMPOSE:=docker-compose-dev.yml help: ## Show this help. @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -16,7 +17,7 @@ deps: ## Install dependencies. @go install golang.org/x/lint/golint@latest @go mod download -image: ## Build the Docker image. +image: ## Build the production Docker image. docker build . lint: ## Lint the code. @@ -34,22 +35,24 @@ test: ## Run tests. vet: # Vet the code go vet $(CHECK_FILES) - -# Run the development containers -dev: - docker-compose -f docker-compose-dev.yml up - -# Run the tests using the development containers -docker-test: - docker-compose -f docker-compose-dev.yml up -d postgres - docker-compose -f docker-compose-dev.yml run gotrue sh -c "./hack/migrate.sh postgres" - docker-compose -f docker-compose-dev.yml run gotrue sh -c "make test" - docker-compose -f docker-compose-dev.yml down -v - -# Remove the development containers and volumes -docker-build: - docker-compose -f docker-compose-dev.yml build --no-cache - -# Remove the development containers and volumes -docker-clean: - docker-compose -f docker-compose-dev.yml rm -fsv +dev: ## Run the development containers + # Start postgres first and apply migrations + docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres + docker-compose -f $(DEV_DOCKER_COMPOSE) run gotrue sh -c "make migrate_dev" + # Actually start the containers for dev + docker-compose -f $(DEV_DOCKER_COMPOSE) up + +docker-test: ## Run the tests using the development containers + docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres + docker-compose -f $(DEV_DOCKER_COMPOSE) run gotrue sh -c "make migrate_test" + docker-compose -f $(DEV_DOCKER_COMPOSE) run gotrue sh -c "make test" + docker-compose -f $(DEV_DOCKER_COMPOSE) down -v + +docker-build: ## Force a full rebuild of the development containers + docker-compose -f $(DEV_DOCKER_COMPOSE) build --no-cache + docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres + docker-compose -f $(DEV_DOCKER_COMPOSE) run gotrue sh -c "make migrate_dev" + docker-compose -f $(DEV_DOCKER_COMPOSE) down + +docker-clean: ## Remove the development containers and volumes + docker-compose -f $(DEV_DOCKER_COMPOSE) rm -fsv From c64f33193cb587976b4788391830a8f17be5ac4e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 22 Apr 2022 17:00:39 -0700 Subject: [PATCH 050/102] fix: shorten email otp (#446) * fix: add unique indices to user token columns * fix: shorten email otp * remove phone_change_token from migration * fix: use enums for each token type * refactor: use RawURLEncoding instead of removePadding * fix: update token format & verify * add description for v1 otp --- api/mail.go | 71 ++++++++++++++++--- api/token.go | 10 +++ api/verify.go | 14 ++++ crypto/crypto.go | 26 +++++-- mailer/template.go | 29 ++++++-- .../20220412150300_add_unique_idx.up.sql | 7 ++ models/user.go | 7 ++ 7 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 migrations/20220412150300_add_unique_idx.up.sql diff --git a/api/mail.go b/api/mail.go index c8ab003ccc..c31249d21d 100644 --- a/api/mail.go +++ b/api/mail.go @@ -14,6 +14,7 @@ import ( "github.com/netlify/gotrue/storage" "github.com/pkg/errors" "github.com/sethvargo/go-password/password" + "github.com/sirupsen/logrus" ) var ( @@ -168,12 +169,15 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { } func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { + var err error if u.ConfirmationSentAt != nil && !u.ConfirmationSentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } - oldToken := u.ConfirmationToken - u.ConfirmationToken = crypto.SecureToken() + u.ConfirmationToken, err = generateUniqueEmailOtp(tx, confirmationToken) + if err != nil { + return err + } now := time.Now() if err := mailer.ConfirmationMail(u, referrerURL); err != nil { u.ConfirmationToken = oldToken @@ -184,8 +188,12 @@ func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mail } func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, referrerURL string) error { + var err error oldToken := u.ConfirmationToken - u.ConfirmationToken = crypto.SecureToken() + u.ConfirmationToken, err = generateUniqueEmailOtp(tx, confirmationToken) + if err != nil { + return err + } now := time.Now() if err := mailer.InviteMail(u, referrerURL); err != nil { u.ConfirmationToken = oldToken @@ -197,12 +205,16 @@ func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, re } func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { + var err error if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } oldToken := u.RecoveryToken - u.RecoveryToken = crypto.SecureToken() + u.RecoveryToken, err = generateUniqueEmailOtp(tx, recoveryToken) + if err != nil { + return err + } now := time.Now() if err := mailer.RecoveryMail(u, referrerURL); err != nil { u.RecoveryToken = oldToken @@ -213,12 +225,16 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile } func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration) error { + var err error if u.ReauthenticationSentAt != nil && !u.ReauthenticationSentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } oldToken := u.ReauthenticationToken - u.ReauthenticationToken = crypto.SecureToken() + u.ReauthenticationToken, err = generateUniqueEmailOtp(tx, reauthenticationToken) + if err != nil { + return err + } now := time.Now() if err := mailer.ReauthenticateMail(u); err != nil { u.ReauthenticationToken = oldToken @@ -229,14 +245,17 @@ func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, ma } func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { + var err error // since Magic Link is just a recovery with a different template and behaviour // around new users we will reuse the recovery db timer to prevent potential abuse if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } - oldToken := u.RecoveryToken - u.RecoveryToken = crypto.SecureToken() + u.RecoveryToken, err = generateUniqueEmailOtp(tx, recoveryToken) + if err != nil { + return err + } now := time.Now() if err := mailer.MagicLinkMail(u, referrerURL); err != nil { u.RecoveryToken = oldToken @@ -248,9 +267,16 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile // sendEmailChange sends out an email change token to the new email. func (a *API) sendEmailChange(tx *storage.Connection, config *conf.Configuration, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error { - u.EmailChangeTokenNew = crypto.SecureToken() + var err error + u.EmailChangeTokenNew, err = generateUniqueEmailOtp(tx, emailChangeTokenNew) + if err != nil { + return err + } if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" { - u.EmailChangeTokenCurrent = crypto.SecureToken() + u.EmailChangeTokenCurrent, err = generateUniqueEmailOtp(tx, emailChangeTokenCurrent) + if err != nil { + return err + } } u.EmailChange = email u.EmailChangeConfirmStatus = zeroConfirmation @@ -280,3 +306,30 @@ func (a *API) validateEmail(ctx context.Context, email string) error { } return nil } + +// generateUniqueEmailOtp returns a unique otp +func generateUniqueEmailOtp(tx *storage.Connection, tokenType tokenType) (string, error) { + maxRetries := 5 + otpLength := 20 + var otp string + var err error + for i := 0; i < maxRetries; i++ { + otp, err = crypto.GenerateEmailOtp(otpLength) + if err != nil { + return "", err + } + _, err = models.FindUserByTokenAndTokenType(tx, otp, string(tokenType)) + if err != nil { + if models.IsNotFoundError(err) { + return otp, nil + } + return "", err + } + logrus.Warn("otp generated is not unique, retrying.") + err = errors.New("Could not generate a unique email otp") + } + if err != nil { + return "", err + } + return "", errors.New("Could not generate a unique email otp") +} diff --git a/api/token.go b/api/token.go index 16f0d396ce..edd86b8378 100644 --- a/api/token.go +++ b/api/token.go @@ -58,6 +58,16 @@ type IdTokenGrantParams struct { Issuer string `json:"issuer"` } +type tokenType string + +const ( + confirmationToken tokenType = "confirmation_token" + recoveryToken tokenType = "recovery_token" + emailChangeTokenNew tokenType = "email_change_token_new" + emailChangeTokenCurrent tokenType = "email_change_token_current" + reauthenticationToken tokenType = "reauthentication_token" +) + const useCookieHeader = "x-use-cookie" const useSessionCookie = "session" const InvalidLoginMessage = "Invalid login credentials" diff --git a/api/verify.go b/api/verify.go index bf1ba333e0..2fcf817c35 100644 --- a/api/verify.go +++ b/api/verify.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/netlify/gotrue/models" @@ -34,6 +35,11 @@ const ( singleConfirmation ) +const ( + // v1 uses crypto.SecureToken() + v1OtpLength = 22 +) + // Only applicable when SECURE_EMAIL_CHANGE_ENABLED const singleConfirmationAccepted = "Confirmation link accepted. Please proceed to confirm link sent to the other email" @@ -77,6 +83,10 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { if params.Token == "" { return badRequestError("Verify requires a token") } + if len(params.Token) > v1OtpLength { + // token follows the v2 format and includes "-" + params.Token = strings.ReplaceAll(params.Token, "-", "") + } if params.Type == "" { return badRequestError("Verify requires a verification type") } @@ -153,6 +163,10 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { if params.Token == "" { return badRequestError("Verify requires a token") } + if len(params.Token) > v1OtpLength { + // token follows the v2 format and includes "-" + params.Token = strings.ReplaceAll(params.Token, "-", "") + } if params.Type == "" { return badRequestError("Verify requires a verification type") diff --git a/crypto/crypto.go b/crypto/crypto.go index 3c788f95ce..22df538a19 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -8,7 +8,6 @@ import ( "math" "math/big" "strconv" - "strings" "github.com/pkg/errors" ) @@ -19,11 +18,7 @@ func SecureToken() string { if _, err := io.ReadFull(rand.Reader, b); err != nil { panic(err.Error()) // rand should never fail } - return removePadding(base64.URLEncoding.EncodeToString(b)) -} - -func removePadding(token string) string { - return strings.TrimRight(token, "=") + return base64.RawURLEncoding.EncodeToString(b) } // GenerateOtp generates a random n digit otp @@ -37,3 +32,22 @@ func GenerateOtp(digits int) (string, error) { otp := fmt.Sprintf(expr, val.String()) return otp, nil } + +// GenerateOtpFromCharset generates a random n-length otp from a charset +func GenerateOtpFromCharset(length int, charset string) (string, error) { + b := make([]byte, length) + for i := range b { + val, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", errors.WithMessage(err, "Error generating otp from charset") + } + b[i] = charset[val.Int64()] + } + return string(b), nil +} + +// GenerateEmailOtp generates a random n-length alphanumeric otp +func GenerateEmailOtp(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyz" + return GenerateOtpFromCharset(length, charset) +} diff --git a/mailer/template.go b/mailer/template.go index 3380ced721..f4e82a867c 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -2,6 +2,7 @@ package mailer import ( "fmt" + "strings" "github.com/badoux/checkmail" "github.com/netlify/gotrue/conf" @@ -79,7 +80,7 @@ func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": user.ConfirmationToken, + "Token": formatEmailOtp(user.ConfirmationToken), "Data": user.UserMetaData, } @@ -109,7 +110,7 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": user.ConfirmationToken, + "Token": formatEmailOtp(user.ConfirmationToken), "Data": user.UserMetaData, } @@ -127,7 +128,7 @@ func (m *TemplateMailer) ReauthenticateMail(user *models.User) error { data := map[string]interface{}{ "SiteURL": m.Config.SiteURL, "Email": user.Email, - "Token": user.ReauthenticationToken, + "Token": formatEmailOtp(user.ReauthenticationToken), "Data": user.UserMetaData, } @@ -151,7 +152,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) emails := []Email{ { Address: user.EmailChange, - Token: user.EmailChangeTokenNew, + Token: formatEmailOtp(user.EmailChangeTokenNew), Subject: string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")), Template: m.Config.Mailer.Templates.EmailChange, }, @@ -161,7 +162,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) if m.Config.Mailer.SecureEmailChangeEnabled && currentEmail != "" { emails = append(emails, Email{ Address: currentEmail, - Token: user.EmailChangeTokenCurrent, + Token: formatEmailOtp(user.EmailChangeTokenCurrent), Subject: string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")), Template: m.Config.Mailer.Templates.EmailChange, }) @@ -233,7 +234,7 @@ func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) err "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": user.RecoveryToken, + "Token": formatEmailOtp(user.RecoveryToken), "Data": user.UserMetaData, } @@ -263,7 +264,7 @@ func (m *TemplateMailer) MagicLinkMail(user *models.User, referrerURL string) er "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": user.RecoveryToken, + "Token": formatEmailOtp(user.RecoveryToken), "Data": user.UserMetaData, } @@ -316,3 +317,17 @@ func (m TemplateMailer) GetEmailActionLink(user *models.User, actionType, referr return url, nil } + +// formatEmailOtp separates the otp into chunks of 5 with "-" as the separator +func formatEmailOtp(otp string) string { + chunkSize := 5 + var chunks []string + for i := 0; i < len(otp); i += chunkSize { + if i+chunkSize >= len(otp) { + chunks = append(chunks, otp[i:]) + } else { + chunks = append(chunks, otp[i:i+chunkSize]) + } + } + return strings.Join(chunks, "-") +} diff --git a/migrations/20220412150300_add_unique_idx.up.sql b/migrations/20220412150300_add_unique_idx.up.sql new file mode 100644 index 0000000000..52eeecf2d7 --- /dev/null +++ b/migrations/20220412150300_add_unique_idx.up.sql @@ -0,0 +1,7 @@ +-- add partial unique indices to confirmation_token, recovery_token, email_change_token_current, email_change_token_new, phone_change_token, reauthentication_token + +CREATE UNIQUE INDEX IF NOT EXISTS confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE confirmation_token != ''; +CREATE UNIQUE INDEX IF NOT EXISTS recovery_token_idx ON auth.users USING btree (recovery_token) WHERE recovery_token != ''; +CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE email_change_token_current != ''; +CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE email_change_token_new != ''; +CREATE UNIQUE INDEX IF NOT EXISTS reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE reauthentication_token != ''; diff --git a/models/user.go b/models/user.go index 7af1e4d0e2..7ede0cf6cd 100644 --- a/models/user.go +++ b/models/user.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "fmt" "strings" "time" @@ -398,6 +399,12 @@ func FindUserByEmailChangeToken(tx *storage.Connection, token string) (*User, er return findUser(tx, "email_change_token_current = ? or email_change_token_new = ?", token, token) } +// FindUserByTokenAndTokenType finds a user with the matching token and token type. +func FindUserByTokenAndTokenType(tx *storage.Connection, token string, tokenType string) (*User, error) { + query := fmt.Sprintf("%v = ?", tokenType) + return findUser(tx, query, token) +} + // FindUserWithRefreshToken finds a user from the provided refresh token. func FindUserWithRefreshToken(tx *storage.Connection, token string) (*User, *RefreshToken, error) { refreshToken := &RefreshToken{} From ca839e42ace1b5fbbcaa1a7573ba6edefa3e6866 Mon Sep 17 00:00:00 2001 From: Bariq Nurlis Date: Wed, 27 Apr 2022 07:43:46 +0800 Subject: [PATCH 051/102] fix: change Discord's discriminator type to string (#457) * change discriminator type to string Signed-off-by: Bariq * refactor discriminator for integer for avatar retrieval Signed-off-by: Bariq --- api/provider/discord.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/api/provider/discord.go b/api/provider/discord.go index d7344eb679..c96717ee6b 100644 --- a/api/provider/discord.go +++ b/api/provider/discord.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/netlify/gotrue/conf" @@ -21,7 +22,7 @@ type discordProvider struct { type discordUser struct { Avatar string `json:"avatar"` - Discriminator int `json:"discriminator,string"` + Discriminator string `json:"discriminator,string"` Email string `json:"email"` ID string `json:"id"` Name string `json:"username"` @@ -76,11 +77,15 @@ func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U var avatarURL string extension := "png" - // https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints: - // In the case of the Default User Avatar endpoint, the value for - // user_discriminator in the path should be the user's discriminator modulo 5 if u.Avatar == "" { - avatarURL = fmt.Sprintf("https://cdn.discordapp.com/embed/avatars/%d.%s", u.Discriminator%5, extension) + if intDiscriminator, err := strconv.Atoi(u.Discriminator); err != nil { + return nil, err + } else { + // https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints: + // In the case of the Default User Avatar endpoint, the value for + // user_discriminator in the path should be the user's discriminator modulo 5 + avatarURL = fmt.Sprintf("https://cdn.discordapp.com/embed/avatars/%d.%s", intDiscriminator%5, extension) + } } else { // https://discord.com/developers/docs/reference#image-formatting: // "In the case of endpoints that support GIFs, the hash will begin with a_ From 60a7a6fab8ac29939fdaaf3799dec0a3c24b946e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 29 Apr 2022 01:06:01 -0700 Subject: [PATCH 052/102] fix: unique index should not apply to phone otps (#460) --- ...dx.up.sql => 20220429010200_add_unique_idx.up.sql} | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) rename migrations/{20220412150300_add_unique_idx.up.sql => 20220429010200_add_unique_idx.up.sql} (64%) diff --git a/migrations/20220412150300_add_unique_idx.up.sql b/migrations/20220429010200_add_unique_idx.up.sql similarity index 64% rename from migrations/20220412150300_add_unique_idx.up.sql rename to migrations/20220429010200_add_unique_idx.up.sql index 52eeecf2d7..c5c137e758 100644 --- a/migrations/20220412150300_add_unique_idx.up.sql +++ b/migrations/20220429010200_add_unique_idx.up.sql @@ -1,7 +1,12 @@ -- add partial unique indices to confirmation_token, recovery_token, email_change_token_current, email_change_token_new, phone_change_token, reauthentication_token +DROP INDEX IF EXISTS confirmation_token_idx; +DROP INDEX IF EXISTS recovery_token_idx; +DROP INDEX IF EXISTS email_change_token_current_idx; +DROP INDEX IF EXISTS email_change_token_new_idx; +DROP INDEX IF EXISTS reauthentication_token_idx; -CREATE UNIQUE INDEX IF NOT EXISTS confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE confirmation_token != ''; -CREATE UNIQUE INDEX IF NOT EXISTS recovery_token_idx ON auth.users USING btree (recovery_token) WHERE recovery_token != ''; +CREATE UNIQUE INDEX IF NOT EXISTS confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE confirmation_token != '' AND confirmation_token !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS recovery_token_idx ON auth.users USING btree (recovery_token) WHERE recovery_token != '' AND confirmation_token !~ '^[0-9 ]*$'; CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE email_change_token_current != ''; CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE email_change_token_new != ''; -CREATE UNIQUE INDEX IF NOT EXISTS reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE reauthentication_token != ''; +CREATE UNIQUE INDEX IF NOT EXISTS reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE reauthentication_token != '' AND confirmation_token !~ '^[0-9 ]*$'; From a4af211c56547dc5f8347f388e8d2d5278db37dc Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 29 Apr 2022 01:29:47 -0700 Subject: [PATCH 053/102] fix: bump gotrue to v2.6.25 (#461) From b02f838e0a731edf93b35cbf934dd6ed34fe5e10 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 29 Apr 2022 09:47:17 -0700 Subject: [PATCH 054/102] fix: discord discriminator (#462) * fix: invalid use of string struct tag * fix: add test for discord discriminator --- api/external_discord_test.go | 2 +- api/provider/discord.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/external_discord_test.go b/api/external_discord_test.go index 057cd9c59e..916f8855ef 100644 --- a/api/external_discord_test.go +++ b/api/external_discord_test.go @@ -10,7 +10,7 @@ import ( ) const ( - discordUser string = `{"id":"discordTestId","avatar":"abc","email":"discord@example.com","username":"Discord Test","verified":true}}` + discordUser string = `{"id":"discordTestId","avatar":"abc","email":"discord@example.com","username":"Discord Test","verified":true,"discriminator":"0001"}}` discordUserWrongEmail string = `{"id":"discordTestId","avatar":"abc","email":"other@example.com","username":"Discord Test","verified":true}}` discordUserNoEmail string = `{"id":"discordTestId","avatar":"abc","username":"Discord Test","verified":true}}` ) diff --git a/api/provider/discord.go b/api/provider/discord.go index c96717ee6b..3011814315 100644 --- a/api/provider/discord.go +++ b/api/provider/discord.go @@ -22,7 +22,7 @@ type discordProvider struct { type discordUser struct { Avatar string `json:"avatar"` - Discriminator string `json:"discriminator,string"` + Discriminator string `json:"discriminator"` Email string `json:"email"` ID string `json:"id"` Name string `json:"username"` From 48d6554eff39d8457cae916e2e5b9267ce61b363 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 29 Apr 2022 10:26:45 -0700 Subject: [PATCH 055/102] fix: update migration for creating partial indices (#463) --- ...x.up.sql => 20220429102000_add_unique_idx.up.sql} | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) rename migrations/{20220429010200_add_unique_idx.up.sql => 20220429102000_add_unique_idx.up.sql} (76%) diff --git a/migrations/20220429010200_add_unique_idx.up.sql b/migrations/20220429102000_add_unique_idx.up.sql similarity index 76% rename from migrations/20220429010200_add_unique_idx.up.sql rename to migrations/20220429102000_add_unique_idx.up.sql index c5c137e758..b4280e0ce7 100644 --- a/migrations/20220429010200_add_unique_idx.up.sql +++ b/migrations/20220429102000_add_unique_idx.up.sql @@ -1,12 +1,14 @@ -- add partial unique indices to confirmation_token, recovery_token, email_change_token_current, email_change_token_new, phone_change_token, reauthentication_token +-- ignores partial unique index creation on fields which contain empty strings, whitespaces or purely numeric otps + DROP INDEX IF EXISTS confirmation_token_idx; DROP INDEX IF EXISTS recovery_token_idx; DROP INDEX IF EXISTS email_change_token_current_idx; DROP INDEX IF EXISTS email_change_token_new_idx; DROP INDEX IF EXISTS reauthentication_token_idx; -CREATE UNIQUE INDEX IF NOT EXISTS confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE confirmation_token != '' AND confirmation_token !~ '^[0-9 ]*$'; -CREATE UNIQUE INDEX IF NOT EXISTS recovery_token_idx ON auth.users USING btree (recovery_token) WHERE recovery_token != '' AND confirmation_token !~ '^[0-9 ]*$'; -CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE email_change_token_current != ''; -CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE email_change_token_new != ''; -CREATE UNIQUE INDEX IF NOT EXISTS reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE reauthentication_token != '' AND confirmation_token !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS confirmation_token_idx ON auth.users USING btree (confirmation_token) WHERE confirmation_token !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS recovery_token_idx ON auth.users USING btree (recovery_token) WHERE recovery_token !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_current_idx ON auth.users USING btree (email_change_token_current) WHERE email_change_token_current !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS email_change_token_new_idx ON auth.users USING btree (email_change_token_new) WHERE email_change_token_new !~ '^[0-9 ]*$'; +CREATE UNIQUE INDEX IF NOT EXISTS reauthentication_token_idx ON auth.users USING btree (reauthentication_token) WHERE reauthentication_token !~ '^[0-9 ]*$'; From 94e22346305b61d76f377b2b4917fd73dc7a146a Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 3 May 2022 18:19:08 -0700 Subject: [PATCH 056/102] chore: update docker-compose to use ports instead of host networking --- .dockerignore | 1 + Dockerfile.dev | 9 +++------ docker-compose-dev.yml | 29 ++++++++++++++++++++--------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.dockerignore b/.dockerignore index 8efdb51bf1..51e529c9e7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ /hack/ /vendor/ /www/ +.env diff --git a/Dockerfile.dev b/Dockerfile.dev index a21af29314..b7a8f59baf 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -9,14 +9,11 @@ WORKDIR /go/src/github.com/netlify/gotrue # Pulling dependencies COPY ./Makefile ./go.* ./ - -# Production dependencies RUN make deps -# Development dependences -RUN go get -u github.com/Thatooine/go-test-html-report -RUN go get github.com/githubnemo/CompileDaemon -RUN go install github.com/githubnemo/CompileDaemon +# Building stuff +COPY . /go/src/github.com/netlify/gotrue +RUN make build RUN adduser -D -u 1000 netlify USER netlify diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 1d8f140d88..7f1b602d8c 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -7,19 +7,30 @@ services: build: context: ./ dockerfile: Dockerfile.dev - # Not sure why ports aren't working. Using host as a hack - # ports: - # - "9999:9999" - network_mode: "host" + ports: + - "9999:9999" environment: - - GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/netlify/gotrue/migrations - volumes: - - ./:/go/src/github.com/netlify/gotrue - command: CompileDaemon --build="make build" --directory=/go/src/github.com/netlify/gotrue --recursive=true -pattern=(.+\.go|.+\.env) -exclude=gotrue -exclude=gotrue-arm64 --command=/go/src/github.com/netlify/gotrue/gotrue + # Update the docker environment accordingly based on example.env + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_DB_MIGRATIONS_PATH: /go/src/github.com/netlify/gotrue/migrations + GOTRUE_DB_DRIVER: postgres + DATABASE_URL: postgres://supabase_auth_admin:root@postgres:5432/postgres + GOTRUE_API_HOST: 0.0.0.0 + PORT: 9999 + GOTRUE_MAILER_AUTOCONFIRM: "true" + GOTRUE_SMS_AUTOCONFIRM: "true" + GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify + GOTRUE_MAILER_URLPATHS_REAUTHENTICATION: /auth/v1/verify + command: ./gotrue postgres: image: postgres:13 container_name: postgres - network_mode: "host" + ports: + - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - ${PWD}/hack/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql From 6a6e3bed6513d9d11f84efd2f4402ea0c3c4c71b Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 4 May 2022 18:19:12 -0700 Subject: [PATCH 057/102] fix: add reuse interval for token refresh (#466) * add reuse interval to config * add test for refresh token reuse detection * fix: add reuse interval to refresh token grant * add tests for reuse interval * refactor token test * ignore reuse interval if revoked token is last token * add test case * refactor query to get child token * remove unnecessary check in refresh token grant * update readme * update example env file --- README.md | 9 +++++ api/token.go | 67 ++++++++++++++++++------------- api/token_test.go | 89 +++++++++++++++++++++++++++++++++++++++++ conf/configuration.go | 1 + example.env | 4 +- models/refresh_token.go | 21 ++++++++-- 6 files changed, 159 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 322b8ef52c..b0342ce7c3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,15 @@ Rate limit the number of emails sent per hr on the following endpoints: `/signup Minimum password length, defaults to 6. +`GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED` - `bool` + +If refresh token rotation is enabled, gotrue will automatically detect malicious attempts to reuse a revoked refresh token. When a malicious attempt is detected, gotrue immediately revokes all tokens that descended from the offending token. + +`GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL` - `string` + +This setting is only applicable if `GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED` is enabled. The reuse interval for a refresh token allows for exchanging the refresh token multiple times during the interval to support concurrency or offline issues. During the reuse interval, gotrue will not consider using a revoked token as a malicious attempt and will simply return the child refresh token. + +Only the previous revoked token can be reused. Using an old refresh token way before the current valid refresh token will trigger the reuse detection. ### API ```properties diff --git a/api/token.go b/api/token.go index edd86b8378..1ce341bee8 100644 --- a/api/token.go +++ b/api/token.go @@ -273,41 +273,50 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return oauthError("invalid_grant", "Invalid Refresh Token") } - if !(config.External.Email.Enabled && config.External.Phone.Enabled) { - providers, err := models.FindProvidersByUser(a.db, user) - if err != nil { - return internalServerError(err.Error()) - } - for _, provider := range providers { - if provider == "email" && !config.External.Email.Enabled { - return badRequestError("Email logins are disabled") + var newToken *models.RefreshToken + if token.Revoked { + a.clearCookieTokens(config, w) + err = a.db.Transaction(func(tx *storage.Connection) error { + validToken, terr := models.GetValidChildToken(tx, token) + if terr != nil { + if errors.Is(terr, models.RefreshTokenNotFoundError{}) { + // revoked token has no descendants + return nil + } + return terr } - if provider == "phone" && !config.External.Phone.Enabled { - return badRequestError("Phone logins are disabled") + // check if token is the last previous revoked token + if validToken.Parent == storage.NullString(token.Token) { + refreshTokenReuseWindow := token.UpdatedAt.Add(time.Second * time.Duration(config.Security.RefreshTokenReuseInterval)) + if time.Now().Before(refreshTokenReuseWindow) { + newToken = validToken + } } + return nil + }) + if err != nil { + return internalServerError("Error validating reuse interval").WithInternalError(err) } - } - if token.Revoked { - a.clearCookieTokens(config, w) - if config.Security.RefreshTokenRotationEnabled { - // Revoke all tokens in token family - err = a.db.Transaction(func(tx *storage.Connection) error { - var terr error - if terr = models.RevokeTokenFamily(tx, token); terr != nil { - return terr + if newToken == nil { + if config.Security.RefreshTokenRotationEnabled { + // Revoke all tokens in token family + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + if terr = models.RevokeTokenFamily(tx, token); terr != nil { + return terr + } + return nil + }) + if err != nil { + return internalServerError(err.Error()) } - return nil - }) - if err != nil { - return internalServerError(err.Error()) } + return oauthError("invalid_grant", "Invalid Refresh Token").WithInternalMessage("Possible abuse attempt: %v", r) } - return oauthError("invalid_grant", "Invalid Refresh Token").WithInternalMessage("Possible abuse attempt: %v", r) } var tokenString string - var newToken *models.RefreshToken var newTokenResponse *AccessTokenResponse err = a.db.Transaction(func(tx *storage.Connection) error { @@ -316,9 +325,11 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return terr } - newToken, terr = models.GrantRefreshTokenSwap(tx, user, token) - if terr != nil { - return internalServerError(terr.Error()) + if newToken == nil { + newToken, terr = models.GrantRefreshTokenSwap(tx, user, token) + if terr != nil { + return internalServerError(terr.Error()) + } } tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) diff --git a/api/token_test.go b/api/token_test.go index eaa5dcb5fa..763cb49da4 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -150,6 +150,95 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenGrantFailure() { assert.Equal(ts.T(), http.StatusBadRequest, w.Code) } +func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { + u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + t := time.Now() + u.EmailConfirmedAt = &t + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") + first, err := models.GrantAuthenticatedUser(ts.API.db, u) + require.NoError(ts.T(), err) + second, err := models.GrantRefreshTokenSwap(ts.API.db, u, first) + require.NoError(ts.T(), err) + third, err := models.GrantRefreshTokenSwap(ts.API.db, u, second) + require.NoError(ts.T(), err) + + cases := []struct { + desc string + refreshTokenRotationEnabled bool + reuseInterval int + refreshToken string + expectedCode int + expectedBody map[string]interface{} + }{ + { + "Valid refresh within reuse interval", + true, + 30, + second.Token, + http.StatusOK, + map[string]interface{}{ + "refresh_token": third.Token, + }, + }, + { + "Invalid refresh, first token is not the previous revoked token", + true, + 0, + first.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", + }, + }, + { + "Invalid refresh, revoked third token", + true, + 0, + second.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", + }, + }, + { + "Invalid refresh, third token revoked by previous case", + true, + 30, + third.Token, + http.StatusBadRequest, + map[string]interface{}{ + "error": "invalid_grant", + "error_description": "Invalid Refresh Token", + }, + }, + } + + for _, c := range cases { + ts.Run(c.desc, func() { + ts.Config.Security.RefreshTokenRotationEnabled = c.refreshTokenRotationEnabled + ts.Config.Security.RefreshTokenReuseInterval = c.reuseInterval + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "refresh_token": c.refreshToken, + })) + req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), c.expectedCode, w.Code) + + data := make(map[string]interface{}) + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + for k, v := range c.expectedBody { + require.Equal(ts.T(), v, data[k]) + } + }) + } +} + func (ts *TokenTestSuite) createBannedUser() *models.User { u, err := models.NewUser(ts.instanceID, "banned@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") diff --git a/conf/configuration.go b/conf/configuration.go index c99e840e61..b3a817e57d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -181,6 +181,7 @@ type CaptchaConfiguration struct { type SecurityConfiguration struct { Captcha CaptchaConfiguration `json:"captcha"` RefreshTokenRotationEnabled bool `json:"refresh_token_rotation_enabled" split_words:"true" default:"true"` + RefreshTokenReuseInterval int `json:"refresh_token_reuse_interval" split_words:"true"` UpdatePasswordRequireReauthentication bool `json:"update_password_require_reauthentication" split_words:"true"` } diff --git a/example.env b/example.env index e38ef11a44..be0e63122b 100644 --- a/example.env +++ b/example.env @@ -190,7 +190,9 @@ GOTRUE_EXTERNAL_SAML_SIGNING_KEY="" # Additional Security config GOTRUE_LOG_LEVEL="debug" -GOTRUE_REFRESH_TOKEN_ROTATION_ENABLED="false" +GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED="false" +GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL="0" +GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION="false" GOTRUE_OPERATOR_TOKEN="unused-operator-token" GOTRUE_RATE_LIMIT_HEADER="X-Forwarded-For" GOTRUE_RATE_LIMIT_EMAIL_SENT="100" diff --git a/models/refresh_token.go b/models/refresh_token.go index 054806e4ea..2af42c505b 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -1,6 +1,7 @@ package models import ( + "database/sql" "time" "github.com/gobuffalo/pop/v5" @@ -57,19 +58,33 @@ func GrantRefreshTokenSwap(tx *storage.Connection, user *User, token *RefreshTok // RevokeTokenFamily revokes all refresh tokens that descended from the provided token. func RevokeTokenFamily(tx *storage.Connection, token *RefreshToken) error { + tablename := (&pop.Model{Value: RefreshToken{}}).TableName() err := tx.RawQuery(` with recursive token_family as ( - select id, user_id, token, revoked, parent from refresh_tokens where parent = ? + select id, user_id, token, revoked, parent from `+tablename+` where parent = ? union - select r.id, r.user_id, r.token, r.revoked, r.parent from `+(&pop.Model{Value: RefreshToken{}}).TableName()+` r inner join token_family t on t.token = r.parent + select r.id, r.user_id, r.token, r.revoked, r.parent from `+tablename+` r inner join token_family t on t.token = r.parent ) - update `+(&pop.Model{Value: RefreshToken{}}).TableName()+` r set revoked = true from token_family where token_family.id = r.id;`, token.Token).Exec() + update `+tablename+` r set revoked = true from token_family where token_family.id = r.id;`, token.Token).Exec() if err != nil { return err } return nil } +// GetValidChildToken returns the child token of the token provided if the child is not revoked. +func GetValidChildToken(tx *storage.Connection, token *RefreshToken) (*RefreshToken, error) { + refreshToken := &RefreshToken{} + err := tx.Q().Where("parent = ? and revoked = false", token.Token).First(refreshToken) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, RefreshTokenNotFoundError{} + } + return nil, err + } + return refreshToken, nil +} + // Logout deletes all refresh tokens for a user. func Logout(tx *storage.Connection, instanceID uuid.UUID, id uuid.UUID) error { return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: RefreshToken{}}).TableName()+" WHERE instance_id = ? AND user_id = ?", instanceID, id).Exec() From 7df9004bcc38eecf4d1e1410bb0f62e996b444b1 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 5 May 2022 00:29:38 -0700 Subject: [PATCH 058/102] chore: rename rate limit token endpoint env var (#470) --- api/api.go | 2 +- api/token_test.go | 2 +- conf/configuration.go | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/api.go b/api/api.go index 5a0bbf83f7..cb98b37fc4 100644 --- a/api/api.go +++ b/api/api.go @@ -124,7 +124,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.With(api.limitHandler( // Allow requests at the specified rate per 5 minutes. - tollbooth.NewLimiter(api.config.RateLimitToken/(60*5), &limiter.ExpirableOptions{ + tollbooth.NewLimiter(api.config.RateLimitTokenRefresh/(60*5), &limiter.ExpirableOptions{ DefaultExpirationTTL: time.Hour, }).SetBurst(30), )).Post("/token", api.Token) diff --git a/api/token_test.go b/api/token_test.go index 763cb49da4..ace039f937 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -57,7 +57,7 @@ func (ts *TokenTestSuite) SetupTest() { require.NoError(ts.T(), err, "Error creating refresh token") } -func (ts *TokenTestSuite) TestRateLimitToken() { +func (ts *TokenTestSuite) TestRateLimitTokenRefresh() { var buffer bytes.Buffer req := httptest.NewRequest(http.MethodPost, "http://localhost/token", &buffer) req.Header.Set("Content-Type", "application/json") diff --git a/conf/configuration.go b/conf/configuration.go index b3a817e57d..21a87d062e 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -66,17 +66,17 @@ type GlobalConfiguration struct { RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"` ExternalURL string `json:"external_url" envconfig:"API_EXTERNAL_URL"` } - DB DBConfiguration - External ProviderConfiguration - Logging LoggingConfig `envconfig:"LOG"` - OperatorToken string `split_words:"true" required:"false"` - MultiInstanceMode bool - Tracing TracingConfig - SMTP SMTPConfiguration - RateLimitHeader string `split_words:"true"` - RateLimitEmailSent float64 `split_words:"true" default:"30"` - RateLimitVerify float64 `split_words:"true" default:"30"` - RateLimitToken float64 `split_words:"true" default:"30"` + DB DBConfiguration + External ProviderConfiguration + Logging LoggingConfig `envconfig:"LOG"` + OperatorToken string `split_words:"true" required:"false"` + MultiInstanceMode bool + Tracing TracingConfig + SMTP SMTPConfiguration + RateLimitHeader string `split_words:"true"` + RateLimitEmailSent float64 `split_words:"true" default:"30"` + RateLimitVerify float64 `split_words:"true" default:"30"` + RateLimitTokenRefresh float64 `split_words:"true" default:"30"` } // EmailContentConfiguration holds the configuration for emails, both subjects and template URLs. From c9bce605cdc74137d32a72312d58ab9f111603a2 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 5 May 2022 15:38:11 -0700 Subject: [PATCH 059/102] overwrite existing env if envfile provided --- conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/configuration.go b/conf/configuration.go index c99e840e61..a2e7c4da54 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -208,7 +208,7 @@ type Configuration struct { func loadEnvironment(filename string) error { var err error if filename != "" { - err = godotenv.Load(filename) + err = godotenv.Overload(filename) } else { err = godotenv.Load() // handle if .env file does not exist, this is OK From d91f8f91aa206a79c9c58a815ed0a415462d5b6f Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 5 May 2022 15:38:39 -0700 Subject: [PATCH 060/102] revert docker-compose to use hot reload --- .dockerignore | 1 - Dockerfile.dev | 8 +++++--- Makefile | 11 +++++++---- docker-compose-dev.yml | 24 ++++++------------------ 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/.dockerignore b/.dockerignore index 51e529c9e7..8efdb51bf1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ /hack/ /vendor/ /www/ -.env diff --git a/Dockerfile.dev b/Dockerfile.dev index b7a8f59baf..cabe1f6f5f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -9,11 +9,13 @@ WORKDIR /go/src/github.com/netlify/gotrue # Pulling dependencies COPY ./Makefile ./go.* ./ + +# Production dependencies RUN make deps -# Building stuff -COPY . /go/src/github.com/netlify/gotrue -RUN make build +# Development dependences +RUN go get github.com/githubnemo/CompileDaemon +RUN go install github.com/githubnemo/CompileDaemon RUN adduser -D -u 1000 netlify USER netlify diff --git a/Makefile b/Makefile index 519ec4374d..c4ae9f4e53 100644 --- a/Makefile +++ b/Makefile @@ -36,11 +36,14 @@ vet: # Vet the code go vet $(CHECK_FILES) dev: ## Run the development containers - # Start postgres first and apply migrations + # Start postgres then gotrue docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres - docker-compose -f $(DEV_DOCKER_COMPOSE) run gotrue sh -c "make migrate_dev" - # Actually start the containers for dev - docker-compose -f $(DEV_DOCKER_COMPOSE) up + docker-compose -f $(DEV_DOCKER_COMPOSE) up -d gotrue + docker-compose -f $(DEV_DOCKER_COMPOSE) logs -f + +down: ## Shutdown the development containers + # Start postgres first and apply migrations + docker-compose -f $(DEV_DOCKER_COMPOSE) down docker-test: ## Run the tests using the development containers docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 7f1b602d8c..49f4eb9cc4 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -8,29 +8,17 @@ services: context: ./ dockerfile: Dockerfile.dev ports: - - "9999:9999" + - '9999:9999' environment: - # Update the docker environment accordingly based on example.env - GOTRUE_SITE_URL: ${SITE_URL} - GOTRUE_JWT_SECRET: ${JWT_SECRET} - GOTRUE_DB_MIGRATIONS_PATH: /go/src/github.com/netlify/gotrue/migrations - GOTRUE_DB_DRIVER: postgres - DATABASE_URL: postgres://supabase_auth_admin:root@postgres:5432/postgres - GOTRUE_API_HOST: 0.0.0.0 - PORT: 9999 - GOTRUE_MAILER_AUTOCONFIRM: "true" - GOTRUE_SMS_AUTOCONFIRM: "true" - GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify - GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify - GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify - GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify - GOTRUE_MAILER_URLPATHS_REAUTHENTICATION: /auth/v1/verify - command: ./gotrue + - GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/netlify/gotrue/migrations + volumes: + - ./:/go/src/github.com/netlify/gotrue + command: CompileDaemon --build="make build" --directory=/go/src/github.com/netlify/gotrue --recursive=true -pattern=(.+\.go|.+\.env) -exclude=gotrue -exclude=gotrue-arm64 -exclude=.env --command="/go/src/github.com/netlify/gotrue/gotrue -c=.env.docker" postgres: image: postgres:13 container_name: postgres ports: - - "5432:5432" + - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data - ${PWD}/hack/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql From 07df71d849de77c6f379a49c6b678070170ca23a Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 5 May 2022 15:41:57 -0700 Subject: [PATCH 061/102] add example docker env file --- example.docker.env | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 example.docker.env diff --git a/example.docker.env b/example.docker.env new file mode 100644 index 0000000000..89eefe0627 --- /dev/null +++ b/example.docker.env @@ -0,0 +1,7 @@ +GOTRUE_SITE_URL="http://localhost:3000" +GOTRUE_JWT_SECRET="" +GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/netlify/gotrue/migrations +GOTRUE_DB_DRIVER=postgres +DATABASE_URL=postgres://supabase_auth_admin:root@postgres:5432/postgres +GOTRUE_API_HOST=0.0.0.0 +PORT=9999 From 362480db9b4ab2d84039bb1ae722f6d6ed3cb3a8 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 5 May 2022 16:16:50 -0700 Subject: [PATCH 062/102] cleanup unnecesary commands --- Dockerfile.dev | 3 --- Makefile | 3 --- hack/migrate.sh | 3 --- hack/migrate_postgres.sh | 12 ------------ 4 files changed, 21 deletions(-) delete mode 100755 hack/migrate_postgres.sh diff --git a/Dockerfile.dev b/Dockerfile.dev index cabe1f6f5f..de4e077916 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -16,6 +16,3 @@ RUN make deps # Development dependences RUN go get github.com/githubnemo/CompileDaemon RUN go install github.com/githubnemo/CompileDaemon - -RUN adduser -D -u 1000 netlify -USER netlify diff --git a/Makefile b/Makefile index c4ae9f4e53..014286e5fe 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,6 @@ deps: ## Install dependencies. @go install golang.org/x/lint/golint@latest @go mod download -image: ## Build the production Docker image. - docker build . - lint: ## Lint the code. golint $(CHECK_FILES) diff --git a/hack/migrate.sh b/hack/migrate.sh index 06c55f9728..2d1f0e5e84 100755 --- a/hack/migrate.sh +++ b/hack/migrate.sh @@ -9,7 +9,4 @@ export GOTRUE_DB_DRIVER="postgres" export GOTRUE_DB_DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/$DB_ENV" export GOTRUE_DB_MIGRATIONS_PATH=$DIR/../migrations -echo soda -v -soda drop -d -e $DB_ENV -c $DATABASE -soda create -d -e $DB_ENV -c $DATABASE go run main.go migrate -c $DIR/test.env diff --git a/hack/migrate_postgres.sh b/hack/migrate_postgres.sh deleted file mode 100755 index 59a8e80c2d..0000000000 --- a/hack/migrate_postgres.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -DB_ENV=$1 - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -DATABASE="$DIR/database.yml" - -export GOTRUE_DB_DRIVER="postgres" -export GOTRUE_DB_DATABASE_URL="postgres://postgres:root@localhost:5432/$DB_ENV?sslmode=disable" -export GOTRUE_DB_MIGRATIONS_PATH=$DIR/../migrations - -go run main.go migrate -c $DIR/test.env From 478166c6d0114f6ce2b7cd926d1841583812d317 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 10 May 2022 17:08:12 -0700 Subject: [PATCH 063/102] update contributing guide --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0421ac48dc..b196dbe2b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ GoTrue has a development container setup that makes it easy to get started contr If you would like to run GoTrue locally or learn more about what these containers are doing for you, continue reading the [Setup and Tooling](#setup-and-tooling) section below. Otherwise, you can skip ahead to the [How To Verify that GoTrue is Available](#how-to-verify-that-gotrue-is-available) section to learn about working with and developing GoTrue. -Before using the containers, you will need to make sure an `.env` file exists by making a copy of `example.env` and configuring it for your needs. A required change for the containers is that the `DATABASE_URL` be changed to `postgres://supabase_auth_admin:root@localhost:5432/postgres` where the address of the Postgres container is changed to `localhost`. +Before using the containers, you will need to make sure an `.env.docker` file exists by making a copy of `example.docker.env` and configuring it for your needs. The set of env vars in `example.docker.env` only contain the necessary env vars for gotrue to start in a docker environment. For the full list of env vars, please refer to `example.env` and copy over the necessary ones into your `.env.docker` file. The following are some basic commands. A full and up to date list of commands can be found in the project's `Makefile` or by running `make help`. From 05627a286129d2bc729664626a33f40bd9c89fab Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 10 May 2022 17:09:03 -0700 Subject: [PATCH 064/102] simplify make dev cmd --- Makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 014286e5fe..9ff0941966 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,7 @@ vet: # Vet the code go vet $(CHECK_FILES) dev: ## Run the development containers - # Start postgres then gotrue - docker-compose -f $(DEV_DOCKER_COMPOSE) up -d postgres - docker-compose -f $(DEV_DOCKER_COMPOSE) up -d gotrue - docker-compose -f $(DEV_DOCKER_COMPOSE) logs -f + docker-compose -f $(DEV_DOCKER_COMPOSE) up down: ## Shutdown the development containers # Start postgres first and apply migrations From 960c70ca4fbe9ca6630894ab9facd4903e365ee7 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 10 May 2022 17:16:33 -0700 Subject: [PATCH 065/102] add quick start instructions for docker --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 322b8ef52c..a057dfd58b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,15 @@ Create a `.env` file to store your own custom env vars. See [`example.env`](exam go build -ldflags "-X github.com/supabase/gotrue/cmd.Version=`git rev-parse HEAD`" GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/supabase/gotrue/cmd.Version=`git rev-parse HEAD`" -o gotrue-arm64 ``` -3. Execute the gotrue binary: `./gotrue` (if you're on x86) `./gotrue-arm64` (if you're on arm) +3. Execute the gotrue binary: `./gotrue` + +### If you have docker installed... +Create a `.env.docker` file to store your own custom env vars. See [`example.docker.env`](example.docker.env) + +1. `make build` +2. `make dev` +3. `docker ps` should show 2 docker containers (`gotrue_postgresql` and `gotrue_gotrue`) +4. That's it! Visit the [health checkendpoint](http://localhost:9999/health) to confirm that gotrue is running. ## Configuration From 1685bf29664011c757b7e8b557c49e92641e810a Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 10 May 2022 18:01:39 -0700 Subject: [PATCH 066/102] fix: add http timeout to add external provider requests (#471) * fix: add http timeout to add external provider requests * read timeout from env --- api/provider/apple.go | 1 - api/provider/notion.go | 2 +- api/provider/provider.go | 17 +++++++++++++++++ api/provider/twitch.go | 2 +- api/sms_provider/messagebird.go | 2 +- api/sms_provider/sms_provider.go | 16 ++++++++++++++++ api/sms_provider/textlocal.go | 2 +- api/sms_provider/twilio.go | 2 +- api/sms_provider/vonage.go | 2 +- 9 files changed, 39 insertions(+), 7 deletions(-) diff --git a/api/provider/apple.go b/api/provider/apple.go index 148841af9f..e04585a4af 100644 --- a/api/provider/apple.go +++ b/api/provider/apple.go @@ -32,7 +32,6 @@ const ( // AppleProvider stores the custom config for apple provider type AppleProvider struct { *oauth2.Config - httpClient *http.Client UserInfoURL string } diff --git a/api/provider/notion.go b/api/provider/notion.go index 877fb6711e..db8f84d024 100644 --- a/api/provider/notion.go +++ b/api/provider/notion.go @@ -77,7 +77,7 @@ func (g notionProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us req.Header.Set("Notion-Version", notionApiVersion) req.Header.Set("Authorization", "Bearer "+tok.AccessToken) - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} resp, err := client.Do(req) if err != nil { diff --git a/api/provider/provider.go b/api/provider/provider.go index d06659e723..426f1875d1 100644 --- a/api/provider/provider.go +++ b/api/provider/provider.go @@ -5,11 +5,27 @@ import ( "context" "encoding/json" "io/ioutil" + "log" "net/http" + "os" + "time" "golang.org/x/oauth2" ) +var defaultTimeout time.Duration = time.Second * 10 + +func init() { + timeoutStr := os.Getenv("GOTRUE_INTERNAL_HTTP_TIMEOUT") + if timeoutStr != "" { + if timeout, err := time.ParseDuration(timeoutStr); err != nil { + log.Fatalf("error loading GOTRUE_INTERNAL_HTTP_TIMEOUT: %v", err.Error()) + } else if timeout != 0 { + defaultTimeout = timeout + } + } +} + type Claims struct { // Reserved claims Issuer string `json:"iss,omitempty"` @@ -103,6 +119,7 @@ func chooseHost(base, defaultHost string) string { func makeRequest(ctx context.Context, tok *oauth2.Token, g *oauth2.Config, url string, dst interface{}) error { client := g.Client(ctx, tok) + client.Timeout = defaultTimeout res, err := client.Get(url) if err != nil { return err diff --git a/api/provider/twitch.go b/api/provider/twitch.go index f4c52d7a01..517109c76f 100644 --- a/api/provider/twitch.go +++ b/api/provider/twitch.go @@ -92,7 +92,7 @@ func (t twitchProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us req.Header.Set("Client-Id", t.Config.ClientID) req.Header.Set("Authorization", "Bearer "+tok.AccessToken) - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} resp, err := client.Do(req) if err != nil { diff --git a/api/sms_provider/messagebird.go b/api/sms_provider/messagebird.go index 88f61df4c9..4520f28966 100644 --- a/api/sms_provider/messagebird.go +++ b/api/sms_provider/messagebird.go @@ -64,7 +64,7 @@ func (t *MessagebirdProvider) SendSms(phone string, message string) error { "datacoding": {"unicode"}, } - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode())) if err != nil { return err diff --git a/api/sms_provider/sms_provider.go b/api/sms_provider/sms_provider.go index 11b3b3f2bd..7d535133e1 100644 --- a/api/sms_provider/sms_provider.go +++ b/api/sms_provider/sms_provider.go @@ -2,10 +2,26 @@ package sms_provider import ( "fmt" + "log" + "os" + "time" "github.com/netlify/gotrue/conf" ) +var defaultTimeout time.Duration = time.Second * 10 + +func init() { + timeoutStr := os.Getenv("GOTRUE_INTERNAL_HTTP_TIMEOUT") + if timeoutStr != "" { + if timeout, err := time.ParseDuration(timeoutStr); err != nil { + log.Fatalf("error loading GOTRUE_INTERNAL_HTTP_TIMEOUT: %v", err.Error()) + } else if timeout != 0 { + defaultTimeout = timeout + } + } +} + type SmsProvider interface { SendSms(phone, message string) error } diff --git a/api/sms_provider/textlocal.go b/api/sms_provider/textlocal.go index 09e30f234b..ac0f4b164a 100644 --- a/api/sms_provider/textlocal.go +++ b/api/sms_provider/textlocal.go @@ -52,7 +52,7 @@ func (t *TextlocalProvider) SendSms(phone string, message string) error { "numbers": {phone}, } - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode())) if err != nil { return err diff --git a/api/sms_provider/twilio.go b/api/sms_provider/twilio.go index a77d9c1072..cfe2ff044e 100644 --- a/api/sms_provider/twilio.go +++ b/api/sms_provider/twilio.go @@ -62,7 +62,7 @@ func (t *TwilioProvider) SendSms(phone string, message string) error { "Body": {message}, } - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode())) if err != nil { return err diff --git a/api/sms_provider/vonage.go b/api/sms_provider/vonage.go index fdc3752584..75ed3577a8 100644 --- a/api/sms_provider/vonage.go +++ b/api/sms_provider/vonage.go @@ -52,7 +52,7 @@ func (t *VonageProvider) SendSms(phone string, message string) error { "api_secret": {t.Config.ApiSecret}, } - client := &http.Client{} + client := &http.Client{Timeout: defaultTimeout} r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode())) if err != nil { return err From 8689fd206f2c5d678880c2254ddf85b53fa22485 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 11 May 2022 11:34:58 -0700 Subject: [PATCH 067/102] fix: do not exclude updated_at (#473) --- storage/dial.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/storage/dial.go b/storage/dial.go index 5761981941..569063fd44 100644 --- a/storage/dial.go +++ b/storage/dial.go @@ -73,6 +73,10 @@ func getExcludedColumns(model interface{}, includeColumns ...string) ([]string, xcols := make([]string, len(cols.Cols)) for n := range cols.Cols { + // gobuffalo updates the updated_at column automatically + if n == "updated_at" { + continue + } xcols = append(xcols, n) } return xcols, nil From e781ee9e6ad5eab1abfbe431e93435857f5908cb Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 26 May 2022 01:37:23 -0700 Subject: [PATCH 068/102] chore: dependabot --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c4c055d103..c0ef454b31 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.12.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df - gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect ) go 1.13 diff --git a/go.sum b/go.sum index 9e930f3db4..18af3b95a2 100644 --- a/go.sum +++ b/go.sum @@ -923,6 +923,8 @@ gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From e22cbc71878f052acff1e6f0aff77f52a0749488 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 31 May 2022 16:29:17 -0700 Subject: [PATCH 069/102] fix: add auth.jwt() function (#484) --- .../20220531120530_add_auth_jwt_function.up.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 migrations/20220531120530_add_auth_jwt_function.up.sql diff --git a/migrations/20220531120530_add_auth_jwt_function.up.sql b/migrations/20220531120530_add_auth_jwt_function.up.sql new file mode 100644 index 0000000000..3e44c358a7 --- /dev/null +++ b/migrations/20220531120530_add_auth_jwt_function.up.sql @@ -0,0 +1,12 @@ +-- add auth.jwt function + +comment on function auth.uid() is 'Deprecated. Use auth.jwt() -> ''sub'' instead.'; +comment on function auth.role() is 'Deprecated. Use auth.jwt() -> ''role'' instead.'; +comment on function auth.email() is 'Deprecated. Use auth.jwt() -> ''email'' instead.'; + +create or replace function auth.jwt() +returns jsonb +language sql stable +as $$ + select nullif(current_setting('request.jwt.claims', true), '')::jsonb +$$; From ac93ce70be8093af8279d9dedd1dccd3b5f88bef Mon Sep 17 00:00:00 2001 From: Michael Blickenstorfer Date: Wed, 1 Jun 2022 17:24:50 +0200 Subject: [PATCH 070/102] fix$docker-compose: add double quotes to command Running the make dev command fails without double quotes at the '-pattern' parameter --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 49f4eb9cc4..831d8eb635 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -13,7 +13,7 @@ services: - GOTRUE_DB_MIGRATIONS_PATH=/go/src/github.com/netlify/gotrue/migrations volumes: - ./:/go/src/github.com/netlify/gotrue - command: CompileDaemon --build="make build" --directory=/go/src/github.com/netlify/gotrue --recursive=true -pattern=(.+\.go|.+\.env) -exclude=gotrue -exclude=gotrue-arm64 -exclude=.env --command="/go/src/github.com/netlify/gotrue/gotrue -c=.env.docker" + command: CompileDaemon --build="make build" --directory=/go/src/github.com/netlify/gotrue --recursive=true -pattern="(.+\.go|.+\.env)" -exclude=gotrue -exclude=gotrue-arm64 -exclude=.env --command="/go/src/github.com/netlify/gotrue/gotrue -c=.env.docker" postgres: image: postgres:13 container_name: postgres From e01be0dfbde07a3cb1ac3bb8258687940f77d0ae Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Wed, 1 Jun 2022 15:28:59 -0700 Subject: [PATCH 071/102] fix: log auth actions (#479) --- models/audit_log_entry.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 72bf6e9770..49755242b0 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -8,6 +8,7 @@ import ( "github.com/gofrs/uuid" "github.com/netlify/gotrue/storage" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) type AuditAction string @@ -96,7 +97,12 @@ func NewAuditLogEntry(tx *storage.Connection, instanceID uuid.UUID, actor *User, l.Payload["traits"] = traits } - return errors.Wrap(tx.Create(&l), "Database error creating audit log entry") + if err := tx.Create(&l); err != nil { + return errors.Wrap(err, "Database error creating audit log entry") + } + + logrus.Infof("{\"actor_id\": %v, \"action\": %v, \"timestamp\": %v, \"log_type\": %v}", actor.ID, action, l.Payload["timestamp"], actionLogTypeMap[action]) + return nil } func FindAuditLogEntries(tx *storage.Connection, instanceID uuid.UUID, filterColumns []string, filterValue string, pageParams *Pagination) ([]*AuditLogEntry, error) { From 0d5d5999506fdef11d2ea116bc4e5f58068b1e3c Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 3 Jun 2022 08:14:39 -0700 Subject: [PATCH 072/102] fix: update auth.jwt function (#488) --- migrations/20220531120530_add_auth_jwt_function.up.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/migrations/20220531120530_add_auth_jwt_function.up.sql b/migrations/20220531120530_add_auth_jwt_function.up.sql index 3e44c358a7..1ddc69a2ef 100644 --- a/migrations/20220531120530_add_auth_jwt_function.up.sql +++ b/migrations/20220531120530_add_auth_jwt_function.up.sql @@ -8,5 +8,9 @@ create or replace function auth.jwt() returns jsonb language sql stable as $$ - select nullif(current_setting('request.jwt.claims', true), '')::jsonb + select + coalesce( + nullif(current_setting('request.jwt.claim', true), ''), + nullif(current_setting('request.jwt.claims', true), '') + )::jsonb $$; From e01241563be141295dad55ce85525bfb6fa1fbb3 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 3 Jun 2022 08:15:46 -0700 Subject: [PATCH 073/102] docs: update uri_allow_list description (#487) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fcb9fd152..292e68fb44 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ The base URL your site is located at. Currently used in combination with other s `URI_ALLOW_LIST` - `string` -A comma separated list of URIs (e.g. "https://supabase.io/welcome,io.supabase.gotruedemo://logincallback") which are permitted as valid `redirect_to` destinations, in addition to SITE_URL. Defaults to []. Supports wildcard matching trough globbing. e.g. add `*.mydomain.com/welcome` -> `x.mydomain.com/welcome` and `y.mydomain.com/welcome` would be accepted. +A comma separated list of URIs (e.g. `"https://foo.example.com,https://*.foo.example.com,https://bar.example.com"`) which are permitted as valid `redirect_to` destinations. Defaults to []. Supports wildcard matching through globbing. e.g. `https://*.foo.example.com` will allow `https://a.foo.example.com` and `https://b.foo.example.com` to be accepted. Globbing is also supported on subdomains. e.g. `https://foo.example.com/*` will allow `https://foo.example.com/page1` and `https://foo.example.com/page2` to be accepted. + +For more common glob patterns, check out the [following link](https://pkg.go.dev/github.com/gobwas/glob#Compile). `OPERATOR_TOKEN` - `string` _Multi-instance mode only_ From 8ef6798b45f9c0d683ec20860d0e04e4d4000d9d Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Tue, 7 Jun 2022 07:58:53 +0800 Subject: [PATCH 074/102] fix: bubble up specific publicly accessible Postgres error messages (#404) * Add `github.com/jackc/pgconn` and `github.com/jackc/pgerrcode` * Utilities: add Postgres error handling utilities * OAuth redirect: provide error message from publicly accessible Postgres error if available * API: provide better error messages for accessible Postgres errors * Include integrity constraint violations Co-authored-by: Kang Ming --- api/errors.go | 10 ++++++ api/external.go | 15 +++++++-- go.mod | 2 ++ go.sum | 2 ++ utilities/postgres.go | 72 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 utilities/postgres.go diff --git a/api/errors.go b/api/errors.go index 26dd37f022..335dc4e7d7 100644 --- a/api/errors.go +++ b/api/errors.go @@ -8,6 +8,7 @@ import ( "runtime/debug" "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/utilities" "github.com/pkg/errors" ) @@ -249,6 +250,15 @@ func handleError(err error, w http.ResponseWriter, r *http.Request) { } else { log.WithError(e.Cause()).Info(e.Error()) } + + // Provide better error messages for certain user-triggered Postgres errors. + if pgErr := utilities.NewPostgresError(e.InternalError); pgErr != nil { + if jsonErr := sendJSON(w, pgErr.HttpStatusCode, pgErr); jsonErr != nil { + handleError(jsonErr, w, r) + } + return + } + if jsonErr := sendJSON(w, e.Code, e); jsonErr != nil { handleError(jsonErr, w, r) } diff --git a/api/external.go b/api/external.go index b2f2d35a00..ad25b71192 100644 --- a/api/external.go +++ b/api/external.go @@ -16,6 +16,7 @@ import ( "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/utilities" "github.com/sirupsen/logrus" ) @@ -465,8 +466,18 @@ func getErrorQueryString(err error, errorID string, log logrus.FieldLogger) *url case ErrorCause: return getErrorQueryString(e.Cause(), errorID, log) default: - q.Set("error", "server_error") - q.Set("error_description", err.Error()) + error_type, error_description := "server_error", err.Error() + + // Provide better error messages for certain user-triggered Postgres errors. + if pgErr := utilities.NewPostgresError(e); pgErr != nil { + error_description = pgErr.Message + if oauthErrorType, ok := oauthErrorMap[pgErr.HttpStatusCode]; ok { + error_type = oauthErrorType + } + } + + q.Set("error", error_type) + q.Set("error_description", error_description) } return &q } diff --git a/go.mod b/go.mod index c0ef454b31..2746cd1388 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,8 @@ require ( github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.1 github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639 + github.com/jackc/pgconn v1.8.0 // indirect + github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 // indirect github.com/jackc/pgproto3/v2 v2.0.7 // indirect github.com/jmoiron/sqlx v1.3.1 // indirect github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index 18af3b95a2..e059f2cb73 100644 --- a/go.sum +++ b/go.sum @@ -288,6 +288,8 @@ github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpT github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo= github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 h1:WAvSpGf7MsFuzAtK4Vk7R4EVe+liW4x83r4oWu0WHKw= +github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= diff --git a/utilities/postgres.go b/utilities/postgres.go new file mode 100644 index 0000000000..08a57b6ad7 --- /dev/null +++ b/utilities/postgres.go @@ -0,0 +1,72 @@ +package utilities + +import ( + "errors" + "strconv" + "strings" + + "github.com/jackc/pgconn" + "github.com/jackc/pgerrcode" +) + +// PostgresError is a custom error struct for marshalling Postgres errors to JSON. +type PostgresError struct { + Code string `json:"code"` + HttpStatusCode int `json:"-"` + Message string `json:"message"` + Hint string `json:"hint,omitempty"` + Detail string `json:"detail,omitempty"` +} + +// NewPostgresError returns a new PostgresError if the error was from a publicly +// accessible Postgres error. +func NewPostgresError(err error) *PostgresError { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && isPubliclyAccessiblePostgresError(pgErr.Code) { + return &PostgresError{ + Code: pgErr.Code, + HttpStatusCode: getHttpStatusCodeFromPostgresErrorCode(pgErr.Code), + Message: pgErr.Message, + Detail: pgErr.Detail, + Hint: pgErr.Hint, + } + } + + return nil +} + +// isPubliclyAccessiblePostgresError checks if the Postgres error should be +// made accessible. +func isPubliclyAccessiblePostgresError(code string) bool { + if len(code) != 5 { + return false + } + + // default response + return getHttpStatusCodeFromPostgresErrorCode(code) != 0 +} + +// getHttpStatusCodeFromPostgresErrorCode maps a Postgres error code to a HTTP +// status code. Returns 0 if the code doesn't map to a given postgres error code. +func getHttpStatusCodeFromPostgresErrorCode(code string) int { + if code == pgerrcode.RaiseException || + code == pgerrcode.IntegrityConstraintViolation || + code == pgerrcode.RestrictViolation || + code == pgerrcode.NotNullViolation || + code == pgerrcode.ForeignKeyViolation || + code == pgerrcode.UniqueViolation || + code == pgerrcode.CheckViolation || + code == pgerrcode.ExclusionViolation { + return 500 + } + + // Use custom HTTP status code if Postgres error was triggered with `PTXXX` + // code. This is consistent with PostgREST's behaviour as well. + if strings.HasPrefix(code, "PT") { + if httpStatusCode, err := strconv.ParseInt(code[2:], 10, 0); err == nil { + return int(httpStatusCode) + } + } + + return 0 +} From c353dbb600554bb635c1b722f17dbe8d79438303 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Tue, 7 Jun 2022 08:52:08 +0800 Subject: [PATCH 075/102] fix: add configurable hcaptcha timeout (#441) * initial commit * feat: minor fixes * fix: directly use env var * fix: run gofmt * chore: remove context import * Update test.env * Update example.env Co-authored-by: Joel Lee --- README.md | 3 ++- api/middleware.go | 1 + api/phone.go | 2 +- example.env | 1 + hack/test.env | 1 + security/hcaptcha.go | 15 +++++++++++++-- 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 292e68fb44..e5471f506d 100644 --- a/README.md +++ b/README.md @@ -509,7 +509,8 @@ Whether captcha middleware is enabled for now the only option supported is: `hcaptcha` -`SECURITY_CAPTCHA_SECRET` - `string` +- `SECURITY_CAPTCHA_SECRET` - `string` +- `SECURITY_CAPTCHA_TIMEOUT` - `string` Retrieve from hcaptcha account diff --git a/api/middleware.go b/api/middleware.go index 54e6fdc07e..a010e70a94 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -234,6 +234,7 @@ func (a *API) verifyCaptcha(w http.ResponseWriter, req *http.Request) (context.C if secret == "" { return nil, internalServerError("server misconfigured") } + verificationResult, err := security.VerifyRequest(req, secret) if err != nil { logrus.WithField("err", err).Infof("failed to validate result") diff --git a/api/phone.go b/api/phone.go index 98bcf9f833..814244b99b 100644 --- a/api/phone.go +++ b/api/phone.go @@ -30,7 +30,7 @@ func (a *API) validatePhone(phone string) (string, error) { return phone, nil } -// validateE165Format checks if phone number follows the E.164 format +// validateE164Format checks if phone number follows the E.164 format func (a *API) validateE164Format(phone string) bool { // match should never fail as long as regexp is valid matched, _ := regexp.Match(e164Format, []byte(phone)) diff --git a/example.env b/example.env index be0e63122b..e707c114cd 100644 --- a/example.env +++ b/example.env @@ -178,6 +178,7 @@ GOTRUE_SMS_VONAGE_FROM="" GOTRUE_SECURITY_CAPTCHA_ENABLED="false" GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha" GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000" +GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s" GOTRUE_SESSION_KEY="" # SAML config diff --git a/hack/test.env b/hack/test.env index d0137c9c2b..b1dcd884ab 100644 --- a/hack/test.env +++ b/hack/test.env @@ -94,3 +94,4 @@ GOTRUE_TRACING_TAGS="env:test" GOTRUE_SECURITY_CAPTCHA_ENABLED="false" GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha" GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000" +GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s" diff --git a/security/hcaptcha.go b/security/hcaptcha.go index 4017f85710..f34c7d61de 100644 --- a/security/hcaptcha.go +++ b/security/hcaptcha.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "net/url" + "os" "strconv" "strings" "time" @@ -40,8 +42,17 @@ const ( var Client *http.Client func init() { - // TODO (darora): make timeout configurable - Client = &http.Client{Timeout: 10 * time.Second} + var defaultTimeout time.Duration = time.Second * 10 + timeoutStr := os.Getenv("GOTRUE_SECURITY_CAPTCHA_TIMEOUT") + if timeoutStr != "" { + if timeout, err := time.ParseDuration(timeoutStr); err != nil { + log.Fatalf("error loading GOTRUE_SECURITY_CAPTCHA_TIMEOUT: %v", err.Error()) + } else if timeout != 0 { + defaultTimeout = timeout + } + } + + Client = &http.Client{Timeout: defaultTimeout} } func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error) { From 94b5e8576625b357b35b0f151ab517cb1e21f1f9 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 10 Jun 2022 14:25:32 +0800 Subject: [PATCH 076/102] refactor: add phone param --- api/admin.go | 5 +---- api/admin_test.go | 32 ++++++++++++++++---------------- api/audit_test.go | 4 ++-- api/external_test.go | 3 ++- api/hook_test.go | 4 ++-- api/invite_test.go | 4 ++-- api/phone_test.go | 3 +-- api/provider/saml.go | 2 +- api/recover_test.go | 2 +- api/signup.go | 7 +++---- api/signup_test.go | 2 +- api/token_test.go | 6 +++--- api/user_test.go | 8 +++----- api/verify_test.go | 3 +-- cmd/admin_cmd.go | 5 +++-- cmd/serve_cmd.go | 2 +- models/identity_test.go | 4 ++-- models/refresh_token_test.go | 4 ++-- models/user.go | 4 ++-- models/user_test.go | 6 +++--- 20 files changed, 52 insertions(+), 58 deletions(-) diff --git a/api/admin.go b/api/admin.go index c2d94e93a6..0ec1aeb959 100644 --- a/api/admin.go +++ b/api/admin.go @@ -250,13 +250,10 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error { params.Password = &password } - user, err := models.NewUser(instanceID, params.Email, *params.Password, aud, params.UserMetaData) + user, err := models.NewUser(instanceID, params.Phone, params.Email, *params.Password, aud, params.UserMetaData) if err != nil { return internalServerError("Error creating user").WithInternalError(err) } - if params.Phone != "" { - user.Phone = storage.NullString(params.Phone) - } if user.AppMetaData == nil { user.AppMetaData = make(map[string]interface{}) } diff --git a/api/admin_test.go b/api/admin_test.go index 9c421630ab..b6e450c690 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -49,7 +49,7 @@ func (ts *AdminTestSuite) SetupTest() { } func (ts *AdminTestSuite) makeSuperAdmin(email string) string { - u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) + u, err := models.NewUser(ts.instanceID, "9123456", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) require.NoError(ts.T(), err, "Error making new user") u.Role = "supabase_admin" @@ -108,11 +108,11 @@ func (ts *AdminTestSuite) TestAdminUsers() { // TestAdminUsers tests API /admin/users route func (ts *AdminTestSuite) TestAdminUsers_Pagination() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - u, err = models.NewUser(ts.instanceID, "test2@example.com", "test", ts.Config.JWT.Aud, nil) + u, err = models.NewUser(ts.instanceID, "987654321", "test2@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -137,13 +137,13 @@ func (ts *AdminTestSuite) TestAdminUsers_Pagination() { // TestAdminUsers tests API /admin/users route func (ts *AdminTestSuite) TestAdminUsers_SortAsc() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") // if the created_at times are the same, then the sort order is not guaranteed time.Sleep(1 * time.Second) - u, err = models.NewUser(ts.instanceID, "test2@example.com", "test", ts.Config.JWT.Aud, nil) + u, err = models.NewUser(ts.instanceID, "", "test2@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -172,13 +172,13 @@ func (ts *AdminTestSuite) TestAdminUsers_SortAsc() { // TestAdminUsers tests API /admin/users route func (ts *AdminTestSuite) TestAdminUsers_SortDesc() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") // if the created_at times are the same, then the sort order is not guaranteed time.Sleep(1 * time.Second) - u, err = models.NewUser(ts.instanceID, "test2@example.com", "test", ts.Config.JWT.Aud, nil) + u, err = models.NewUser(ts.instanceID, "987654321", "test2@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -204,7 +204,7 @@ func (ts *AdminTestSuite) TestAdminUsers_SortDesc() { // TestAdminUsers tests API /admin/users route func (ts *AdminTestSuite) TestAdminUsers_FilterEmail() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -229,11 +229,11 @@ func (ts *AdminTestSuite) TestAdminUsers_FilterEmail() { // TestAdminUsers tests API /admin/users route func (ts *AdminTestSuite) TestAdminUsers_FilterName() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) + u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - u, err = models.NewUser(ts.instanceID, "test2@example.com", "test", ts.Config.JWT.Aud, nil) + u, err = models.NewUser(ts.instanceID, "", "test2@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -354,7 +354,7 @@ func (ts *AdminTestSuite) TestAdminUserCreate() { // TestAdminUserGet tests API /admin/user route (GET) func (ts *AdminTestSuite) TestAdminUserGet() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test Get User"}) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test Get User"}) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -380,7 +380,7 @@ func (ts *AdminTestSuite) TestAdminUserGet() { // TestAdminUserUpdate tests API /admin/user route (UPDATE) func (ts *AdminTestSuite) TestAdminUserUpdate() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -421,7 +421,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdate() { // TestAdminUserUpdate tests API /admin/user route (UPDATE) as system user func (ts *AdminTestSuite) TestAdminUserUpdateAsSystemUser() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -466,7 +466,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdateAsSystemUser() { } func (ts *AdminTestSuite) TestAdminUserUpdatePasswordFailed() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -489,7 +489,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdatePasswordFailed() { } func (ts *AdminTestSuite) TestAdminUserUpdateBannedUntilFailed() { - u, err := models.NewUser(ts.instanceID, "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -513,7 +513,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdateBannedUntilFailed() { // TestAdminUserDelete tests API /admin/user route (DELETE) func (ts *AdminTestSuite) TestAdminUserDelete() { - u, err := models.NewUser(ts.instanceID, "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") diff --git a/api/audit_test.go b/api/audit_test.go index b6a3fdd08b..dae7bc4513 100644 --- a/api/audit_test.go +++ b/api/audit_test.go @@ -46,7 +46,7 @@ func (ts *AuditTestSuite) SetupTest() { } func (ts *AuditTestSuite) makeSuperAdmin(email string) string { - u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) + u, err := models.NewUser(ts.instanceID, "", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) require.NoError(ts.T(), err, "Error making new user") u.Role = "supabase_admin" @@ -121,7 +121,7 @@ func (ts *AuditTestSuite) TestAuditFilters() { func (ts *AuditTestSuite) prepareDeleteEvent() { // DELETE USER - u, err := models.NewUser(ts.instanceID, "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") diff --git a/api/external_test.go b/api/external_test.go index d6260e24c4..bbef2fb2ca 100644 --- a/api/external_test.go +++ b/api/external_test.go @@ -47,7 +47,8 @@ func (ts *ExternalTestSuite) createUser(providerId string, email string, name st require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user") } - u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"provider_id": providerId, "full_name": name, "avatar_url": avatar}) + // TODO: [Joel] -- refactor to take in phone + u, err := models.NewUser(ts.instanceID, "", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"provider_id": providerId, "full_name": name, "avatar_url": avatar}) if confirmationToken != "" { u.ConfirmationToken = confirmationToken diff --git a/api/hook_test.go b/api/hook_test.go index f60a0218c4..26a6e7ce1a 100644 --- a/api/hook_test.go +++ b/api/hook_test.go @@ -25,7 +25,7 @@ func TestSignupHookSendInstanceID(t *testing.T) { require.NoError(t, err) iid := uuid.Must(uuid.NewV4()) - user, err := models.NewUser(iid, "test@truth.com", "thisisapassword", "", nil) + user, err := models.NewUser(iid, "81234567", "test@truth.com", "thisisapassword", "", nil) require.NoError(t, err) var callCount int @@ -68,7 +68,7 @@ func TestSignupHookFromClaims(t *testing.T) { require.NoError(t, err) iid := uuid.Must(uuid.NewV4()) - user, err := models.NewUser(iid, "test@truth.com", "thisisapassword", "", nil) + user, err := models.NewUser(iid, "", "test@truth.com", "thisisapassword", "", nil) require.NoError(t, err) var callCount int diff --git a/api/invite_test.go b/api/invite_test.go index 8d105213c4..e4359168c5 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -55,7 +55,7 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string { require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user") } - u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) + u, err := models.NewUser(ts.instanceID, "123456789", email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": "Test User"}) require.NoError(ts.T(), err, "Error making new user") u.Role = "supabase_admin" @@ -145,7 +145,7 @@ func (ts *InviteTestSuite) TestVerifyInvite() { for _, c := range cases { ts.Run(c.desc, func() { - user, err := models.NewUser(ts.instanceID, c.email, "", ts.Config.JWT.Aud, nil) + user, err := models.NewUser(ts.instanceID, "", c.email, "", ts.Config.JWT.Aud, nil) now := time.Now() user.InvitedAt = &now user.ConfirmationSentAt = &now diff --git a/api/phone_test.go b/api/phone_test.go index 878e6256ef..37d94bb6c6 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -54,8 +54,7 @@ func (ts *PhoneTestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user - u, err := models.NewUser(ts.instanceID, "", "password", ts.Config.JWT.Aud, nil) - u.Phone = "123456789" + u, err := models.NewUser(ts.instanceID, "123456789", "", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } diff --git a/api/provider/saml.go b/api/provider/saml.go index 9b5af7b6e2..807438aa00 100644 --- a/api/provider/saml.go +++ b/api/provider/saml.go @@ -20,11 +20,11 @@ import ( "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" + "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" saml2 "github.com/russellhaering/gosaml2" "github.com/russellhaering/gosaml2/types" dsig "github.com/russellhaering/goxmldsig" - "github.com/gofrs/uuid" "golang.org/x/oauth2" ) diff --git a/api/recover_test.go b/api/recover_test.go index 548d772782..9838e3f2d8 100644 --- a/api/recover_test.go +++ b/api/recover_test.go @@ -42,7 +42,7 @@ func (ts *RecoverTestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user - u, err := models.NewUser(ts.instanceID, "test@example.com", "password", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } diff --git a/api/signup.go b/api/signup.go index 73e6b80781..d3101ef0aa 100644 --- a/api/signup.go +++ b/api/signup.go @@ -278,13 +278,12 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param var err error switch params.Provider { case "email": - user, err = models.NewUser(instanceID, params.Email, params.Password, params.Aud, params.Data) + user, err = models.NewUser(instanceID, "", params.Email, params.Password, params.Aud, params.Data) case "phone": - user, err = models.NewUser(instanceID, "", params.Password, params.Aud, params.Data) - user.Phone = storage.NullString(params.Phone) + user, err = models.NewUser(instanceID, params.Phone, "", params.Password, params.Aud, params.Data) default: // handles external provider case - user, err = models.NewUser(instanceID, params.Email, params.Password, params.Aud, params.Data) + user, err = models.NewUser(instanceID, "", params.Email, params.Password, params.Aud, params.Data) } if err != nil { diff --git a/api/signup_test.go b/api/signup_test.go index f06756bc94..aa5137056b 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -233,7 +233,7 @@ func (ts *SignupTestSuite) TestSignupTwice() { } func (ts *SignupTestSuite) TestVerifySignup() { - user, err := models.NewUser(ts.instanceID, "test@example.com", "testing", ts.Config.JWT.Aud, nil) + user, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "testing", ts.Config.JWT.Aud, nil) user.ConfirmationToken = "asdf3" now := time.Now() user.ConfirmationSentAt = &now diff --git a/api/token_test.go b/api/token_test.go index ace039f937..972702b87a 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -46,7 +46,7 @@ func (ts *TokenTestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user & refresh token - u, err := models.NewUser(ts.instanceID, "test@example.com", "password", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "12345678", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") t := time.Now() u.EmailConfirmedAt = &t @@ -151,7 +151,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenGrantFailure() { } func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { - u, err := models.NewUser(ts.instanceID, "foo@example.com", "password", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "foo@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") t := time.Now() u.EmailConfirmedAt = &t @@ -240,7 +240,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { } func (ts *TokenTestSuite) createBannedUser() *models.User { - u, err := models.NewUser(ts.instanceID, "banned@example.com", "password", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "banned@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") t := time.Now() u.EmailConfirmedAt = &t diff --git a/api/user_test.go b/api/user_test.go index 8acc6b6a45..a1c0cc1db7 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -42,8 +42,7 @@ func (ts *UserTestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user - u, err := models.NewUser(ts.instanceID, "test@example.com", "password", ts.Config.JWT.Aud, nil) - u.Phone = "123456789" + u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } @@ -109,7 +108,7 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { for _, c := range cases { ts.Run(c.desc, func() { - u, err := models.NewUser(ts.instanceID, "", "", ts.Config.JWT.Aud, nil) + u, err := models.NewUser(ts.instanceID, "", "", "", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), u.SetEmail(ts.API.db, c.userData["email"]), "Error setting user email") require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"]), "Error setting user phone") @@ -138,8 +137,7 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - existingUser, err := models.NewUser(ts.instanceID, "", "", ts.Config.JWT.Aud, nil) - existingUser.Phone = "22222222" + existingUser, err := models.NewUser(ts.instanceID, "22222222", "", "", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err) require.NoError(ts.T(), ts.API.db.Create(existingUser)) diff --git a/api/verify_test.go b/api/verify_test.go index 896a0f1be7..37350ba853 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -45,8 +45,7 @@ func (ts *VerifyTestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user - u, err := models.NewUser(ts.instanceID, "test@example.com", "password", ts.Config.JWT.Aud, nil) - u.Phone = "12345678" + u, err := models.NewUser(ts.instanceID, "12345678", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } diff --git a/cmd/admin_cmd.go b/cmd/admin_cmd.go index 24a2f30662..3d975c7566 100644 --- a/cmd/admin_cmd.go +++ b/cmd/admin_cmd.go @@ -1,10 +1,10 @@ package cmd import ( + "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" - "github.com/gofrs/uuid" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -83,7 +83,8 @@ func adminCreateUser(globalConfig *conf.GlobalConfiguration, config *conf.Config logrus.Fatalf("Error checking user email: %+v", err) } - user, err := models.NewUser(iid, args[0], args[1], aud, nil) + // TODO(Joel): Update the command + user, err := models.NewUser(iid, "", args[0], args[1], aud, nil) if err != nil { logrus.Fatalf("Error creating new user: %+v", err) } diff --git a/cmd/serve_cmd.go b/cmd/serve_cmd.go index 4b5cbc0d1e..c76f8909ed 100644 --- a/cmd/serve_cmd.go +++ b/cmd/serve_cmd.go @@ -4,10 +4,10 @@ import ( "context" "fmt" + "github.com/gofrs/uuid" "github.com/netlify/gotrue/api" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" - "github.com/gofrs/uuid" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) diff --git a/models/identity_test.go b/models/identity_test.go index c94b6a567f..e34b952422 100644 --- a/models/identity_test.go +++ b/models/identity_test.go @@ -61,7 +61,7 @@ func (ts *IdentityTestSuite) TestFindUserIdentities() { } func (ts *IdentityTestSuite) createUserWithEmail(email string) *User { - user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + user, err := NewUser(uuid.Nil, "", email, "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(user) @@ -71,7 +71,7 @@ func (ts *IdentityTestSuite) createUserWithEmail(email string) *User { } func (ts *IdentityTestSuite) createUserWithIdentity(email string) *User { - user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + user, err := NewUser(uuid.Nil, "", email, "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(user) diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go index 96df7262f5..e5e8790d54 100644 --- a/models/refresh_token_test.go +++ b/models/refresh_token_test.go @@ -3,10 +3,10 @@ package models import ( "testing" + "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" - "github.com/gofrs/uuid" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -79,7 +79,7 @@ func (ts *RefreshTokenTestSuite) createUser() *User { } func (ts *RefreshTokenTestSuite) createUserWithEmail(email string) *User { - user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + user, err := NewUser(uuid.Nil, "", email, "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(user) diff --git a/models/user.go b/models/user.go index 7ede0cf6cd..e52a8f8691 100644 --- a/models/user.go +++ b/models/user.go @@ -68,8 +68,7 @@ type User struct { } // NewUser initializes a new user from an email, password and user data. -// TODO: Refactor NewUser to take in phone as an arg -func NewUser(instanceID uuid.UUID, email, password, aud string, userData map[string]interface{}) (*User, error) { +func NewUser(instanceID uuid.UUID, phone, email, password, aud string, userData map[string]interface{}) (*User, error) { id, err := uuid.NewV4() if err != nil { return nil, errors.Wrap(err, "Error generating unique id") @@ -86,6 +85,7 @@ func NewUser(instanceID uuid.UUID, email, password, aud string, userData map[str ID: id, Aud: aud, Email: storage.NullString(strings.ToLower(email)), + Phone: storage.NullString(strings.ToLower(phone)), UserMetaData: userData, EncryptedPassword: pw, } diff --git a/models/user_test.go b/models/user_test.go index 752adf44de..28771df4f9 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -39,7 +39,7 @@ func TestUser(t *testing.T) { } func (ts *UserTestSuite) TestUpdateAppMetadata() { - u, err := NewUser(uuid.Nil, "", "", "", nil) + u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) require.NoError(ts.T(), u.UpdateAppMetaData(ts.db, make(map[string]interface{}))) @@ -58,7 +58,7 @@ func (ts *UserTestSuite) TestUpdateAppMetadata() { } func (ts *UserTestSuite) TestUpdateUserMetadata() { - u, err := NewUser(uuid.Nil, "", "", "", nil) + u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) require.NoError(ts.T(), u.UpdateUserMetaData(ts.db, make(map[string]interface{}))) @@ -186,7 +186,7 @@ func (ts *UserTestSuite) createUser() *User { } func (ts *UserTestSuite) createUserWithEmail(email string) *User { - user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + user, err := NewUser(uuid.Nil, "", email, "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(user) From 30cdb36328b301c454e5d039247410ad65776c25 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 10 Jun 2022 14:37:55 +0800 Subject: [PATCH 077/102] chore: remove update comment --- cmd/admin_cmd.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/admin_cmd.go b/cmd/admin_cmd.go index 3d975c7566..d14d22cc22 100644 --- a/cmd/admin_cmd.go +++ b/cmd/admin_cmd.go @@ -83,7 +83,6 @@ func adminCreateUser(globalConfig *conf.GlobalConfiguration, config *conf.Config logrus.Fatalf("Error checking user email: %+v", err) } - // TODO(Joel): Update the command user, err := models.NewUser(iid, "", args[0], args[1], aud, nil) if err != nil { logrus.Fatalf("Error creating new user: %+v", err) From 19abe10df5ff10fef0873536f44d8d462c61793f Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 10 Jun 2022 16:13:55 +0800 Subject: [PATCH 078/102] Update user.go --- models/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/user.go b/models/user.go index e52a8f8691..46bba3c4fa 100644 --- a/models/user.go +++ b/models/user.go @@ -85,7 +85,7 @@ func NewUser(instanceID uuid.UUID, phone, email, password, aud string, userData ID: id, Aud: aud, Email: storage.NullString(strings.ToLower(email)), - Phone: storage.NullString(strings.ToLower(phone)), + Phone: storage.NullString(phone), UserMetaData: userData, EncryptedPassword: pw, } From bfaa68ec2412abb44b76838dcfb817e68eb49aed Mon Sep 17 00:00:00 2001 From: Div Arora Date: Tue, 14 Jun 2022 17:12:22 +0800 Subject: [PATCH 079/102] chore: specify an application_name when running migrations This allows [automated] migrations activity to be differentiated from user-initiated activity. --- cmd/migrate_cmd.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/migrate_cmd.go b/cmd/migrate_cmd.go index e0a5b90d56..3b355c9f2f 100644 --- a/cmd/migrate_cmd.go +++ b/cmd/migrate_cmd.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "net/url" "os" @@ -53,9 +54,16 @@ func migrate(cmd *cobra.Command, args []string) { } } + u, err := url.Parse(globalConfig.DB.URL) + processedUrl := globalConfig.DB.URL + if len(u.Query()) != 0 { + processedUrl = fmt.Sprintf("%s&application_name=gotrue_migrations", processedUrl) + } else { + processedUrl = fmt.Sprintf("%s?application_name=gotrue_migrations", processedUrl) + } deets := &pop.ConnectionDetails{ Dialect: globalConfig.DB.Driver, - URL: globalConfig.DB.URL, + URL: processedUrl, } deets.Options = map[string]string{ "migration_table_name": "schema_migrations", From eafe66e0c818bba579752e286af82040ee15b329 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 22:31:13 +0800 Subject: [PATCH 080/102] chore: add IP address field --- api/admin.go | 6 +++--- api/audit.go | 1 - api/external.go | 6 +++--- api/invite.go | 2 +- api/logout.go | 2 +- api/magic_link.go | 2 +- api/mail.go | 4 ++-- api/otp.go | 2 +- api/reauthenticate.go | 2 +- api/recover.go | 2 +- api/signup.go | 12 ++++++------ api/token.go | 8 ++++---- api/user.go | 2 +- api/verify.go | 10 +++++----- ...74223_add_ip_address_to_audit_log.postgres.up.sql | 3 +++ models/audit_log_entry.go | 6 +++--- models/refresh_token.go | 2 +- 17 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql diff --git a/api/admin.go b/api/admin.go index 0ec1aeb959..8c9c620f50 100644 --- a/api/admin.go +++ b/api/admin.go @@ -175,7 +175,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, @@ -270,7 +270,7 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserSignedUpAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserSignedUpAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, @@ -323,7 +323,7 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error { adminUser := getAdminUser(ctx) err := a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserDeletedAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserDeletedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, diff --git a/api/audit.go b/api/audit.go index ea1236798d..13218bbe02 100644 --- a/api/audit.go +++ b/api/audit.go @@ -17,7 +17,6 @@ func (a *API) adminAuditLog(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() instanceID := getInstanceID(ctx) // aud := a.requestAud(ctx, r) - pageParams, err := paginate(r) if err != nil { return badRequestError("Bad Pagination Parameters: %v", err) diff --git a/api/external.go b/api/external.go index ad25b71192..947af3da56 100644 --- a/api/external.go +++ b/api/external.go @@ -245,7 +245,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re return nil } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": providerType, }); terr != nil { return terr @@ -259,7 +259,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re return internalServerError("Error updating user").WithInternalError(terr) } } else { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, "", map[string]interface{}{ "provider": providerType, }); terr != nil { return terr @@ -347,7 +347,7 @@ func (a *API) processInvite(ctx context.Context, tx *storage.Connection, userDat return nil, internalServerError("Database error updating user").WithInternalError(err) } - if err := models.NewAuditLogEntry(tx, instanceID, user, models.InviteAcceptedAction, map[string]interface{}{ + if err := models.NewAuditLogEntry(tx, instanceID, user, models.InviteAcceptedAction, "", map[string]interface{}{ "provider": providerType, }); err != nil { return nil, err diff --git a/api/invite.go b/api/invite.go index 6c13861c89..7ee6fcbe17 100644 --- a/api/invite.go +++ b/api/invite.go @@ -55,7 +55,7 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserInvitedAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserInvitedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, }); terr != nil { diff --git a/api/logout.go b/api/logout.go index 77b4f2fa54..38e298cf4e 100644 --- a/api/logout.go +++ b/api/logout.go @@ -21,7 +21,7 @@ func (a *API) Logout(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, u, models.LogoutAction, nil); terr != nil { + if terr := models.NewAuditLogEntry(tx, instanceID, u, models.LogoutAction, "", nil); terr != nil { return terr } return models.Logout(tx, instanceID, u.ID) diff --git a/api/magic_link.go b/api/magic_link.go index c4d6b58e09..a4c427371f 100644 --- a/api/magic_link.go +++ b/api/magic_link.go @@ -76,7 +76,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } diff --git a/api/mail.go b/api/mail.go index c31249d21d..12444aa19c 100644 --- a/api/mail.go +++ b/api/mail.go @@ -73,7 +73,7 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { var terr error switch params.Type { case "magiclink", "recovery": - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } user.RecoveryToken = crypto.SecureToken() @@ -96,7 +96,7 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { return terr } } - if terr = models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserInvitedAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserInvitedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, }); terr != nil { diff --git a/api/otp.go b/api/otp.go index 395baa95f1..b9cbea984e 100644 --- a/api/otp.go +++ b/api/otp.go @@ -117,7 +117,7 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if err := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); err != nil { + if err := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, "", nil); err != nil { return err } smsProvider, terr := sms_provider.GetSmsProvider(*config) diff --git a/api/reauthenticate.go b/api/reauthenticate.go index 3d8743c99c..530effa326 100644 --- a/api/reauthenticate.go +++ b/api/reauthenticate.go @@ -49,7 +49,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserReauthenticateAction, nil); terr != nil { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserReauthenticateAction, "", nil); terr != nil { return terr } if email != "" { diff --git a/api/recover.go b/api/recover.go index 1701f5b2d4..7c75dcd0af 100644 --- a/api/recover.go +++ b/api/recover.go @@ -46,7 +46,7 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, nil); terr != nil { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } mailer := a.Mailer(ctx) diff --git a/api/signup.go b/api/signup.go index d3101ef0aa..43c0d22c8e 100644 --- a/api/signup.go +++ b/api/signup.go @@ -113,7 +113,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { if params.Provider == "email" && !user.IsConfirmed() { if config.Mailer.Autoconfirm { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -127,7 +127,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { } else { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserConfirmationRequestedAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -143,7 +143,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { } } else if params.Provider == "phone" && !user.IsPhoneConfirmed() { if config.Sms.Autoconfirm { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -155,7 +155,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error updating user").WithInternalError(terr) } } else { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserConfirmationRequestedAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -179,7 +179,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { } if errors.Is(err, UserExistsError) { err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRepeatedSignUpAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserRepeatedSignUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -206,7 +206,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { var token *AccessTokenResponse err = a.db.Transaction(func(tx *storage.Connection) error { var terr error - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr diff --git a/api/token.go b/api/token.go index 1ce341bee8..b1bb6c2d8d 100644 --- a/api/token.go +++ b/api/token.go @@ -219,7 +219,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri var token *AccessTokenResponse err = a.db.Transaction(func(tx *storage.Connection) error { var terr error - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, map[string]interface{}{ + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, "", map[string]interface{}{ "provider": provider, }); terr != nil { return terr @@ -321,7 +321,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h err = a.db.Transaction(func(tx *storage.Connection) error { var terr error - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.TokenRefreshedAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.TokenRefreshedAction, "", nil); terr != nil { return terr } @@ -487,7 +487,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return unauthorizedError("Error unverified email") } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr @@ -501,7 +501,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return internalServerError("Error updating user").WithInternalError(terr) } } else { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr diff --git a/api/user.go b/api/user.go index 389414b4d5..d5e9d757bd 100644 --- a/api/user.go +++ b/api/user.go @@ -161,7 +161,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, "", nil); terr != nil { return internalServerError("Error recording audit log entry").WithInternalError(terr) } diff --git a/api/verify.go b/api/verify.go index 2fcf817c35..22676d7fc2 100644 --- a/api/verify.go +++ b/api/verify.go @@ -246,7 +246,7 @@ func (a *API) signupVerify(ctx context.Context, conn *storage.Connection, user * } } - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", nil); terr != nil { return terr } @@ -275,7 +275,7 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, user return terr } if !user.IsConfirmed() { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", nil); terr != nil { return terr } @@ -286,7 +286,7 @@ func (a *API) recoverVerify(ctx context.Context, conn *storage.Connection, user return terr } } else { - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.LoginAction, "", nil); terr != nil { return terr } if terr = triggerEventHooks(ctx, tx, LoginEvent, user, instanceID, config); terr != nil { @@ -308,7 +308,7 @@ func (a *API) smsVerify(ctx context.Context, conn *storage.Connection, user *mod err := conn.Transaction(func(tx *storage.Connection) error { var terr error - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserSignedUpAction, "", nil); terr != nil { return terr } @@ -380,7 +380,7 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p err := conn.Transaction(func(tx *storage.Connection) error { var terr error - if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, nil); terr != nil { + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, "", nil); terr != nil { return terr } diff --git a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql new file mode 100644 index 0000000000..c600c47b41 --- /dev/null +++ b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql @@ -0,0 +1,3 @@ +-- Add IP Address to audit log +ALTER TABLE auth.audit_log_entries +ADD COLUMN IF NOT EXISTS ip_address VARCHAR(256) NOT NULL DEFAULT ''; diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 49755242b0..597d702d48 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -54,10 +54,9 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ type AuditLogEntry struct { InstanceID uuid.UUID `json:"-" db:"instance_id"` ID uuid.UUID `json:"id" db:"id"` - Payload JSONMap `json:"payload" db:"payload"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + IPAddress string `json:"ip_address" db:"ip_address"` } func (AuditLogEntry) TableName() string { @@ -65,7 +64,7 @@ func (AuditLogEntry) TableName() string { return tableName } -func NewAuditLogEntry(tx *storage.Connection, instanceID uuid.UUID, actor *User, action AuditAction, traits map[string]interface{}) error { +func NewAuditLogEntry(tx *storage.Connection, instanceID uuid.UUID, actor *User, action AuditAction, ipAddress string, traits map[string]interface{}) error { id, err := uuid.NewV4() if err != nil { return errors.Wrap(err, "Error generating unique id") @@ -87,6 +86,7 @@ func NewAuditLogEntry(tx *storage.Connection, instanceID uuid.UUID, actor *User, "action": action, "log_type": actionLogTypeMap[action], }, + IPAddress: ipAddress, } if name, ok := actor.UserMetaData["full_name"]; ok { diff --git a/models/refresh_token.go b/models/refresh_token.go index 2af42c505b..ea73da36d4 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -42,7 +42,7 @@ func GrantRefreshTokenSwap(tx *storage.Connection, user *User, token *RefreshTok var newToken *RefreshToken err := tx.Transaction(func(rtx *storage.Connection) error { var terr error - if terr = NewAuditLogEntry(tx, user.InstanceID, user, TokenRevokedAction, nil); terr != nil { + if terr = NewAuditLogEntry(tx, user.InstanceID, user, TokenRevokedAction, "", nil); terr != nil { return errors.Wrap(terr, "error creating audit log entry") } From 5e10354f2172d2ad7d41a8256c0b309209cf6f06 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 22:53:00 +0800 Subject: [PATCH 081/102] chore: switch db type to inet --- .../20220614074223_add_ip_address_to_audit_log.postgres.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql index c600c47b41..4c40013271 100644 --- a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql +++ b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql @@ -1,3 +1,3 @@ -- Add IP Address to audit log ALTER TABLE auth.audit_log_entries -ADD COLUMN IF NOT EXISTS ip_address VARCHAR(256) NOT NULL DEFAULT ''; +ADD COLUMN IF NOT EXISTS ip_address inet NULL; From b390146dfbaf2860cdd0b4b107ca01393e3cff55 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 23:06:57 +0800 Subject: [PATCH 082/102] Revert "chore: switch db type to inet" This reverts commit 5e10354f2172d2ad7d41a8256c0b309209cf6f06. --- .../20220614074223_add_ip_address_to_audit_log.postgres.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql index 4c40013271..c600c47b41 100644 --- a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql +++ b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql @@ -1,3 +1,3 @@ -- Add IP Address to audit log ALTER TABLE auth.audit_log_entries -ADD COLUMN IF NOT EXISTS ip_address inet NULL; +ADD COLUMN IF NOT EXISTS ip_address VARCHAR(256) NOT NULL DEFAULT ''; From 702a515724686ece3a4763a914f749d599e787ef Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 23:13:25 +0800 Subject: [PATCH 083/102] chore:reduce number of chars permitted --- .../20220614074223_add_ip_address_to_audit_log.postgres.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql index c600c47b41..61e2b649f4 100644 --- a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql +++ b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql @@ -1,3 +1,3 @@ -- Add IP Address to audit log ALTER TABLE auth.audit_log_entries -ADD COLUMN IF NOT EXISTS ip_address VARCHAR(256) NOT NULL DEFAULT ''; +ADD COLUMN IF NOT EXISTS ip_address VARCHAR(32) NOT NULL DEFAULT ''; From df6bf877bd5c8cb8702b94fa08d36b70d601cf46 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 04:40:51 +0800 Subject: [PATCH 084/102] fix: add IP Address to logs --- models/audit_log_entry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 597d702d48..ffa942a765 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -101,7 +101,7 @@ func NewAuditLogEntry(tx *storage.Connection, instanceID uuid.UUID, actor *User, return errors.Wrap(err, "Database error creating audit log entry") } - logrus.Infof("{\"actor_id\": %v, \"action\": %v, \"timestamp\": %v, \"log_type\": %v}", actor.ID, action, l.Payload["timestamp"], actionLogTypeMap[action]) + logrus.Infof("{\"actor_id\": %v, \"action\": %v, \"timestamp\": %v, \"log_type\": %v, \"ip_address\": %v}", actor.ID, action, l.Payload["timestamp"], actionLogTypeMap[action], ipAddress) return nil } From 7568953bc9aee7b6d940b5a2ff767bc96ec144b4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 04:42:43 +0800 Subject: [PATCH 085/102] fix: increase size of ip address field --- .../20220614074223_add_ip_address_to_audit_log.postgres.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql index 61e2b649f4..070e829add 100644 --- a/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql +++ b/migrations/20220614074223_add_ip_address_to_audit_log.postgres.up.sql @@ -1,3 +1,3 @@ -- Add IP Address to audit log ALTER TABLE auth.audit_log_entries -ADD COLUMN IF NOT EXISTS ip_address VARCHAR(32) NOT NULL DEFAULT ''; +ADD COLUMN IF NOT EXISTS ip_address VARCHAR(64) NOT NULL DEFAULT ''; From de088851015a2970310627b6fa739d40a120aaa5 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 23 Jun 2022 21:31:37 +0800 Subject: [PATCH 086/102] ci: add gofmt check --- .github/workflows/test.yml | 2 ++ Makefile | 3 +++ api/admin.go | 2 +- models/audit_log_entry.go | 6 +++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcdc78303e..24c63c3f59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,8 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 + - name: Formatting checks + run: if [ ! -z $(gofmt -l .) ]; then echo 'Make sure to run "go fmt ./..." before commit!' && exit 1; fi - name: Init Database run: psql -f hack/init_postgres.sql postgresql://postgres:root@localhost:5432/postgres - name: Run migrations diff --git a/Makefile b/Makefile index 9ff0941966..745be990fb 100644 --- a/Makefile +++ b/Makefile @@ -53,3 +53,6 @@ docker-build: ## Force a full rebuild of the development containers docker-clean: ## Remove the development containers and volumes docker-compose -f $(DEV_DOCKER_COMPOSE) rm -fsv + +format: + go fmt ./... diff --git a/api/admin.go b/api/admin.go index 8c9c620f50..f9117fef5d 100644 --- a/api/admin.go +++ b/api/admin.go @@ -175,7 +175,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index ffa942a765..7187a694b1 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -54,9 +54,9 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ type AuditLogEntry struct { InstanceID uuid.UUID `json:"-" db:"instance_id"` ID uuid.UUID `json:"id" db:"id"` - Payload JSONMap `json:"payload" db:"payload"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - IPAddress string `json:"ip_address" db:"ip_address"` + Payload JSONMap `json:"payload" db:"payload"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + IPAddress string `json:"ip_address" db:"ip_address"` } func (AuditLogEntry) TableName() string { From 067d0393608bd10703675fafa7f9275ded8974c6 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Sun, 26 Jun 2022 23:21:21 -0700 Subject: [PATCH 087/102] fix: handle all non-2xx errors (#515) --- api/sms_provider/twilio.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/sms_provider/twilio.go b/api/sms_provider/twilio.go index cfe2ff044e..ce29db825e 100644 --- a/api/sms_provider/twilio.go +++ b/api/sms_provider/twilio.go @@ -73,7 +73,7 @@ func (t *TwilioProvider) SendSms(phone string, message string) error { if err != nil { return err } - if res.StatusCode == http.StatusBadRequest || res.StatusCode == http.StatusForbidden { + if res.StatusCode/100 != 2 { resp := &twilioErrResponse{} if err := json.NewDecoder(res.Body).Decode(resp); err != nil { return err From 895644eeda8378bf23b1afe9b8645cb4f12a73d5 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 27 Jun 2022 09:09:54 -0700 Subject: [PATCH 088/102] fix: bump gotrue version (#518) From 56b938e1b556c02cb83d68999ad2e0358d2697c0 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 29 Jun 2022 09:02:27 +0800 Subject: [PATCH 089/102] chore: switch to gofmt -s -w . --- .github/workflows/test.yml | 2 +- Makefile | 2 +- api/admin.go | 2 +- api/audit.go | 6 +++--- api/hook_test.go | 2 +- models/instance.go | 4 ++-- models/user_test.go | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24c63c3f59..e445886cd3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Formatting checks - run: if [ ! -z $(gofmt -l .) ]; then echo 'Make sure to run "go fmt ./..." before commit!' && exit 1; fi + run: if [ ! -z $(gofmt -l .) ]; then echo 'Make sure to run "gofmt -s -w ." before commit!' && exit 1; fi - name: Init Database run: psql -f hack/init_postgres.sql postgresql://postgres:root@localhost:5432/postgres - name: Run migrations diff --git a/Makefile b/Makefile index 745be990fb..6033ff3b74 100644 --- a/Makefile +++ b/Makefile @@ -55,4 +55,4 @@ docker-clean: ## Remove the development containers and volumes docker-compose -f $(DEV_DOCKER_COMPOSE) rm -fsv format: - go fmt ./... + go fmt -s -w . diff --git a/api/admin.go b/api/admin.go index f9117fef5d..2134ce2d3e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -68,7 +68,7 @@ func (a *API) adminUsers(w http.ResponseWriter, r *http.Request) error { return badRequestError("Bad Pagination Parameters: %v", err) } - sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{models.SortField{Name: models.CreatedAt, Dir: models.Descending}}) + sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{{Name: models.CreatedAt, Dir: models.Descending}}) if err != nil { return badRequestError("Bad Sort Parameters: %v", err) } diff --git a/api/audit.go b/api/audit.go index 13218bbe02..b7b9545127 100644 --- a/api/audit.go +++ b/api/audit.go @@ -8,9 +8,9 @@ import ( ) var filterColumnMap = map[string][]string{ - "author": []string{"actor_username", "actor_name"}, - "action": []string{"action"}, - "type": []string{"log_type"}, + "author": {"actor_username", "actor_name"}, + "action": {"action"}, + "type": {"log_type"}, } func (a *API) adminAuditLog(w http.ResponseWriter, r *http.Request) error { diff --git a/api/hook_test.go b/api/hook_test.go index 26a6e7ce1a..fb6a35932b 100644 --- a/api/hook_test.go +++ b/api/hook_test.go @@ -99,7 +99,7 @@ func TestSignupHookFromClaims(t *testing.T) { ctx := context.Background() ctx = withFunctionHooks(ctx, map[string][]string{ - "signup": []string{svr.URL}, + "signup": {svr.URL}, }) require.NoError(t, triggerEventHooks(ctx, conn, SignupEvent, user, iid, config)) diff --git a/models/instance.go b/models/instance.go index 009c456a65..4cd2ba7f0a 100644 --- a/models/instance.go +++ b/models/instance.go @@ -74,8 +74,8 @@ func GetInstanceByUUID(tx *storage.Connection, uuid uuid.UUID) (*Instance, error func DeleteInstance(conn *storage.Connection, instance *Instance) error { return conn.Transaction(func(tx *storage.Connection) error { delModels := map[string]*pop.Model{ - "user": &pop.Model{Value: &User{}}, - "refresh token": &pop.Model{Value: &RefreshToken{}}, + "user": {Value: &User{}}, + "refresh token": {Value: &RefreshToken{}}, } for name, dm := range delModels { diff --git a/models/user_test.go b/models/user_test.go index 28771df4f9..c15b40f725 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -113,7 +113,7 @@ func (ts *UserTestSuite) TestFindUsersInAudience() { sp := &SortParams{ Fields: []SortField{ - SortField{Name: "created_at", Dir: Descending}, + {Name: "created_at", Dir: Descending}, }, } n, err = FindUsersInAudience(ts.db, u.InstanceID, u.Aud, nil, sp, "") From 7d9635df69a1169d05b2f8de3da46b71afbd4583 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 29 Jun 2022 09:10:51 +0800 Subject: [PATCH 090/102] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e5471f506d..deb745a547 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,7 @@ Adds a prefix to all table names. **Migrations Note** -Migrations are not applied automatically, so you will need to run them after -you've built gotrue. +Migrations are applied automatically when you run `./gotrue`. However, you also have the option to rerun the migrations via the following methods: - If built locally: `./gotrue migrate` - Using Docker: `docker run --rm gotrue gotrue migrate` From 32a6e1fd23d08d2a8f4078684d5dca0205d6b240 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 28 Jun 2022 22:34:29 -0700 Subject: [PATCH 091/102] feat: add captcha to verify and token endpoints (#520) * fix: add captcha to verify and token endpoints * don't enable captcha on refresh token grant_type * refactor: rename hcaptcha_token to captcha_token for generalizability --- api/api.go | 4 ++-- api/middleware_test.go | 6 +++--- security/hcaptcha.go | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/api.go b/api/api.go index cb98b37fc4..116707de66 100644 --- a/api/api.go +++ b/api/api.go @@ -127,7 +127,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati tollbooth.NewLimiter(api.config.RateLimitTokenRefresh/(60*5), &limiter.ExpirableOptions{ DefaultExpirationTTL: time.Hour, }).SetBurst(30), - )).Post("/token", api.Token) + )).With(api.verifyCaptcha).Post("/token", api.Token) r.With(api.limitHandler( // Allow requests at the specified rate per 5 minutes. @@ -136,7 +136,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati }).SetBurst(30), )).Route("/verify", func(r *router) { r.Get("/", api.Verify) - r.Post("/", api.Verify) + r.With(api.verifyCaptcha).Post("/", api.Verify) }) r.With(api.requireAuthentication).Post("/logout", api.Logout) diff --git a/api/middleware_test.go b/api/middleware_test.go index 1c99037c3d..dde8a4cdc2 100644 --- a/api/middleware_test.go +++ b/api/middleware_test.go @@ -48,7 +48,7 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaValid() { "email": "test@example.com", "password": "secret", "gotrue_meta_security": map[string]interface{}{ - "hcaptcha_token": HCaptchaResponse, + "captcha_token": HCaptchaResponse, }, })) @@ -75,7 +75,7 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaValid() { "email": "test@example.com", "password": "secret", "gotrue_meta_security": map[string]interface{}{ - "hcaptcha_token": HCaptchaResponse, + "captcha_token": HCaptchaResponse, }, })) @@ -129,7 +129,7 @@ func (ts *MiddlewareTestSuite) TestVerifyCaptchaInvalid() { "email": "test@example.com", "password": "secret", "gotrue_meta_security": map[string]interface{}{ - "hcaptcha_token": HCaptchaResponse, + "captcha_token": HCaptchaResponse, }, })) req := httptest.NewRequest(http.MethodPost, "http://localhost", &buffer) diff --git a/security/hcaptcha.go b/security/hcaptcha.go index f34c7d61de..64da884d71 100644 --- a/security/hcaptcha.go +++ b/security/hcaptcha.go @@ -22,7 +22,7 @@ type GotrueRequest struct { } type GotrueSecurity struct { - Token string `json:"hcaptcha_token"` + Token string `json:"captcha_token"` } type VerificationResponse struct { @@ -56,6 +56,10 @@ func init() { } func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error) { + if r.FormValue("grant_type") == "refresh_token" { + // captcha shouldn't be enabled on requests to refresh the token + return SuccessfullyVerified, nil + } res := GotrueRequest{} bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { From 0177301ddaef479e57a442e53baca96c7e6ab2f2 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Thu, 30 Jun 2022 08:01:44 +0800 Subject: [PATCH 092/102] fix: go fmt -> gofmt (#522) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6033ff3b74..4e212bff36 100644 --- a/Makefile +++ b/Makefile @@ -55,4 +55,4 @@ docker-clean: ## Remove the development containers and volumes docker-compose -f $(DEV_DOCKER_COMPOSE) rm -fsv format: - go fmt -s -w . + gofmt -s -w . From 28aafd0f643a91894eedaf0b21f91e36194a999e Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Fri, 1 Jul 2022 18:20:03 +0200 Subject: [PATCH 093/102] chore: Add CODEOWNERS (#525) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..cb9b3cac93 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @supabase/auth From 9bc1b663a7261e769e589d62f87c8e474c260388 Mon Sep 17 00:00:00 2001 From: Hadi Mahdavi Date: Tue, 5 Jul 2022 05:56:40 +0430 Subject: [PATCH 094/102] Edit keycloak url Keycloak URL is not included /auth/ after hostname. Readme file and description has a mistake. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index deb745a547..edd1c314c8 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ The URI a OAuth2 provider will redirect to with the `code` and `state` values. `EXTERNAL_X_URL` - `string` -The base URL used for constructing the URLs to request authorization and access tokens. Used by `gitlab` and `keycloak`. For `gitlab` it defaults to `https://gitlab.com`. For `keycloak` you need to set this to your instance, for example: `https://keycloak.example.com/auth/realms/myrealm` +The base URL used for constructing the URLs to request authorization and access tokens. Used by `gitlab` and `keycloak`. For `gitlab` it defaults to `https://gitlab.com`. For `keycloak` you need to set this to your instance, for example: `https://keycloak.example.com/realms/myrealm` #### Apple OAuth From 397c949443365e4ebf5ddd8bd98a64eecd4fa9b4 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 5 Jul 2022 02:23:25 -0700 Subject: [PATCH 095/102] fix: shorten email otp length (#513) * fix: add migrations to hash email * add email otp length to config * remove email hash migration * send email hash & otp in email link * verify post should check for token hash * fix verify tests * fix tests * update generate_link endpoint * remove magic number * use sum224 instead of md5 --- api/external.go | 2 +- api/invite.go | 3 +- api/invite_test.go | 5 +- api/magic_link.go | 2 +- api/mail.go | 92 +++++++++++++++------------------ api/phone.go | 7 +-- api/reauthenticate.go | 10 ++-- api/recover.go | 2 +- api/signup.go | 2 +- api/signup_test.go | 23 +++++---- api/token.go | 2 +- api/user.go | 2 +- api/user_test.go | 7 ++- api/verify.go | 115 +++++++++++++++++++++++++++++++----------- api/verify_test.go | 107 ++++++++++++++++++--------------------- conf/configuration.go | 6 +++ crypto/crypto.go | 1 + mailer/mailer.go | 12 ++--- mailer/template.go | 64 +++++++++++++---------- 19 files changed, 269 insertions(+), 195 deletions(-) diff --git a/api/external.go b/api/external.go index 947af3da56..32e3c69bb0 100644 --- a/api/external.go +++ b/api/external.go @@ -235,7 +235,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re if !emailData.Verified && !config.Mailer.Autoconfirm { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer); terr != nil { + if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, config.Mailer.OtpLength); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { return tooManyRequestsError("For security purposes, you can only request this once every minute") } diff --git a/api/invite.go b/api/invite.go index 7ee6fcbe17..53da7f2040 100644 --- a/api/invite.go +++ b/api/invite.go @@ -17,6 +17,7 @@ type InviteParams struct { // Invite is the endpoint for inviting a new user func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() + config := a.getConfig(ctx) instanceID := getInstanceID(ctx) adminUser := getAdminUser(ctx) params := &InviteParams{} @@ -64,7 +65,7 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if err := sendInvite(tx, user, mailer, referrer); err != nil { + if err := sendInvite(tx, user, mailer, referrer, config.Mailer.OtpLength); err != nil { return internalServerError("Error inviting user").WithInternalError(err) } return nil diff --git a/api/invite_test.go b/api/invite_test.go index e4359168c5..92e1db62fd 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "net/http" @@ -126,6 +127,7 @@ func (ts *InviteTestSuite) TestVerifyInvite() { "Verify invite with password", "test@example.com", map[string]interface{}{ + "email": "test@example.com", "type": "invite", "token": "asdf", "password": "testing", @@ -136,6 +138,7 @@ func (ts *InviteTestSuite) TestVerifyInvite() { "Verify invite with no password", "test1@example.com", map[string]interface{}{ + "email": "test1@example.com", "type": "invite", "token": "asdf", }, @@ -150,7 +153,7 @@ func (ts *InviteTestSuite) TestVerifyInvite() { user.InvitedAt = &now user.ConfirmationSentAt = &now user.EncryptedPassword = "" - user.ConfirmationToken = c.requestBody["token"].(string) + user.ConfirmationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(c.email+c.requestBody["token"].(string)))) require.NoError(ts.T(), err) require.NoError(ts.T(), ts.API.db.Create(user)) diff --git a/api/magic_link.go b/api/magic_link.go index a4c427371f..ff7e275faa 100644 --- a/api/magic_link.go +++ b/api/magic_link.go @@ -82,7 +82,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - return a.sendMagicLink(tx, user, mailer, config.SMTP.MaxFrequency, referrer) + return a.sendMagicLink(tx, user, mailer, config.SMTP.MaxFrequency, referrer, config.Mailer.OtpLength) }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/api/mail.go b/api/mail.go index 12444aa19c..973183e359 100644 --- a/api/mail.go +++ b/api/mail.go @@ -2,6 +2,7 @@ package api import ( "context" + "crypto/sha256" "encoding/json" "fmt" "net/http" @@ -14,7 +15,6 @@ import ( "github.com/netlify/gotrue/storage" "github.com/pkg/errors" "github.com/sethvargo/go-password/password" - "github.com/sirupsen/logrus" ) var ( @@ -69,6 +69,10 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { var url string referrer := a.getRedirectURLOrReferrer(r, params.RedirectTo) now := time.Now() + otp, err := crypto.GenerateOtp(config.Mailer.OtpLength) + if err != nil { + return err + } err = a.db.Transaction(func(tx *storage.Connection) error { var terr error switch params.Type { @@ -76,7 +80,7 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserRecoveryRequestedAction, "", nil); terr != nil { return terr } - user.RecoveryToken = crypto.SecureToken() + user.RecoveryToken = fmt.Sprintf("%x", sha256.Sum224([]byte(user.GetEmail()+otp))) user.RecoverySentAt = &now terr = errors.Wrap(tx.UpdateOnly(user, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery") case "invite": @@ -102,7 +106,7 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { }); terr != nil { return terr } - user.ConfirmationToken = crypto.SecureToken() + user.ConfirmationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(user.GetEmail()+otp))) user.ConfirmationSentAt = &now user.InvitedAt = &now terr = errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at", "invited_at"), "Database error updating user for invite") @@ -133,7 +137,7 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { return terr } } - user.ConfirmationToken = crypto.SecureToken() + user.ConfirmationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(user.GetEmail()+otp))) user.ConfirmationSentAt = &now terr = errors.Wrap(tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation") default: @@ -168,18 +172,19 @@ func (a *API) GenerateLink(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, resp) } -func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { +func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, otpLength int) error { var err error if u.ConfirmationSentAt != nil && !u.ConfirmationSentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } oldToken := u.ConfirmationToken - u.ConfirmationToken, err = generateUniqueEmailOtp(tx, confirmationToken) + otp, err := crypto.GenerateOtp(otpLength) if err != nil { return err } + u.ConfirmationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otp))) now := time.Now() - if err := mailer.ConfirmationMail(u, referrerURL); err != nil { + if err := mailer.ConfirmationMail(u, otp, referrerURL); err != nil { u.ConfirmationToken = oldToken return errors.Wrap(err, "Error sending confirmation email") } @@ -187,15 +192,16 @@ func sendConfirmation(tx *storage.Connection, u *models.User, mailer mailer.Mail return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "confirmation_sent_at"), "Database error updating user for confirmation") } -func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, referrerURL string) error { +func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, referrerURL string, otpLength int) error { var err error oldToken := u.ConfirmationToken - u.ConfirmationToken, err = generateUniqueEmailOtp(tx, confirmationToken) + otp, err := crypto.GenerateOtp(otpLength) if err != nil { return err } + u.ConfirmationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otp))) now := time.Now() - if err := mailer.InviteMail(u, referrerURL); err != nil { + if err := mailer.InviteMail(u, otp, referrerURL); err != nil { u.ConfirmationToken = oldToken return errors.Wrap(err, "Error sending invite email") } @@ -204,19 +210,20 @@ func sendInvite(tx *storage.Connection, u *models.User, mailer mailer.Mailer, re return errors.Wrap(tx.UpdateOnly(u, "confirmation_token", "confirmation_sent_at", "invited_at"), "Database error updating user for invite") } -func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { +func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, otpLength int) error { var err error if u.RecoverySentAt != nil && !u.RecoverySentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } oldToken := u.RecoveryToken - u.RecoveryToken, err = generateUniqueEmailOtp(tx, recoveryToken) + otp, err := crypto.GenerateOtp(otpLength) if err != nil { return err } + u.RecoveryToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otp))) now := time.Now() - if err := mailer.RecoveryMail(u, referrerURL); err != nil { + if err := mailer.RecoveryMail(u, otp, referrerURL); err != nil { u.RecoveryToken = oldToken return errors.Wrap(err, "Error sending recovery email") } @@ -224,19 +231,23 @@ func (a *API) sendPasswordRecovery(tx *storage.Connection, u *models.User, maile return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery") } -func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration) error { +func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, otpLength int) error { var err error if u.ReauthenticationSentAt != nil && !u.ReauthenticationSentAt.Add(maxFrequency).Before(time.Now()) { return MaxFrequencyLimitError } oldToken := u.ReauthenticationToken - u.ReauthenticationToken, err = generateUniqueEmailOtp(tx, reauthenticationToken) + otp, err := crypto.GenerateOtp(otpLength) + if err != nil { + return err + } + u.ReauthenticationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otp))) if err != nil { return err } now := time.Now() - if err := mailer.ReauthenticateMail(u); err != nil { + if err := mailer.ReauthenticateMail(u, otp); err != nil { u.ReauthenticationToken = oldToken return errors.Wrap(err, "Error sending reauthentication email") } @@ -244,7 +255,7 @@ func (a *API) sendReauthenticationOtp(tx *storage.Connection, u *models.User, ma return errors.Wrap(tx.UpdateOnly(u, "reauthentication_token", "reauthentication_sent_at"), "Database error updating user for reauthentication") } -func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string) error { +func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer mailer.Mailer, maxFrequency time.Duration, referrerURL string, otpLength int) error { var err error // since Magic Link is just a recovery with a different template and behaviour // around new users we will reuse the recovery db timer to prevent potential abuse @@ -252,12 +263,13 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile return MaxFrequencyLimitError } oldToken := u.RecoveryToken - u.RecoveryToken, err = generateUniqueEmailOtp(tx, recoveryToken) + otp, err := crypto.GenerateOtp(otpLength) if err != nil { return err } + u.RecoveryToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otp))) now := time.Now() - if err := mailer.MagicLinkMail(u, referrerURL); err != nil { + if err := mailer.MagicLinkMail(u, otp, referrerURL); err != nil { u.RecoveryToken = oldToken return errors.Wrap(err, "Error sending magic link email") } @@ -266,14 +278,21 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile } // sendEmailChange sends out an email change token to the new email. -func (a *API) sendEmailChange(tx *storage.Connection, config *conf.Configuration, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error { +func (a *API) sendEmailChange(tx *storage.Connection, config *conf.Configuration, u *models.User, mailer mailer.Mailer, email string, referrerURL string, otpLength int) error { var err error - u.EmailChangeTokenNew, err = generateUniqueEmailOtp(tx, emailChangeTokenNew) + otpNew, err := crypto.GenerateOtp(otpLength) if err != nil { return err } + u.EmailChangeTokenNew = fmt.Sprintf("%x", sha256.Sum224([]byte(u.EmailChange+otpNew))) + + otpCurrent := "" if config.Mailer.SecureEmailChangeEnabled && u.GetEmail() != "" { - u.EmailChangeTokenCurrent, err = generateUniqueEmailOtp(tx, emailChangeTokenCurrent) + otpCurrent, err = crypto.GenerateOtp(otpLength) + if err != nil { + return err + } + u.EmailChangeTokenCurrent = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+otpCurrent))) if err != nil { return err } @@ -281,7 +300,7 @@ func (a *API) sendEmailChange(tx *storage.Connection, config *conf.Configuration u.EmailChange = email u.EmailChangeConfirmStatus = zeroConfirmation now := time.Now() - if err := mailer.EmailChangeMail(u, referrerURL); err != nil { + if err := mailer.EmailChangeMail(u, otpNew, otpCurrent, referrerURL); err != nil { return err } @@ -306,30 +325,3 @@ func (a *API) validateEmail(ctx context.Context, email string) error { } return nil } - -// generateUniqueEmailOtp returns a unique otp -func generateUniqueEmailOtp(tx *storage.Connection, tokenType tokenType) (string, error) { - maxRetries := 5 - otpLength := 20 - var otp string - var err error - for i := 0; i < maxRetries; i++ { - otp, err = crypto.GenerateEmailOtp(otpLength) - if err != nil { - return "", err - } - _, err = models.FindUserByTokenAndTokenType(tx, otp, string(tokenType)) - if err != nil { - if models.IsNotFoundError(err) { - return otp, nil - } - return "", err - } - logrus.Warn("otp generated is not unique, retrying.") - err = errors.New("Could not generate a unique email otp") - } - if err != nil { - return "", err - } - return "", errors.New("Could not generate a unique email otp") -} diff --git a/api/phone.go b/api/phone.go index 814244b99b..e72e10bf47 100644 --- a/api/phone.go +++ b/api/phone.go @@ -2,6 +2,7 @@ package api import ( "context" + "crypto/sha256" "fmt" "regexp" "strings" @@ -77,13 +78,13 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, tx *storage.Connection, if err != nil { return internalServerError("error generating otp").WithInternalError(err) } - *token = otp + *token = fmt.Sprintf("%x", sha256.Sum224([]byte(phone+otp))) var message string if config.Sms.Template == "" { - message = fmt.Sprintf(defaultSmsMessage, *token) + message = fmt.Sprintf(defaultSmsMessage, otp) } else { - message = strings.Replace(config.Sms.Template, "{{ .Code }}", *token, -1) + message = strings.Replace(config.Sms.Template, "{{ .Code }}", otp, -1) } if serr := smsProvider.SendSms(phone, message); serr != nil { diff --git a/api/reauthenticate.go b/api/reauthenticate.go index 530effa326..6debc67ea7 100644 --- a/api/reauthenticate.go +++ b/api/reauthenticate.go @@ -1,7 +1,9 @@ package api import ( + "crypto/sha256" "errors" + "fmt" "net/http" "github.com/gofrs/uuid" @@ -54,7 +56,7 @@ func (a *API) Reauthenticate(w http.ResponseWriter, r *http.Request) error { } if email != "" { mailer := a.Mailer(ctx) - return a.sendReauthenticationOtp(tx, user, mailer, config.SMTP.MaxFrequency) + return a.sendReauthenticationOtp(tx, user, mailer, config.SMTP.MaxFrequency, config.Mailer.OtpLength) } else if phone != "" { smsProvider, terr := sms_provider.GetSmsProvider(*config) if terr != nil { @@ -81,9 +83,11 @@ func (a *API) verifyReauthentication(nonce string, tx *storage.Connection, confi } var isValid bool if user.GetEmail() != "" { - isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Mailer.OtpExp) + tokenHash := fmt.Sprintf("%x", sha256.Sum224([]byte(user.GetEmail()+nonce))) + isValid = isOtpValid(tokenHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Mailer.OtpExp) } else if user.GetPhone() != "" { - isValid = isOtpValid(nonce, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Sms.OtpExp) + tokenHash := fmt.Sprintf("%x", sha256.Sum224([]byte(user.GetPhone()+nonce))) + isValid = isOtpValid(tokenHash, user.ReauthenticationToken, user.ReauthenticationSentAt, config.Sms.OtpExp) } else { return unprocessableEntityError("Reauthentication requires an email or a phone number") } diff --git a/api/recover.go b/api/recover.go index 7c75dcd0af..17cc36763e 100644 --- a/api/recover.go +++ b/api/recover.go @@ -51,7 +51,7 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { } mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer) + return a.sendPasswordRecovery(tx, user, mailer, config.SMTP.MaxFrequency, referrer, config.Mailer.OtpLength) }) if err != nil { if errors.Is(err, MaxFrequencyLimitError) { diff --git a/api/signup.go b/api/signup.go index 43c0d22c8e..2cbef93eaa 100644 --- a/api/signup.go +++ b/api/signup.go @@ -132,7 +132,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { }); terr != nil { return terr } - if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer); terr != nil { + if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, config.Mailer.OtpLength); terr != nil { if errors.Is(terr, MaxFrequencyLimitError) { now := time.Now() left := user.ConfirmationSentAt.Add(config.SMTP.MaxFrequency).Sub(now) / time.Second diff --git a/api/signup_test.go b/api/signup_test.go index aa5137056b..140d52ab49 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -3,9 +3,11 @@ package api import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" + "net/url" "testing" "time" @@ -244,21 +246,20 @@ func (ts *SignupTestSuite) TestVerifySignup() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - // Request body - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "type": "signup", - "token": u.ConfirmationToken, - })) - // Setup request - req := httptest.NewRequest(http.MethodPost, "http://localhost/verify", &buffer) - req.Header.Set("Content-Type", "application/json") + reqUrl := fmt.Sprintf("http://localhost/verify?type=%s&token=%s", signupVerification, u.ConfirmationToken) + req := httptest.NewRequest(http.MethodGet, reqUrl, nil) // Setup response recorder w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusSeeOther, w.Code) - assert.Equal(ts.T(), http.StatusOK, w.Code, w.Body.String()) + urlVal, err := url.Parse(w.Result().Header.Get("Location")) + require.NoError(ts.T(), err) + v, err := url.ParseQuery(urlVal.Fragment) + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), v.Get("access_token")) + require.NotEmpty(ts.T(), v.Get("expires_in")) + require.NotEmpty(ts.T(), v.Get("refresh_token")) } diff --git a/api/token.go b/api/token.go index b1bb6c2d8d..581b383478 100644 --- a/api/token.go +++ b/api/token.go @@ -481,7 +481,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R if (!ok || !isEmailVerified) && !config.Mailer.Autoconfirm { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer); terr != nil { + if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, config.Mailer.OtpLength); terr != nil { return internalServerError("Error sending confirmation mail").WithInternalError(terr) } return unauthorizedError("Error unverified email") diff --git a/api/user.go b/api/user.go index d5e9d757bd..7a89eea66b 100644 --- a/api/user.go +++ b/api/user.go @@ -132,7 +132,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { mailer := a.Mailer(ctx) referrer := a.getReferrer(r) - if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer); terr != nil { + if terr = a.sendEmailChange(tx, config, user, mailer, params.Email, referrer, config.Mailer.OtpLength); terr != nil { return internalServerError("Error sending change email").WithInternalError(terr) } } diff --git a/api/user_test.go b/api/user_test.go index a1c0cc1db7..eae0b2c870 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "net/http" @@ -291,11 +292,15 @@ func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() { require.NotEmpty(ts.T(), u.ReauthenticationToken) require.NotEmpty(ts.T(), u.ReauthenticationSentAt) + // update reauthentication token to a known token + u.ReauthenticationToken = fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+"123456"))) + require.NoError(ts.T(), ts.API.db.Update(u)) + // update password with reauthentication token var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ "password": "newpass", - "nonce": u.ReauthenticationToken, + "nonce": "123456", })) req = httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) diff --git a/api/verify.go b/api/verify.go index 22676d7fc2..f84b765eed 100644 --- a/api/verify.go +++ b/api/verify.go @@ -2,8 +2,10 @@ package api import ( "context" + "crypto/sha256" "encoding/json" "errors" + "fmt" "net/http" "net/url" "strconv" @@ -37,7 +39,8 @@ const ( const ( // v1 uses crypto.SecureToken() - v1OtpLength = 22 + v1OtpLength = 22 + sum224HashLength = 28 ) // Only applicable when SECURE_EMAIL_CHANGE_ENABLED @@ -91,7 +94,7 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { return badRequestError("Verify requires a verification type") } aud := a.requestAud(ctx, r) - user, terr = a.verifyUserAndToken(ctx, tx, params, aud) + user, terr = a.verifyEmailLink(ctx, tx, params, aud) if terr != nil { return terr } @@ -401,6 +404,50 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p return user, nil } +func (a *API) verifyEmailLink(ctx context.Context, conn *storage.Connection, params *VerifyParams, aud string) (*models.User, error) { + config := getConfig(ctx) + + var user *models.User + var err error + + switch params.Type { + case signupVerification, inviteVerification: + user, err = models.FindUserByConfirmationToken(conn, params.Token) + case recoveryVerification, magicLinkVerification: + user, err = models.FindUserByRecoveryToken(conn, params.Token) + case emailChangeVerification: + user, err = models.FindUserByEmailChangeToken(conn, params.Token) + default: + return nil, badRequestError("Invalid email verification type") + } + + if err != nil { + if models.IsNotFoundError(err) { + return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(redirectWithQueryError) + } + return nil, internalServerError("Database error finding user from email link").WithInternalError(err) + } + + if user.IsBanned() { + return nil, unauthorizedError("Error confirming user").WithInternalError(redirectWithQueryError) + } + + var isExpired bool + switch params.Type { + case signupVerification, inviteVerification: + isExpired = isOtpExpired(user.ConfirmationSentAt, config.Mailer.OtpExp) + case recoveryVerification, magicLinkVerification: + isExpired = isOtpExpired(user.RecoverySentAt, config.Mailer.OtpExp) + case emailChangeVerification: + isExpired = isOtpExpired(user.EmailChangeSentAt, config.Mailer.OtpExp) + } + + if isExpired { + return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(redirectWithQueryError) + } + return user, nil +} + // verifyUserAndToken verifies the token associated to the user based on the verify type func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, params *VerifyParams, aud string) (*models.User, error) { instanceID := getInstanceID(ctx) @@ -408,25 +455,13 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, var user *models.User var err error - if isUrlLinkVerification(params) { - switch params.Type { - case signupVerification, inviteVerification: - user, err = models.FindUserByConfirmationToken(conn, params.Token) - case recoveryVerification, magicLinkVerification: - user, err = models.FindUserByRecoveryToken(conn, params.Token) - case emailChangeVerification: - user, err = models.FindUserByEmailChangeToken(conn, params.Token) - default: - return nil, badRequestError("Invalid email verification type") - } - } else if isPhoneOtpVerification(params) { - if params.Phone == "" { - return nil, unprocessableEntityError("Sms Verification requires a phone number") - } + var tokenHash string + if isPhoneOtpVerification(params) { params.Phone, err = a.validatePhone(params.Phone) if err != nil { return nil, err } + tokenHash = fmt.Sprintf("%x", sha256.Sum224([]byte(string(params.Phone)+params.Token))) switch params.Type { case phoneChangeVerification: user, err = models.FindUserByPhoneChangeAndAudience(conn, instanceID, params.Phone, aud) @@ -439,14 +474,15 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, if err := a.validateEmail(ctx, params.Email); err != nil { return nil, unprocessableEntityError("Invalid email format").WithInternalError(err) } + tokenHash = fmt.Sprintf("%x", sha256.Sum224([]byte(string(params.Email)+params.Token))) switch params.Type { case emailChangeVerification: - user, err = models.FindUserForEmailChange(conn, instanceID, params.Email, params.Token, aud, config.Mailer.SecureEmailChangeEnabled) + user, err = models.FindUserForEmailChange(conn, instanceID, params.Email, tokenHash, aud, config.Mailer.SecureEmailChangeEnabled) default: user, err = models.FindUserByEmailAndAudience(conn, instanceID, params.Email, aud) } } else { - return nil, badRequestError("Only an email address or phone number should be provided on verify, not both.") + return nil, badRequestError("Only an email address or phone number should be provided on verify") } if err != nil { @@ -463,15 +499,38 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection, var isValid bool switch params.Type { case signupVerification, inviteVerification: - isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt, config.Mailer.OtpExp) + // TODO(km): remove when old token format is deprecated + // the new token format is represented by a MD5 hash which is 32 characters (128 bits) long + // anything shorter than 32 characters can safely be assumed to be using the old token format + if len(user.ConfirmationToken) < sum224HashLength { + tokenHash = params.Token + } + isValid = isOtpValid(tokenHash, user.ConfirmationToken, user.ConfirmationSentAt, config.Mailer.OtpExp) case recoveryVerification, magicLinkVerification: - isValid = isOtpValid(params.Token, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp) + // TODO(km): remove when old token format is deprecated + if len(user.RecoveryToken) < sum224HashLength { + tokenHash = params.Token + } + isValid = isOtpValid(tokenHash, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp) case emailChangeVerification: - isValid = isOtpValid(params.Token, user.EmailChangeTokenCurrent, user.EmailChangeSentAt, config.Mailer.OtpExp) || isOtpValid(params.Token, user.EmailChangeTokenNew, user.EmailChangeSentAt, config.Mailer.OtpExp) + // TODO(km): remove when old token format is deprecated + if len(user.EmailChangeTokenCurrent) < sum224HashLength && len(user.EmailChangeTokenNew) < sum224HashLength { + tokenHash = params.Token + } + isValid = isOtpValid(tokenHash, user.EmailChangeTokenCurrent, user.EmailChangeSentAt, config.Mailer.OtpExp) || + isOtpValid(tokenHash, user.EmailChangeTokenNew, user.EmailChangeSentAt, config.Mailer.OtpExp) case phoneChangeVerification: - isValid = isOtpValid(params.Token, user.PhoneChangeToken, user.PhoneChangeSentAt, config.Sms.OtpExp) + // TODO(km): remove when old token format is deprecated + if len(user.PhoneChangeToken) < sum224HashLength { + tokenHash = params.Token + } + isValid = isOtpValid(tokenHash, user.PhoneChangeToken, user.PhoneChangeSentAt, config.Sms.OtpExp) case smsVerification: - isValid = isOtpValid(params.Token, user.ConfirmationToken, user.ConfirmationSentAt, config.Sms.OtpExp) + // TODO(km): remove when old token format is deprecated + if len(user.ConfirmationToken) < sum224HashLength { + tokenHash = params.Token + } + isValid = isOtpValid(tokenHash, user.ConfirmationToken, user.ConfirmationSentAt, config.Sms.OtpExp) } if !isValid || err != nil { @@ -485,13 +544,11 @@ func isOtpValid(actual, expected string, sentAt *time.Time, otpExp uint) bool { if expected == "" || sentAt == nil { return false } - expiresAt := sentAt.Add(time.Second * time.Duration(otpExp)) - return time.Now().Before(expiresAt) && (actual == expected) + return !isOtpExpired(sentAt, otpExp) && (actual == expected) } -// isUrlLinkVerification checks if the verification came from clicking an email link which wouldn't contain the email field in the params -func isUrlLinkVerification(params *VerifyParams) bool { - return params.Phone == "" && params.Email == "" +func isOtpExpired(sentAt *time.Time, otpExp uint) bool { + return time.Now().After(sentAt.Add(time.Second * time.Duration(otpExp))) } // isPhoneOtpVerification checks if the verification came from a phone otp diff --git a/api/verify_test.go b/api/verify_test.go index 37350ba853..8c65161afc 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "io/ioutil" @@ -77,19 +78,12 @@ func (ts *VerifyTestSuite) TestVerifyPasswordRecovery() { assert.WithinDuration(ts.T(), time.Now(), *u.RecoverySentAt, 1*time.Second) assert.False(ts.T(), u.IsConfirmed()) - // Send Verify request - var vbuffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&vbuffer).Encode(map[string]interface{}{ - "type": "recovery", - "token": u.RecoveryToken, - })) - - req = httptest.NewRequest(http.MethodPost, "http://localhost/verify", &vbuffer) - req.Header.Set("Content-Type", "application/json") + reqURL := fmt.Sprintf("http://localhost/verify?type=%s&token=%s", recoveryVerification, u.RecoveryToken) + req = httptest.NewRequest(http.MethodGet, reqURL, nil) w = httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - assert.Equal(ts.T(), http.StatusOK, w.Code) + assert.Equal(ts.T(), http.StatusSeeOther, w.Code) u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) @@ -129,45 +123,38 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { assert.False(ts.T(), u.IsConfirmed()) // Verify new email - var vbuffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&vbuffer).Encode(map[string]interface{}{ - "type": "email_change", - "token": u.EmailChangeTokenNew, - })) - - req = httptest.NewRequest(http.MethodPost, "http://localhost/verify", &vbuffer) - req.Header.Set("Content-Type", "application/json") + reqURL := fmt.Sprintf("http://localhost/verify?type=%s&token=%s", emailChangeVerification, u.EmailChangeTokenNew) + req = httptest.NewRequest(http.MethodGet, reqURL, nil) w = httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - data := make(map[string]interface{}) - require.Equal(ts.T(), http.StatusOK, w.Code) - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.Equal(ts.T(), singleConfirmationAccepted, data["msg"]) + require.Equal(ts.T(), http.StatusSeeOther, w.Code) + urlVal, err := url.Parse(w.Result().Header.Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + v, err := url.ParseQuery(urlVal.Fragment) + ts.Require().NoError(err) + ts.Require().NotEmpty(v.Get("message")) u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) assert.Equal(ts.T(), singleConfirmation, u.EmailChangeConfirmStatus) // Verify old email - require.NoError(ts.T(), json.NewEncoder(&vbuffer).Encode(map[string]interface{}{ - "type": "email_change", - "token": u.EmailChangeTokenCurrent, - })) - - req = httptest.NewRequest(http.MethodPost, "http://localhost/verify", &vbuffer) - req.Header.Set("Content-Type", "application/json") + reqURL = fmt.Sprintf("http://localhost/verify?type=%s&token=%s", emailChangeVerification, u.EmailChangeTokenCurrent) + req = httptest.NewRequest(http.MethodGet, reqURL, nil) w = httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) - data = make(map[string]interface{}) - require.Equal(ts.T(), http.StatusOK, w.Code) - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.NotNil(ts.T(), data["access_token"]) - require.NotNil(ts.T(), data["expires_in"]) - require.NotNil(ts.T(), data["refresh_token"]) - require.NotNil(ts.T(), data["user"]) + require.Equal(ts.T(), http.StatusSeeOther, w.Code) + + urlVal, err = url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + v, err = url.ParseQuery(urlVal.Fragment) + ts.Require().NoError(err) + ts.Require().NotEmpty(v.Get("access_token")) + ts.Require().NotEmpty(v.Get("expires_in")) + ts.Require().NotEmpty(v.Get("refresh_token")) // user's email should've been updated to new@example.com u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "new@example.com", ts.Config.JWT.Aud) @@ -200,7 +187,7 @@ func (ts *VerifyTestSuite) TestExpiredConfirmationToken() { require.NoError(ts.T(), err) fmt.Println(f) assert.Equal(ts.T(), "401", f.Get("error_code")) - assert.Equal(ts.T(), "Token has expired or is invalid", f.Get("error_description")) + assert.Equal(ts.T(), "Email link is invalid or has expired", f.Get("error_description")) assert.Equal(ts.T(), "unauthorized_client", f.Get("error")) } @@ -619,7 +606,8 @@ func (ts *VerifyTestSuite) TestVerifyBannedUser() { func (ts *VerifyTestSuite) TestVerifyValidOtp() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) - + u.EmailChange = "new@example.com" + u.Phone = "12345678" u.PhoneChange = "1234567890" require.NoError(ts.T(), ts.API.db.Update(u)) @@ -641,9 +629,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { desc: "Valid SMS OTP", sentTime: time.Now(), body: map[string]interface{}{ - "type": smsVerification, - "token": "123456", - "phone": "12345678", + "type": smsVerification, + "tokenHash": fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetPhone()+"123456"))), + "token": "123456", + "phone": u.GetPhone(), }, expected: expectedResponse, }, @@ -651,9 +640,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { desc: "Valid Confirmation OTP", sentTime: time.Now(), body: map[string]interface{}{ - "type": signupVerification, - "token": "123456", - "email": u.GetEmail(), + "type": signupVerification, + "tokenHash": fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+"123456"))), + "token": "123456", + "email": u.GetEmail(), }, expected: expectedResponse, }, @@ -661,9 +651,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { desc: "Valid Recovery OTP", sentTime: time.Now(), body: map[string]interface{}{ - "type": recoveryVerification, - "token": "123456", - "email": u.GetEmail(), + "type": recoveryVerification, + "tokenHash": fmt.Sprintf("%x", sha256.Sum224([]byte(u.GetEmail()+"123456"))), + "token": "123456", + "email": u.GetEmail(), }, expected: expectedResponse, }, @@ -671,9 +662,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { desc: "Valid Email Change OTP", sentTime: time.Now(), body: map[string]interface{}{ - "type": emailChangeVerification, - "token": "123456", - "email": u.GetEmail(), + "type": emailChangeVerification, + "tokenHash": fmt.Sprintf("%x", sha256.Sum224([]byte(u.EmailChange+"123456"))), + "token": "123456", + "email": u.EmailChange, }, expected: expectedResponse, }, @@ -681,9 +673,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { desc: "Valid Phone Change OTP", sentTime: time.Now(), body: map[string]interface{}{ - "type": phoneChangeVerification, - "token": "123456", - "phone": u.PhoneChange, + "type": phoneChangeVerification, + "tokenHash": fmt.Sprintf("%x", sha256.Sum224([]byte(u.PhoneChange+"123456"))), + "token": "123456", + "phone": u.PhoneChange, }, expected: expectedResponse, }, @@ -696,10 +689,10 @@ func (ts *VerifyTestSuite) TestVerifyValidOtp() { u.RecoverySentAt = &c.sentTime u.EmailChangeSentAt = &c.sentTime u.PhoneChangeSentAt = &c.sentTime - u.ConfirmationToken = c.body["token"].(string) - u.RecoveryToken = c.body["token"].(string) - u.EmailChangeTokenCurrent = c.body["token"].(string) - u.PhoneChangeToken = c.body["token"].(string) + u.ConfirmationToken = c.body["tokenHash"].(string) + u.RecoveryToken = c.body["tokenHash"].(string) + u.EmailChangeTokenNew = c.body["tokenHash"].(string) + u.PhoneChangeToken = c.body["tokenHash"].(string) require.NoError(ts.T(), ts.API.db.Update(u)) var buffer bytes.Buffer diff --git a/conf/configuration.go b/conf/configuration.go index a9e28ee4da..d0dc4defea 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -131,6 +131,7 @@ type MailerConfiguration struct { URLPaths EmailContentConfiguration `json:"url_paths"` SecureEmailChangeEnabled bool `json:"secure_email_change_enabled" split_words:"true" default:"true"` OtpExp uint `json:"otp_exp" split_words:"true"` + OtpLength int `json:"otp_length" split_words:"true"` } type PhoneProviderConfiguration struct { @@ -308,6 +309,11 @@ func (config *Configuration) ApplyDefaults() { config.Mailer.OtpExp = 86400 // 1 day } + if config.Mailer.OtpLength == 0 || config.Mailer.OtpLength < 6 || config.Mailer.OtpLength > 10 { + // 6-digit otp by default + config.Mailer.OtpLength = 6 + } + if config.SMTP.MaxFrequency == 0 { config.SMTP.MaxFrequency = 1 * time.Minute } diff --git a/crypto/crypto.go b/crypto/crypto.go index 22df538a19..a6af6d8c59 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -28,6 +28,7 @@ func GenerateOtp(digits int) (string, error) { if err != nil { return "", errors.WithMessage(err, "Error generating otp") } + // adds a variable zero-padding to the left to ensure otp is uniformly random expr := "%0" + strconv.Itoa(digits) + "v" otp := fmt.Sprintf(expr, val.String()) return otp, nil diff --git a/mailer/mailer.go b/mailer/mailer.go index 3441eb5a47..9db10bb3e6 100644 --- a/mailer/mailer.go +++ b/mailer/mailer.go @@ -14,12 +14,12 @@ import ( // Mailer defines the interface a mailer must implement. type Mailer interface { Send(user *models.User, subject, body string, data map[string]interface{}) error - InviteMail(user *models.User, referrerURL string) error - ConfirmationMail(user *models.User, referrerURL string) error - RecoveryMail(user *models.User, referrerURL string) error - MagicLinkMail(user *models.User, referrerURL string) error - EmailChangeMail(user *models.User, referrerURL string) error - ReauthenticateMail(user *models.User) error + InviteMail(user *models.User, otp, referrerURL string) error + ConfirmationMail(user *models.User, otp, referrerURL string) error + RecoveryMail(user *models.User, otp, referrerURL string) error + MagicLinkMail(user *models.User, otp, referrerURL string) error + EmailChangeMail(user *models.User, otpNew, otpCurrent, referrerURL string) error + ReauthenticateMail(user *models.User, otp string) error ValidateEmail(email string) error GetEmailActionLink(user *models.User, actionType, referrerURL string) (string, error) } diff --git a/mailer/template.go b/mailer/template.go index f4e82a867c..e336d9b3e2 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -64,7 +64,7 @@ func (m TemplateMailer) ValidateEmail(email string) error { } // InviteMail sends a invite mail to a new user -func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error { +func (m *TemplateMailer) InviteMail(user *models.User, otp, referrerURL string) error { globalConfig, err := conf.LoadGlobal(configFile) redirectParam := "" @@ -80,7 +80,7 @@ func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": formatEmailOtp(user.ConfirmationToken), + "Token": otp, "Data": user.UserMetaData, } @@ -94,14 +94,15 @@ func (m *TemplateMailer) InviteMail(user *models.User, referrerURL string) error } // ConfirmationMail sends a signup confirmation mail to a new user -func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) error { +func (m *TemplateMailer) ConfirmationMail(user *models.User, otp, referrerURL string) error { globalConfig, err := conf.LoadGlobal(configFile) - + if err != nil { + return err + } redirectParam := "" if len(referrerURL) > 0 { redirectParam = "&redirect_to=" + referrerURL } - url, err := getSiteURL(referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.Confirmation, "token="+user.ConfirmationToken+"&type=signup"+redirectParam) if err != nil { return err @@ -110,7 +111,7 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": formatEmailOtp(user.ConfirmationToken), + "Token": otp, "Data": user.UserMetaData, } @@ -124,11 +125,11 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string) } // ReauthenticateMail sends a reauthentication mail to an authenticated user -func (m *TemplateMailer) ReauthenticateMail(user *models.User) error { +func (m *TemplateMailer) ReauthenticateMail(user *models.User, otp string) error { data := map[string]interface{}{ "SiteURL": m.Config.SiteURL, "Email": user.Email, - "Token": formatEmailOtp(user.ReauthenticationToken), + "Token": otp, "Data": user.UserMetaData, } @@ -142,29 +143,32 @@ func (m *TemplateMailer) ReauthenticateMail(user *models.User) error { } // EmailChangeMail sends an email change confirmation mail to a user -func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) error { +func (m *TemplateMailer) EmailChangeMail(user *models.User, otpNew, otpCurrent, referrerURL string) error { type Email struct { - Address string - Token string - Subject string - Template string + Address string + Otp string + TokenHash string + Subject string + Template string } emails := []Email{ { - Address: user.EmailChange, - Token: formatEmailOtp(user.EmailChangeTokenNew), - Subject: string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")), - Template: m.Config.Mailer.Templates.EmailChange, + Address: user.EmailChange, + Otp: otpNew, + TokenHash: user.EmailChangeTokenNew, + Subject: string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")), + Template: m.Config.Mailer.Templates.EmailChange, }, } currentEmail := user.GetEmail() if m.Config.Mailer.SecureEmailChangeEnabled && currentEmail != "" { emails = append(emails, Email{ - Address: currentEmail, - Token: formatEmailOtp(user.EmailChangeTokenCurrent), - Subject: string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")), - Template: m.Config.Mailer.Templates.EmailChange, + Address: currentEmail, + Otp: otpCurrent, + TokenHash: user.EmailChangeTokenCurrent, + Subject: string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")), + Template: m.Config.Mailer.Templates.EmailChange, }) } @@ -183,7 +187,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) referrerURL, globalConfig.API.ExternalURL, m.Config.Mailer.URLPaths.EmailChange, - "token="+email.Token+"&type=email_change"+redirectParam, + "token="+email.TokenHash+"&type=email_change"+redirectParam, ) if err != nil { return err @@ -204,7 +208,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) defaultEmailChangeMail, data, ) - }(email.Address, email.Token, email.Template) + }(email.Address, email.Otp, email.Template) } for i := 0; i < len(emails); i++ { @@ -218,8 +222,11 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) } // RecoveryMail sends a password recovery mail -func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) error { +func (m *TemplateMailer) RecoveryMail(user *models.User, otp, referrerURL string) error { globalConfig, err := conf.LoadGlobal(configFile) + if err != nil { + return err + } redirectParam := "" if len(referrerURL) > 0 { @@ -234,7 +241,7 @@ func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) err "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": formatEmailOtp(user.RecoveryToken), + "Token": otp, "Data": user.UserMetaData, } @@ -248,8 +255,11 @@ func (m *TemplateMailer) RecoveryMail(user *models.User, referrerURL string) err } // MagicLinkMail sends a login link mail -func (m *TemplateMailer) MagicLinkMail(user *models.User, referrerURL string) error { +func (m *TemplateMailer) MagicLinkMail(user *models.User, otp, referrerURL string) error { globalConfig, err := conf.LoadGlobal(configFile) + if err != nil { + return err + } redirectParam := "" if len(referrerURL) > 0 { @@ -264,7 +274,7 @@ func (m *TemplateMailer) MagicLinkMail(user *models.User, referrerURL string) er "SiteURL": m.Config.SiteURL, "ConfirmationURL": url, "Email": user.Email, - "Token": formatEmailOtp(user.RecoveryToken), + "Token": otp, "Data": user.UserMetaData, } From 45756e3e68740a93cf20cdbc869f072a4e624f36 Mon Sep 17 00:00:00 2001 From: Danylo Patsora Date: Fri, 8 Oct 2021 10:39:45 +0300 Subject: [PATCH 096/102] Feature: Asymmetric key authentication & RS256 JWT signature --- README.md | 55 +++- api/admin_test.go | 15 +- api/api.go | 2 + api/asymmetric_login_test.go | 309 ++++++++++++++++++ api/asymmetric_signin.go | 159 +++++++++ api/audit_test.go | 7 +- api/auth.go | 4 +- api/external.go | 8 +- api/external_apple_test.go | 2 +- api/external_azure_test.go | 2 +- api/external_bitbucket_test.go | 2 +- api/external_discord_test.go | 2 +- api/external_facebook_test.go | 2 +- api/external_github_test.go | 2 +- api/external_gitlab_test.go | 2 +- api/external_google_test.go | 2 +- api/external_twitch_test.go | 2 +- api/instance_test.go | 2 +- api/invite_test.go | 7 +- api/middleware.go | 2 +- api/token.go | 28 +- api/token_test.go | 68 ++++ api/user_test.go | 5 +- conf/configuration.go | 48 +++ go.mod | 9 +- go.sum | 267 ++++++++++++++- .../20211006231400_create_keys_table.up.sql | 16 + models/asymmetric_key.go | 181 ++++++++++ models/errors.go | 8 + models/user.go | 18 + 30 files changed, 1190 insertions(+), 46 deletions(-) create mode 100644 api/asymmetric_login_test.go create mode 100644 api/asymmetric_signin.go create mode 100644 migrations/20211006231400_create_keys_table.up.sql create mode 100644 models/asymmetric_key.go diff --git a/README.md b/README.md index edd1c314c8..0225ea7f19 100644 --- a/README.md +++ b/README.md @@ -189,13 +189,17 @@ The name to use for the service. ```properties GOTRUE_JWT_SECRET=supersecretvalue +GOTRUE_JWT_ALGORITHM=RS256 GOTRUE_JWT_EXP=3600 GOTRUE_JWT_AUD=netlify ``` +`JWT_ALGORITHM` - `string` + +The signing algorithm for the JWT. Defaults to HS256. `JWT_SECRET` - `string` **required** -The secret used to sign JWT tokens with. +The secret used to sign JWT tokens with. If signing alogrithm is RS256, secret has to be Base64 encoded RSA private key. `JWT_EXP` - `number` @@ -987,3 +991,52 @@ External provider should redirect to here Redirects to `#access_token=&refresh_token=&provider_token=&expires_in=3600&provider=` If additional scopes were requested then `provider_token` will be populated, you can use this to fetch additional data from the provider or interact with their services +### **POST /sign_challenge** + + This is an endpoint for user sign up with Asymmetric key. + Currently implemets only sign up with Ethereum address( not public key). + + body: + ```json + // Sign up with Metamask browser extension + { + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "ETH" + } + ``` + + Returns: + ```json + { + "challenge_token": "d188f5a4-f9d6-4ede-8cfd-2a45927b0edc" + } + ``` + Returned challenge token has to be signed with Metamask and sent back to /asymmetric_login + +### **POST /asymmetric_login** + + This is an endpoint for user sign in with Asymmetric key. + Accepts signed challenge token from `/sign_challenge` endpoint + + body: + ```json + // Login with with Metamask browser extension + { + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "challenge_token_signature": "0x3129682f92a0f3f6ef648623c3256ae39ab16de4fefcc50c60a375c8dd224dde291f750d0fd3d475b403a00a631dd8979583b8d036d2e3b2408668a1b4ea6b321c" + } + ``` + + Returns: + ```json + { + "access_token": "jwt-token-representing-the-user", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "a-refresh-token" + } + ``` + + Once you have an access token, you can access the methods requiring authentication + by settings the `Authorization: Bearer YOUR_ACCESS_TOKEN_HERE` header. + diff --git a/api/admin_test.go b/api/admin_test.go index b6e450c690..5f0dae6a34 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -54,12 +54,16 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error finding keys") + + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) require.NoError(ts.T(), err, "Error parsing token") @@ -70,12 +74,15 @@ func (ts *AdminTestSuite) makeSystemUser() string { u := models.NewSystemUser(uuid.Nil, ts.Config.JWT.Aud) u.Role = "service_role" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error finding keys") + + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) require.NoError(ts.T(), err, "Error parsing token") diff --git a/api/api.go b/api/api.go index 116707de66..eae132a08e 100644 --- a/api/api.go +++ b/api/api.go @@ -117,6 +117,8 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati sharedLimiter := api.limitEmailSentHandler() r.With(sharedLimiter).With(api.requireAdminCredentials).Post("/invite", api.Invite) r.With(sharedLimiter).With(api.verifyCaptcha).Post("/signup", api.Signup) + r.With(sharedLimiter).With(api.verifyCaptcha).Post("/sign_challenge", api.GetChallengeToken) + r.With(sharedLimiter).With(api.verifyCaptcha).Post("/asymmetric_login", api.SignInWithAsymmetricKey) r.With(sharedLimiter).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover) r.With(sharedLimiter).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink) diff --git a/api/asymmetric_login_test.go b/api/asymmetric_login_test.go new file mode 100644 index 0000000000..bab447b0c2 --- /dev/null +++ b/api/asymmetric_login_test.go @@ -0,0 +1,309 @@ +package api + +import ( + "bytes" + "crypto/ecdsa" + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/golang-jwt/jwt" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +type ChallengeTokenTestSuite struct { + suite.Suite + API *API + Config *conf.Configuration +} + +func TestGetChallengeToken(t *testing.T) { + api, config, _, err := setupAPIForTestForInstance() + require.NoError(t, err) + + ts := &ChallengeTokenTestSuite{ + API: api, + Config: config, + } + defer api.db.Close() + + suite.Run(t, ts) +} + +func (ts *ChallengeTokenTestSuite) SetupTest() { + models.TruncateAll(ts.API.db) +} + +// TestSignup tests API /signup route +func (ts *SignupTestSuite) TestSuccessfulGetChallengeToken() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + jsonData := GetChallengeTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + + user, key, err := models.FindUserWithAsymmetrickey(ts.API.db, "0x6BE46d7D863666546b77951D5dfffcF075F36E68") + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), user) + require.NotEmpty(ts.T(), key) + assert.Equal(ts.T(), key.ChallengeToken.String(), jsonData.ChallengeToken) + +} + +func (ts *SignupTestSuite) TestWrongAlgorithmGetChallengeToken() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "test", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":422,"msg":"Key verification failed: Provided algorithm is not supported"}`), msg) +} + +func (ts *SignupTestSuite) TestWrongKeyFormatGetChallengeToken() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "testtest", + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":422,"msg":"Key verification failed: Provided key cannot be ETH address"}`), msg) +} + +type AsymmetricSignInTestSuite struct { + suite.Suite + API *API + Config *conf.Configuration + + privateKey *ecdsa.PrivateKey + address string + challengeToken string +} + +func TestSignInWithAsymmetricKey(t *testing.T) { + api, config, _, err := setupAPIForTestForInstance() + require.NoError(t, err) + + ts := &AsymmetricSignInTestSuite{ + API: api, + Config: config, + } + defer api.db.Close() + + suite.Run(t, ts) +} + +func (ts *AsymmetricSignInTestSuite) SetupTest() { + models.TruncateAll(ts.API.db) + + privateKey, err := crypto.GenerateKey() + require.NoError(ts.T(), err) + ts.privateKey = privateKey + ts.address = crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": ts.address, + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + jsonData := GetChallengeTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + ts.challengeToken = jsonData.ChallengeToken +} + +func (ts *AsymmetricSignInTestSuite) TestSuccessfulSignIn() { + hash := models.SignEthMessageHash([]byte(ts.challengeToken)) + signature, err := crypto.Sign(hash, ts.privateKey) + require.NoError(ts.T(), err) + fmt.Println("Signature1:", hexutil.Encode(signature)) + signature[64] += 27 + + fmt.Println("Signature2:", hexutil.Encode(signature)) + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": ts.address, + "challenge_token_signature": hexutil.Encode(signature), + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/asymmetric_login", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + jsonData := AccessTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + + jwtToken, err := jwt.ParseWithClaims(jsonData.Token, &GoTrueClaims{}, func(token *jwt.Token) (interface{}, error) { + return ts.Config.JWT.GetVerificationKey(), nil + }) + require.NoError(ts.T(), err) + require.True(ts.T(), jwtToken.Valid) + + claims, ok := jwtToken.Claims.(*GoTrueClaims) + require.True(ts.T(), ok) + + require.Equal(ts.T(), ts.address, claims.MainAsymmetricKey) + require.Equal(ts.T(), "ETH", claims.MainAsymmetricKeyAlgorithm) +} + +func (ts *AsymmetricSignInTestSuite) TestSignatureWithout0xPrefixSignIn() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": ts.address, + "challenge_token_signature": "testest", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/asymmetric_login", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":422,"msg":"Signature verification failed:hex string without 0x prefix"}`), msg) +} + +func (ts *AsymmetricSignInTestSuite) TestWrongSignatureFormatSignIn() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": ts.address, + "challenge_token_signature": "0x39c95778651840beee168d95577abe5e42d83bf88ba6e39569de2d2bd674da6f2844a42d45206f09f945fb1768e9c7045e818ea5bee0dce1258005e43855b50601", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/asymmetric_login", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":422,"msg":"Signature verification failed:Provided signature has wrong format"}`), msg) +} + +func (ts *AsymmetricSignInTestSuite) TestAnotherKeySignatureSignIn() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": ts.address, + "challenge_token_signature": "0x89568ade6b6f87652de7832b83652176788862bf6b2EB4260ef8d7f98dc067475e2d0fdb2aee6c5630d94e3c4a596acd8c62ce97bce2946f2003908c375116da1c", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/asymmetric_login", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnprocessableEntity, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":422,"msg":"Signature verification failed:Provided signature does not match with Key"}`), msg) +} + +func (ts *AsymmetricSignInTestSuite) TestMissingKeySignIn() { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "challenge_token_signature": "0x89568ade6b6f87652de7832b83652176788862bf6b2EB4260ef8d7f98dc067475e2d0fdb2aee6c5630d94e3c4a596acd8c62ce97bce2946f2003908c375116da1c", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/asymmetric_login", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusUnauthorized, w.Code) + + msg, err := ioutil.ReadAll(w.Body) + require.NoError(ts.T(), err) + require.Equal(ts.T(), []byte(`{"code":401,"msg":"Unauthorized"}`), msg) +} diff --git a/api/asymmetric_signin.go b/api/asymmetric_signin.go new file mode 100644 index 0000000000..f2e3954bd7 --- /dev/null +++ b/api/asymmetric_signin.go @@ -0,0 +1,159 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/models" + + "github.com/netlify/gotrue/storage" +) + +// GetChallengeTokenParams are the parameters the Signup endpoint accepts +type GetChallengeTokenParams struct { + Key string `json:"key"` + Algorithm string `json:"algorithm"` +} + +// GetChallengeTokenResponse is the response struct from Signup endpoint +type GetChallengeTokenResponse struct { + ChallengeToken string `json:"challenge_token"` +} + +func (a *API) GetChallengeToken(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.getConfig(ctx) + + params := &GetChallengeTokenParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Could not read GetChallengeTokenParams params: %v", err) + } + + err = models.VerifyKeyAndAlgorithm(params.Key, params.Algorithm) + if err != nil { + return unprocessableEntityError("Key verification failed: %v", err) + } + + user, key, err := models.FindUserWithAsymmetrickey(a.db, params.Key) + var challengeToken uuid.UUID + + if err != nil && !models.IsNotFoundError(err) { + return internalServerError("Database error finding user").WithInternalError(err) + } + + aud := a.requestAud(ctx, r) + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + if user != nil && key != nil { + challengeToken, terr = key.GetChallengeToken(tx) + if terr != nil { + return terr + } + } else if user == nil && key == nil { + if config.DisableSignup { + return forbiddenError("Signups not allowed for this instance") + } + + user, terr = a.signupNewUser(ctx, tx, &SignupParams{ + Email: "", + Phone: "", + Password: "", + Data: nil, + Provider: "AsymmetricKey", + Aud: aud, + }) + if terr != nil { + return terr + } + + key, terr = models.NewAsymmetricKey(user.ID, params.Key, params.Algorithm, true) + if terr != nil { + return terr + } + + if terr := tx.Create(key); terr != nil { + return terr + } + + challengeToken, terr = key.GetChallengeToken(tx) + if terr != nil { + return terr + } + } else { + return internalServerError("Impossible case") + } + return nil + }) + + if err != nil { + return err + } + + return sendJSON(w, http.StatusOK, GetChallengeTokenResponse{ChallengeToken: challengeToken.String()}) +} + +// AsymmetricSignInParams are the parameters the Signin endpoint accepts +type AsymmetricSignInParams struct { + Key string `json:"key"` + ChallengeTokenSignature string `json:"challenge_token_signature"` +} + +func (a *API) SignInWithAsymmetricKey(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.getConfig(ctx) + cookie := r.Header.Get(useCookieHeader) + + params := &AsymmetricSignInParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Could not read AsymmetricSignInParams params: %v", err) + } + + user, key, err := models.FindUserWithAsymmetrickey(a.db, params.Key) + if err != nil && models.IsNotFoundError(err) { + return unauthorizedError("Unauthorized") + } + if err != nil && !models.IsNotFoundError(err) { + return internalServerError("Database error finding key").WithInternalError(err) + } + + if key.IsChallengeTokenExpired() { + return unprocessableEntityError("Key challenge token has been expired") + } + + if err = key.VerifySignature(params.ChallengeTokenSignature); err != nil { + return unprocessableEntityError("Signature verification failed:%v", err) + } + + var token *AccessTokenResponse + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + terr = tx.UpdateOnly(key, "challenge_passed") + if terr != nil { + return terr + } + + token, terr = a.issueRefreshToken(ctx, tx, user) + if terr != nil { + return terr + } + + if cookie != "" && config.Cookie.Duration > 0 { + if terr = a.setCookieTokens(config, token, cookie == useSessionCookie, w); terr != nil { + return internalServerError("Failed to set JWT cookie. %s", terr) + } + } + return nil + }) + + if err != nil { + return err + } + + token.User = user + return sendJSON(w, http.StatusOK, token) +} diff --git a/api/audit_test.go b/api/audit_test.go index dae7bc4513..9a9d679ff2 100644 --- a/api/audit_test.go +++ b/api/audit_test.go @@ -51,12 +51,15 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error finding keys") + + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) require.NoError(ts.T(), err, "Error parsing token") diff --git a/api/auth.go b/api/auth.go index 35d82444b1..be569592a2 100644 --- a/api/auth.go +++ b/api/auth.go @@ -60,9 +60,9 @@ func (a *API) parseJWTClaims(bearer string, r *http.Request, w http.ResponseWrit ctx := r.Context() config := a.getConfig(ctx) - p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + p := jwt.Parser{ValidMethods: []string{config.JWT.Algorithm}} token, err := p.ParseWithClaims(bearer, &GoTrueClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(config.JWT.Secret), nil + return config.JWT.GetVerificationKey(), nil }) if err != nil { a.clearCookieTokens(config, w) diff --git a/api/external.go b/api/external.go index 32e3c69bb0..117e7e538d 100644 --- a/api/external.go +++ b/api/external.go @@ -63,7 +63,7 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e log := getLogEntry(r) log.WithField("provider", providerType).Info("Redirecting to external provider") - token := jwt.NewWithClaims(jwt.SigningMethodHS256, ExternalProviderClaims{ + token := jwt.NewWithClaims(config.JWT.GetSigningMethod(), ExternalProviderClaims{ NetlifyMicroserviceClaims: NetlifyMicroserviceClaims{ StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(5 * time.Minute).Unix(), @@ -76,7 +76,7 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e InviteToken: inviteToken, Referrer: redirectURL, }) - tokenString, err := token.SignedString([]byte(config.JWT.Secret)) + tokenString, err := token.SignedString(config.JWT.GetVerificationKey()) if err != nil { return internalServerError("Error creating state").WithInternalError(err) } @@ -366,9 +366,9 @@ func (a *API) processInvite(ctx context.Context, tx *storage.Connection, userDat func (a *API) loadExternalState(ctx context.Context, state string) (context.Context, error) { config := a.getConfig(ctx) claims := ExternalProviderClaims{} - p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + p := jwt.Parser{ValidMethods: []string{config.JWT.Algorithm}} _, err := p.ParseWithClaims(state, &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(config.JWT.Secret), nil + return config.JWT.GetVerificationKey(), nil }) if err != nil || claims.Provider == "" { return nil, badRequestError("OAuth state is invalid: %v", err) diff --git a/api/external_apple_test.go b/api/external_apple_test.go index 8b2e99a98a..5e385fcc91 100644 --- a/api/external_apple_test.go +++ b/api/external_apple_test.go @@ -24,7 +24,7 @@ func (ts *ExternalTestSuite) TestSignupExternalApple() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_azure_test.go b/api/external_azure_test.go index 4e8c874bf5..8c57e40619 100644 --- a/api/external_azure_test.go +++ b/api/external_azure_test.go @@ -30,7 +30,7 @@ func (ts *ExternalTestSuite) TestSignupExternalAzure() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_bitbucket_test.go b/api/external_bitbucket_test.go index ab48a1e1b8..1c193fec5d 100644 --- a/api/external_bitbucket_test.go +++ b/api/external_bitbucket_test.go @@ -29,7 +29,7 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucket() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_discord_test.go b/api/external_discord_test.go index 916f8855ef..7a6b059858 100644 --- a/api/external_discord_test.go +++ b/api/external_discord_test.go @@ -31,7 +31,7 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscord() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_facebook_test.go b/api/external_facebook_test.go index 253715438f..6d041c891c 100644 --- a/api/external_facebook_test.go +++ b/api/external_facebook_test.go @@ -31,7 +31,7 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebook() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_github_test.go b/api/external_github_test.go index 9ebcda49b5..a65ef806f9 100644 --- a/api/external_github_test.go +++ b/api/external_github_test.go @@ -28,7 +28,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGithub() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_gitlab_test.go b/api/external_gitlab_test.go index 8a8b0fbf08..322ca258f5 100644 --- a/api/external_gitlab_test.go +++ b/api/external_gitlab_test.go @@ -31,7 +31,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitlab() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_google_test.go b/api/external_google_test.go index 8992d0a651..abe8f6928b 100644 --- a/api/external_google_test.go +++ b/api/external_google_test.go @@ -31,7 +31,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogle() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/external_twitch_test.go b/api/external_twitch_test.go index a4e473cae9..066aa8ce4b 100644 --- a/api/external_twitch_test.go +++ b/api/external_twitch_test.go @@ -30,7 +30,7 @@ func (ts *ExternalTestSuite) TestSignupExternalTwitch() { claims := ExternalProviderClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) ts.Require().NoError(err) diff --git a/api/instance_test.go b/api/instance_test.go index 17d720d2a0..ab30899e04 100644 --- a/api/instance_test.go +++ b/api/instance_test.go @@ -132,7 +132,7 @@ func (ts *InstanceTestSuite) TestUpdate() { i, err := models.GetInstanceByUUID(ts.API.db, testUUID) require.NoError(ts.T(), err) - require.Equal(ts.T(), i.BaseConfig.JWT.Secret, "testsecret") + require.Equal(ts.T(), []byte("testsecret"), i.BaseConfig.JWT.GetVerificationKey()) require.Equal(ts.T(), i.BaseConfig.SiteURL, "https://test.mysite.com") } diff --git a/api/invite_test.go b/api/invite_test.go index 92e1db62fd..eaa3ccf0b8 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -61,12 +61,15 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error finding keys") + + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err = p.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil + return ts.Config.JWT.GetVerificationKey(), nil }) require.NoError(ts.T(), err, "Error parsing token") diff --git a/api/middleware.go b/api/middleware.go index a010e70a94..14d9dadca5 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -97,7 +97,7 @@ func (a *API) loadInstanceConfig(w http.ResponseWriter, r *http.Request) (contex claims := NetlifyMicroserviceClaims{} p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} _, err := p.ParseWithClaims(signature, &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(config.JWT.Secret), nil + return config.JWT.GetVerificationKey(), nil }) if err != nil { return nil, badRequestError("Operator microservice signature is invalid: %v", err) diff --git a/api/token.go b/api/token.go index 581b383478..a50db5c107 100644 --- a/api/token.go +++ b/api/token.go @@ -26,6 +26,9 @@ type GoTrueClaims struct { AppMetaData map[string]interface{} `json:"app_metadata"` UserMetaData map[string]interface{} `json:"user_metadata"` Role string `json:"role"` + + MainAsymmetricKey string `json:"asymmetric_key"` + MainAsymmetricKeyAlgorithm string `json:"asymmetric_key_algorithm"` } // AccessTokenResponse represents an OAuth2 success response @@ -332,7 +335,12 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h } } - tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + key, terr := models.FindMainAsymmetricKeyByUser(tx, user) + if terr != nil { + return internalServerError("Database error granting user").WithInternalError(terr) + } + + tokenString, terr = generateAccessToken(user, key, time.Second*time.Duration(config.JWT.Exp), config.JWT.GetSigningMethod(), config.JWT.GetSigningKey()) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } @@ -530,7 +538,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return sendJSON(w, http.StatusOK, token) } -func generateAccessToken(user *models.User, expiresIn time.Duration, secret string) (string, error) { +func generateAccessToken(user *models.User, key *models.AsymmetricKey, expiresIn time.Duration, algorithm jwt.SigningMethod, secret interface{}) (string, error) { claims := &GoTrueClaims{ StandardClaims: jwt.StandardClaims{ Subject: user.ID.String(), @@ -544,8 +552,13 @@ func generateAccessToken(user *models.User, expiresIn time.Duration, secret stri Role: user.Role, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(secret)) + if key != nil { + claims.MainAsymmetricKey = key.Key + claims.MainAsymmetricKeyAlgorithm = key.Algorithm + } + + token := jwt.NewWithClaims(algorithm, claims) + return token.SignedString(secret) } func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User) (*AccessTokenResponse, error) { @@ -564,7 +577,12 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u return internalServerError("Database error granting user").WithInternalError(terr) } - tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + key, terr := models.FindMainAsymmetricKeyByUser(tx, user) + if terr != nil { + return internalServerError("Database error granting user").WithInternalError(terr) + } + + tokenString, terr = generateAccessToken(user, key, time.Second*time.Duration(config.JWT.Exp), config.JWT.GetSigningMethod(), config.JWT.GetSigningKey()) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } diff --git a/api/token_test.go b/api/token_test.go index 972702b87a..79dec3b2be 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -2,13 +2,23 @@ package api import ( "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" "encoding/json" + "encoding/pem" + "fmt" "net/http" "net/http/httptest" "os" "testing" "time" + "github.com/golang-jwt/jwt" + "github.com/netlify/gotrue/storage" + "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" @@ -253,3 +263,61 @@ func (ts *TokenTestSuite) createBannedUser() *models.User { return u } + +func (ts *TokenTestSuite) TestRSAToken() { + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(ts.T(), err) + + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privatekey) + require.NoError(ts.T(), err) + + keyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }, + ) + + ts.Config.JWT.Secret = base64.URLEncoding.EncodeToString(keyPEM) + ts.Config.JWT.Algorithm = "RS256" + ts.Config.JWT.InitializeSigningSecret() + + ctx, err := WithInstanceConfig(context.Background(), ts.Config, uuid.Nil) + + var token *AccessTokenResponse + err = ts.API.db.Transaction(func(tx *storage.Connection) error { + user, terr := ts.API.signupNewUser(ctx, ts.API.db, &SignupParams{ + Aud: "authenticated", + }) + if terr != nil { + return terr + } + + token, err = ts.API.issueRefreshToken(ctx, tx, user) + return err + }) + require.NoError(ts.T(), err) + + jwtToken, err := jwt.ParseWithClaims(token.Token, &GoTrueClaims{}, func(token *jwt.Token) (interface{}, error) { + return ts.Config.JWT.GetVerificationKey(), nil + }) + + require.NoError(ts.T(), err) + require.True(ts.T(), jwtToken.Valid) + require.Equal(ts.T(), jwt.SigningMethodRS256, jwtToken.Method) + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "password": "newpass", + })) + + // Setup request + req := httptest.NewRequest(http.MethodGet, "http://localhost/user", &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Token)) + + // Setup response recorder + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), w.Code, http.StatusOK) +} diff --git a/api/user_test.go b/api/user_test.go index eae0b2c870..e31ad92683 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -248,7 +248,10 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) req.Header.Set("Content-Type", "application/json") - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "error finding keys") + + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) diff --git a/conf/configuration.go b/conf/configuration.go index d0dc4defea..e35a824a69 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -1,13 +1,16 @@ package conf import ( + "crypto/rsa" "database/sql/driver" + "encoding/base64" "encoding/json" "errors" "os" "time" "github.com/gobwas/glob" + jwt "github.com/golang-jwt/jwt" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" ) @@ -49,12 +52,14 @@ type DBConfiguration struct { // JWTConfiguration holds all the JWT related configuration. type JWTConfiguration struct { + Algorithm string `json:"algorithm" default:"HS256"` Secret string `json:"secret" required:"true"` Exp int `json:"exp"` Aud string `json:"aud"` AdminGroupName string `json:"admin_group_name" split_words:"true"` AdminRoles []string `json:"admin_roles" split_words:"true"` DefaultGroupName string `json:"default_group_name" split_words:"true"` + pKey *rsa.PrivateKey } // GlobalConfiguration holds all the configuration that applies to all instances. @@ -360,6 +365,8 @@ func (config *Configuration) ApplyDefaults() { if config.PasswordMinLength < defaultMinPasswordLength { config.PasswordMinLength = defaultMinPasswordLength } + + config.JWT.InitializeSigningSecret() } func (config *Configuration) Value() (driver.Value, error) { @@ -448,3 +455,44 @@ func (t *VonageProviderConfiguration) Validate() error { } return nil } + +func (j *JWTConfiguration) InitializeSigningSecret() { + if j.Algorithm == "RS256" { + pemPrivateKey, err := base64.URLEncoding.DecodeString(j.Secret) + if err != nil { + panic(err) + } + + key, err := jwt.ParseRSAPrivateKeyFromPEM(pemPrivateKey) + if err != nil { + panic(err) + } + + j.pKey = key + } +} + +func (j *JWTConfiguration) GetSigningKey() interface{} { + if j.Algorithm == "RS256" { + return j.pKey + } + + return []byte(j.Secret) +} + +func (j *JWTConfiguration) GetVerificationKey() interface{} { + if j.Algorithm == "RS256" { + return j.pKey.Public() + } + + return []byte(j.Secret) +} + +func (j *JWTConfiguration) GetSigningMethod() jwt.SigningMethod { + switch j.Algorithm { + case "RS256": + return jwt.SigningMethodRS256 + default: + return jwt.SigningMethodHS256 + } +} diff --git a/go.mod b/go.mod index 2746cd1388..772edd8142 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/beevik/etree v1.1.0 github.com/coreos/go-oidc/v3 v3.0.0 github.com/didip/tollbooth/v5 v5.1.1 + github.com/ethereum/go-ethereum v1.10.9 github.com/fatih/color v1.10.0 // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/go-sql-driver/mysql v1.5.0 @@ -40,19 +41,17 @@ require ( github.com/opentracing/opentracing-go v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 - github.com/rs/cors v1.6.0 + github.com/rs/cors v1.7.0 github.com/russellhaering/gosaml2 v0.6.1-0.20210916051624-757d23f1bc28 github.com/russellhaering/goxmldsig v1.1.1 github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 github.com/sethvargo/go-password v0.2.0 github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.1.3 - github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.12.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index e059f2cb73..0df3e6fc1f 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= @@ -21,6 +23,7 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -33,9 +36,24 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= +github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240 h1:bCOIpv1VinSRhS5ezZeCEGG82gib2WtXfiJOHmMSuls= github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= @@ -43,13 +61,29 @@ github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 h1:vMqcPzLT1/mbYew0gM6EJy4/sCNy9lY9rmlFO+pPwhY= @@ -62,19 +96,36 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -87,37 +138,65 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/didip/tollbooth/v5 v5.1.1 h1:QpKFg56jsbNuQ6FFj++Z1gn2fbBsvAc1ZPLUaDOYW5k= github.com/didip/tollbooth/v5 v5.1.1/go.mod h1:d9rzwOULswrD3YIrAQmP3bfjxab32Df4IaO6+D25l9g= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.10.9 h1:uMSWt0qDhaqqCk0PWqfDFOMUExmk4Tnbma6c6oXW+Pk= +github.com/ethereum/go-ethereum v1.10.9/go.mod h1:CaTMQrv51WaAlD2eULQ3f03KiahDRO28fleQcKjWrrg= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -172,12 +251,16 @@ github.com/gobuffalo/validate/v3 v3.3.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwy github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -203,10 +286,16 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -214,8 +303,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -227,6 +318,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -235,18 +327,21 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -263,17 +358,34 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639 h1:VMd01CgpBpmLpuERyY4Oibn2PpcVS1fK9sjh5UZG8+o= github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -335,6 +447,11 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= @@ -343,11 +460,16 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= @@ -358,7 +480,13 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -372,6 +500,10 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM= github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -385,12 +517,15 @@ github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRn github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -398,9 +533,13 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -408,10 +547,14 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc= @@ -426,11 +569,16 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nats-io/jwt v0.2.6/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY= github.com/nats-io/nats-server/v2 v2.0.0/go.mod h1:RyVdsHHvY4B6c9pWG+uRLpZ0h0XsqiuKp2XCTurP5LI= github.com/nats-io/nats-streaming-server v0.15.1/go.mod h1:bJ1+2CS8MqvkGfr/NwnCF+Lw6aLnL3F5kenM8bZmdCw= @@ -443,7 +591,18 @@ github.com/netlify/mailme v1.1.1 h1:S/ANl+Hy/EIoJUgGiLJYYLZJ2QOTG452R73qTQudMns= github.com/netlify/mailme v1.1.1/go.mod h1:8g03BJmU+ps7ma5vcH+t8aMtaicQTMX3ffP7RJ8xY8g= github.com/netlify/netlify-commons v0.32.0 h1:IgpqedBa6aFrc+daRgGZ+SmU9eBXlDXzKSAjevWmshM= github.com/netlify/netlify-commons v0.32.0/go.mod h1:xZH7auZrc/N/ZKS9BRO74yNf8i9LitXq1h6JVFZ2jTc= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -451,32 +610,42 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/patrickmn/go-cache v0.0.0-20170418232947-7ac151875ffb/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -486,8 +655,9 @@ github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -502,11 +672,14 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM= github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:DmcHeT/UuSDXaCVb8IijmL+fHX+FK9TLy98W7mfDXXg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= @@ -528,6 +701,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.4-0.20190321000552-67fc4837d267/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= @@ -541,24 +715,38 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -580,6 +768,7 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -590,15 +779,21 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= @@ -608,6 +803,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -628,9 +824,11 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20161007143504-f4b625ec9b21/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -660,12 +858,19 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -683,11 +888,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -707,10 +914,14 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -723,12 +934,23 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -740,6 +962,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -747,11 +972,15 @@ golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -779,6 +1008,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -801,6 +1031,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -808,6 +1039,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -839,6 +1076,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -846,6 +1084,7 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= @@ -904,21 +1143,28 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -927,6 +1173,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -934,6 +1181,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/migrations/20211006231400_create_keys_table.up.sql b/migrations/20211006231400_create_keys_table.up.sql new file mode 100644 index 0000000000..4494907546 --- /dev/null +++ b/migrations/20211006231400_create_keys_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE auth.asymmetric_keys ( + id bigserial NOT NULL, + user_id uuid NOT NULL, + key VARCHAR ( 150 ) UNIQUE NOT NULL, + algorithm VARCHAR (15) NOT NULL, + main bool DEFAULT false NOT NULL, + challenge_token uuid NOT NULL, + challenge_token_issued_at timestamptz NOT NULL, + challenge_token_expires_at timestamptz NOT NULL, + challenge_passed bool DEFAULT false NOT NULL, + + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT asymmetric_keys_pkey PRIMARY KEY (id), + CONSTRAINT asymmetric_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE +); diff --git a/models/asymmetric_key.go b/models/asymmetric_key.go new file mode 100644 index 0000000000..a310bcba47 --- /dev/null +++ b/models/asymmetric_key.go @@ -0,0 +1,181 @@ +package models + +import ( + "database/sql" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" +) + +const challengeTokenExpirationDuration = 30 * time.Minute + +var AlgorithmNotSupportedError = errors.New("Provided algorithm is not supported") +var WrongEthAddressFormatError = errors.New("Provided key cannot be ETH address") +var WrongSignatureFormatError = errors.New("Provided signature has wrong format") +var WrongPublicKeyError = errors.New("Provided signature does not match with Key") + +// RefreshToken is the database model for refresh tokens. +type AsymmetricKey struct { + ID int64 `db:"id"` + UserID uuid.UUID `db:"user_id"` + Key string `db:"key"` + Algorithm string `db:"algorithm"` + Main bool `db:"main"` + + ChallengeToken uuid.UUID `db:"challenge_token"` + ChallengeTokenIssuedAt time.Time `db:"challenge_token_issued_at"` + ChallengeTokenExpiresAt time.Time `db:"challenge_token_expires_at"` + ChallengePassed bool `db:"challenge_passed"` + + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (AsymmetricKey) TableName() string { + tableName := "asymmetric_keys" + return tableName +} + +func NewAsymmetricKey(userId uuid.UUID, pubkey, algorithm string, main bool) (*AsymmetricKey, error) { + err := VerifyKeyAndAlgorithm(pubkey, algorithm) + if err != nil { + return nil, err + } + + k := &AsymmetricKey{ + UserID: userId, + Key: pubkey, + Algorithm: algorithm, + Main: main, + } + + k.generateChallengeToken() + return k, nil +} + +func (a *AsymmetricKey) IsChallengeTokenExpired() bool { + return time.Now().Unix() >= a.ChallengeTokenExpiresAt.Unix() || a.ChallengePassed +} + +func (a *AsymmetricKey) GetChallengeToken(tx *storage.Connection) (uuid.UUID, error) { + if a.IsChallengeTokenExpired() { + err := a.generateChallengeToken() + if err != nil { + return uuid.Nil, err + } + + err = tx.UpdateOnly( + a, + "challenge_token", + "challenge_token_issued_at", + "challenge_token_expires_at", + "challenge_passed") + + if err != nil { + return uuid.Nil, err + } + } + + return a.ChallengeToken, nil +} + +func (a *AsymmetricKey) generateChallengeToken() error { + newToken, err := uuid.NewV4() + if err != nil { + return err + } + + a.ChallengeToken = newToken + a.ChallengeTokenIssuedAt = time.Now() + a.ChallengeTokenExpiresAt = time.Now().Add(challengeTokenExpirationDuration) + a.ChallengePassed = false + + return nil +} + +func (a *AsymmetricKey) VerifySignature(signature string) error { + var err error + switch a.Algorithm { + case "ETH": + err = a.verifyEthKeySignature(signature) + default: + return AlgorithmNotSupportedError + } + + if err == nil { + a.ChallengePassed = true + } + return err +} + +func (a *AsymmetricKey) verifyEthKeySignature(rawSignature string) error { + signature, err := hexutil.Decode(rawSignature) + if err != nil { + return err + } + + // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 + // Note, the signature must conform to the secp256k1 curve R, S and V values, where + // the V value must be be 27 or 28 for legacy reasons. + if signature[64] != 27 && signature[64] != 28 { + return WrongSignatureFormatError + } + signature[64] -= 27 + + signaturePublicKey, err := crypto.SigToPub(SignEthMessageHash([]byte(a.ChallengeToken.String())), signature) + if err != nil { + return err + } + + addr := crypto.PubkeyToAddress(*signaturePublicKey) + if addr.String() != a.Key { + return WrongPublicKeyError + } + + return nil +} + +// verifyKeyAndAlgorithm verifies public key format for specific algorithm. +// If key satisfies conditions, nil is returned +func VerifyKeyAndAlgorithm(pubkey, algorithm string) error { + var err error + switch algorithm { + case "ETH": + err = verifyEthKey(pubkey) + default: + return AlgorithmNotSupportedError + } + return err +} + +func verifyEthKey(key string) error { + if common.IsHexAddress(key) { + return nil + } + return WrongEthAddressFormatError +} + +// SignEthMessageHash is a helper function that calculates a hash for the given message in the Ethereum format +// The hash is calculated as +// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). +func SignEthMessageHash(data []byte) []byte { + msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) + return crypto.Keccak256([]byte(msg)) +} + +// FindMainAsymmetricKeyByUser is the helper function that finds the main( used for sign up) Asymmetric key for the given User. +func FindMainAsymmetricKeyByUser(tx *storage.Connection, user *User) (*AsymmetricKey, error) { + key := &AsymmetricKey{} + if err := tx.Q().Where("user_id = ? and main = true", user.ID).First(key); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return &AsymmetricKey{}, nil + } + return &AsymmetricKey{}, errors.Wrap(err, "error finding keys") + } + return key, nil +} diff --git a/models/errors.go b/models/errors.go index 6c33c70c24..a72f591476 100644 --- a/models/errors.go +++ b/models/errors.go @@ -9,6 +9,8 @@ func IsNotFoundError(err error) bool { return true case RefreshTokenNotFoundError: return true + case AsymmetricKeyNotFoundError: + return true case InstanceNotFoundError: return true case TotpSecretNotFoundError: @@ -47,6 +49,12 @@ func (e RefreshTokenNotFoundError) Error() string { return "Refresh Token not found" } +type AsymmetricKeyNotFoundError struct{} + +func (e AsymmetricKeyNotFoundError) Error() string { + return "Asymmetric Key not found" +} + // InstanceNotFoundError represents when an instance is not found. type InstanceNotFoundError struct{} diff --git a/models/user.go b/models/user.go index 46bba3c4fa..c0e91e6d23 100644 --- a/models/user.go +++ b/models/user.go @@ -423,6 +423,24 @@ func FindUserWithRefreshToken(tx *storage.Connection, token string) (*User, *Ref return user, refreshToken, nil } +// FindUserWithRefreshToken finds a user from the provided refresh token. +func FindUserWithAsymmetrickey(tx *storage.Connection, key string) (*User, *AsymmetricKey, error) { + asymmetricKey := &AsymmetricKey{} + if err := tx.Where("key = ? and main = true", key).First(asymmetricKey); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, nil, AsymmetricKeyNotFoundError{} + } + return nil, nil, errors.Wrap(err, "error finding asymmetric key") + } + + user, err := findUser(tx, "id = ?", asymmetricKey.UserID) + if err != nil { + return nil, nil, err + } + + return user, asymmetricKey, nil +} + // FindUsersInAudience finds users with the matching audience. func FindUsersInAudience(tx *storage.Connection, instanceID uuid.UUID, aud string, pageParams *Pagination, sortParams *SortParams, filter string) ([]*User, error) { users := []*User{} From 30d6ab81d2b128a721602c3c2096e7092e711a55 Mon Sep 17 00:00:00 2001 From: Danylo Patsora Date: Thu, 3 Mar 2022 17:18:58 +0700 Subject: [PATCH 097/102] Enhancement: Add Kaigara to the Dockerfile --- Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 56c222b235..fd2f9ec74e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ENV GO111MODULE=on ENV CGO_ENABLED=0 ENV GOOS=linux -RUN apk add --no-cache make git +RUN apk add --no-cache make git curl WORKDIR /go/src/github.com/netlify/gotrue @@ -15,11 +15,16 @@ RUN make deps COPY . /go/src/github.com/netlify/gotrue RUN make build +ARG KAIGARA_VERSION=0.1.29 +RUN curl -Lo ./kaigara https://github.com/openware/kaigara/releases/download/${KAIGARA_VERSION}/kaigara \ + && chmod +x ./kaigara + FROM alpine:3.15 RUN adduser -D -u 1000 netlify RUN apk add --no-cache ca-certificates COPY --from=build /go/src/github.com/netlify/gotrue/gotrue /usr/local/bin/gotrue +COPY --from=build /go/src/github.com/netlify/gotrue/kaigara /usr/local/bin/kaigara COPY --from=build /go/src/github.com/netlify/gotrue/migrations /usr/local/etc/gotrue/migrations/ ENV GOTRUE_DB_MIGRATIONS_PATH /usr/local/etc/gotrue/migrations From 3e874a48beadd15cb3719dde79d87333befd78ae Mon Sep 17 00:00:00 2001 From: Danylo Patsora Date: Thu, 3 Mar 2022 17:24:56 +0700 Subject: [PATCH 098/102] Fix: update tests --- api/verify_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/verify_test.go b/api/verify_test.go index 8c65161afc..664e227108 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -106,8 +106,11 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) req.Header.Set("Content-Type", "application/json") + key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error finding keys") + // Generate access token for request - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, key, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) From b75749a952253e758a6f4b9cf6f6e8a663616b27 Mon Sep 17 00:00:00 2001 From: eliot-blankenberg <99249520+eliot-blankenberg@users.noreply.github.com> Date: Tue, 8 Mar 2022 16:08:19 +0700 Subject: [PATCH 099/102] feat: First user as superadmin (#4) --- api/asymmetric_login_test.go | 115 +++++++++++++++++++++++++++++++++++ api/asymmetric_signin.go | 1 + api/signup.go | 19 +++++- api/signup_test.go | 2 +- conf/configuration.go | 27 ++++---- example.env | 1 + go.mod | 9 +-- go.sum | 3 +- models/user.go | 22 +++++++ 9 files changed, 177 insertions(+), 22 deletions(-) diff --git a/api/asymmetric_login_test.go b/api/asymmetric_login_test.go index bab447b0c2..05ba7441e0 100644 --- a/api/asymmetric_login_test.go +++ b/api/asymmetric_login_test.go @@ -120,6 +120,121 @@ func (ts *SignupTestSuite) TestWrongKeyFormatGetChallengeToken() { require.Equal(ts.T(), []byte(`{"code":422,"msg":"Key verification failed: Provided key cannot be ETH address"}`), msg) } +func (ts *SignupTestSuite) TestFirstSignInSuperAdmin() { + //FirstUser is SuperAdmin config on true + ts.Config.FirstUserSuperAdmin = true + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + jsonData := GetChallengeTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + + user, key, err := models.FindUserWithAsymmetrickey(ts.API.db, "0x6BE46d7D863666546b77951D5dfffcF075F36E68") + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), user) + require.NotEmpty(ts.T(), key) + assert.Equal(ts.T(), key.ChallengeToken.String(), jsonData.ChallengeToken) + assert.Equal(ts.T(), user.Role, "superadmin") + assert.Equal(ts.T(), user.IsSuperAdmin, true) +} + +func (ts *SignupTestSuite) TestFirstSignInConfigFalse() { + //FirstUser is SuperAdmin config on false + ts.Config.FirstUserSuperAdmin = false + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + jsonData := GetChallengeTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + + user, key, err := models.FindUserWithAsymmetrickey(ts.API.db, "0x6BE46d7D863666546b77951D5dfffcF075F36E68") + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), user) + require.NotEmpty(ts.T(), key) + assert.Equal(ts.T(), key.ChallengeToken.String(), jsonData.ChallengeToken) + assert.Equal(ts.T(), user.Role, "authenticated") + assert.Equal(ts.T(), user.IsSuperAdmin, false) +} + +func (ts *SignupTestSuite) TestNotFirstSignIn() { + //FirstUser is SuperAdmin config on true + ts.Config.FirstUserSuperAdmin = true + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546b77951D5dfffcF075F36E68", + "algorithm": "ETH", + })) + + var buffer2 bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer2).Encode(map[string]interface{}{ + "key": "0x6BE46d7D863666546677951D5dfffcF075F36E68", + "algorithm": "ETH", + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + + req = httptest.NewRequest(http.MethodPost, "/sign_challenge", &buffer2) + req.Header.Set("Content-Type", "application/json") + + ts.API.handler.ServeHTTP(w, req) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + jsonData := GetChallengeTokenResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&jsonData)) + require.NotEmpty(ts.T(), jsonData) + + user, key, err := models.FindUserWithAsymmetrickey(ts.API.db, "0x6BE46d7D863666546677951D5dfffcF075F36E68") + require.NoError(ts.T(), err) + require.NotEmpty(ts.T(), user) + require.NotEmpty(ts.T(), key) + assert.Equal(ts.T(), user.Role, "authenticated") + assert.Equal(ts.T(), user.IsSuperAdmin, false) +} + type AsymmetricSignInTestSuite struct { suite.Suite API *API diff --git a/api/asymmetric_signin.go b/api/asymmetric_signin.go index f2e3954bd7..8f17483702 100644 --- a/api/asymmetric_signin.go +++ b/api/asymmetric_signin.go @@ -65,6 +65,7 @@ func (a *API) GetChallengeToken(w http.ResponseWriter, r *http.Request) error { Provider: "AsymmetricKey", Aud: aud, }) + if terr != nil { return terr } diff --git a/api/signup.go b/api/signup.go index 2cbef93eaa..a6fbbf7572 100644 --- a/api/signup.go +++ b/api/signup.go @@ -305,12 +305,27 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param err = conn.Transaction(func(tx *storage.Connection) error { var terr error + userExist, terr := models.AnyUser(tx) + + if terr != nil { + return terr + } + if terr = tx.Create(user); terr != nil { return internalServerError("Database error saving new user").WithInternalError(terr) } - if terr = user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil { - return internalServerError("Database error updating user").WithInternalError(terr) + + if config.FirstUserSuperAdmin && !userExist { + terr = user.SetSuperAdmin(tx) + if terr != nil { + return terr + } + } else { + if terr = user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil { + return internalServerError("Database error updating user").WithInternalError(terr) + } } + if terr = triggerEventHooks(ctx, tx, ValidateEvent, user, instanceID, config); terr != nil { return terr } diff --git a/api/signup_test.go b/api/signup_test.go index 140d52ab49..1faa531587 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -117,7 +117,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { assert.Len(u, 10) // assert.Equal(t, user.ID, u["id"]) TODO assert.Equal("authenticated", u["aud"]) - assert.Equal("authenticated", u["role"]) + assert.Equal("superadmin", u["role"]) assert.Equal("test@example.com", u["email"]) appmeta, ok := u["app_metadata"].(map[string]interface{}) diff --git a/conf/configuration.go b/conf/configuration.go index e35a824a69..96df370ee0 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -193,19 +193,20 @@ type SecurityConfiguration struct { // Configuration holds all the per-instance configuration. type Configuration struct { - SiteURL string `json:"site_url" split_words:"true" required:"true"` - URIAllowList []string `json:"uri_allow_list" split_words:"true"` - URIAllowListMap map[string]glob.Glob - PasswordMinLength int `json:"password_min_length" split_words:"true"` - JWT JWTConfiguration `json:"jwt"` - SMTP SMTPConfiguration `json:"smtp"` - Mailer MailerConfiguration `json:"mailer"` - External ProviderConfiguration `json:"external"` - Sms SmsProviderConfiguration `json:"sms"` - DisableSignup bool `json:"disable_signup" split_words:"true"` - Webhook WebhookConfig `json:"webhook" split_words:"true"` - Security SecurityConfiguration `json:"security"` - Cookie struct { + SiteURL string `json:"site_url" split_words:"true" required:"true"` + URIAllowList []string `json:"uri_allow_list" split_words:"true"` + URIAllowListMap map[string]glob.Glob + PasswordMinLength int `json:"password_min_length" split_words:"true"` + JWT JWTConfiguration `json:"jwt"` + SMTP SMTPConfiguration `json:"smtp"` + Mailer MailerConfiguration `json:"mailer"` + External ProviderConfiguration `json:"external"` + Sms SmsProviderConfiguration `json:"sms"` + DisableSignup bool `json:"disable_signup" split_words:"true"` + FirstUserSuperAdmin bool `json:"first_user_super_admin" split_words:"true"` + Webhook WebhookConfig `json:"webhook" split_words:"true"` + Security SecurityConfiguration `json:"security"` + Cookie struct { Key string `json:"key"` Domain string `json:"domain"` Duration int `json:"duration"` diff --git a/example.env b/example.env index e707c114cd..f4147f276a 100644 --- a/example.env +++ b/example.env @@ -50,6 +50,7 @@ GOTRUE_SITE_URL="http://localhost:3000" GOTRUE_EXTERNAL_EMAIL_ENABLED="true" GOTRUE_EXTERNAL_PHONE_ENABLED="true" GOTRUE_EXTERNAL_IOS_BUNDLE_ID="com.supabase.gotrue" +GOTRUE_FIRST_USER_SUPER_ADMIN="true" # Whitelist redirect to URLs here GOTRUE_URI_ALLOW_LIST=["http://localhost:3000"] diff --git a/go.mod b/go.mod index 772edd8142..689c0e77c9 100644 --- a/go.mod +++ b/go.mod @@ -20,14 +20,14 @@ require ( github.com/gobuffalo/plush/v4 v4.1.0 // indirect github.com/gobuffalo/pop/v5 v5.3.3 github.com/gobuffalo/validate/v3 v3.3.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect + github.com/gobwas/glob v0.2.3 github.com/gofrs/uuid v4.0.0+incompatible github.com/golang-jwt/jwt v3.2.1+incompatible github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.1 github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639 - github.com/jackc/pgconn v1.8.0 // indirect - github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 // indirect + github.com/jackc/pgconn v1.8.0 + github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 github.com/jackc/pgproto3/v2 v2.0.7 // indirect github.com/jmoiron/sqlx v1.3.1 // indirect github.com/joho/godotenv v1.3.0 @@ -35,7 +35,7 @@ require ( github.com/lestrrat-go/jwx v0.9.0 github.com/lib/pq v1.9.0 // indirect github.com/microcosm-cc/bluemonday v1.0.16 // indirect - github.com/mitchellh/mapstructure v1.1.2 + github.com/mitchellh/mapstructure v1.4.1 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 github.com/netlify/mailme v1.1.1 github.com/opentracing/opentracing-go v1.1.0 @@ -54,6 +54,7 @@ require ( golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 gopkg.in/DataDog/dd-trace-go.v1 v1.12.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 0df3e6fc1f..2433eb759d 100644 --- a/go.sum +++ b/go.sum @@ -567,8 +567,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1169,7 +1169,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20190924164351-c8b7dadae555/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/models/user.go b/models/user.go index c0e91e6d23..f1dd215b7a 100644 --- a/models/user.go +++ b/models/user.go @@ -179,6 +179,16 @@ func (u *User) SetRole(tx *storage.Connection, roleName string) error { return tx.UpdateOnly(u, "role") } +//SetSuperAdmin sets the user as SuperAdmin +func (u *User) SetSuperAdmin(tx *storage.Connection) error { + u.IsSuperAdmin = true + err := u.SetRole(tx, "superadmin") + if err != nil { + return err + } + return tx.UpdateOnly(u, "is_super_admin") +} + // HasRole returns true when the users role is set to roleName func (u *User) HasRole(roleName string) bool { return u.Role == roleName @@ -360,6 +370,18 @@ func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, return obj, nil } +func AnyUser(tx *storage.Connection) (bool, error) { + obj := &User{} + err := tx.Eager().Q().First(obj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return false, nil + } + return false, err + } + return true, nil +} + // FindUserByConfirmationToken finds users with the matching confirmation token. func FindUserByConfirmationToken(tx *storage.Connection, token string) (*User, error) { user, err := findUser(tx, "confirmation_token = ?", token) From 94da029e833a0a9a2d4212237d82e16cb1e69672 Mon Sep 17 00:00:00 2001 From: Vee Date: Mon, 28 Mar 2022 14:23:20 +0700 Subject: [PATCH 100/102] Feature: Upgrade Kaigara to support sql storage (#5) * Upgrade Kaigara to version 0.1.34 Co-authored-by: Anton Filonenko --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fd2f9ec74e..708a0a699a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN make deps COPY . /go/src/github.com/netlify/gotrue RUN make build -ARG KAIGARA_VERSION=0.1.29 +ARG KAIGARA_VERSION=0.1.34 RUN curl -Lo ./kaigara https://github.com/openware/kaigara/releases/download/${KAIGARA_VERSION}/kaigara \ && chmod +x ./kaigara From 451848adbc7187c0f398870af5352d67ce5dd07a Mon Sep 17 00:00:00 2001 From: Valentine Shatravenko Date: Tue, 29 Mar 2022 10:53:30 +0700 Subject: [PATCH 101/102] feature(ci): add .drone.yml w/ Docker build --- .drone.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000000..d19c3a6f7c --- /dev/null +++ b/.drone.yml @@ -0,0 +1,26 @@ +--- +type: docker +kind: pipeline +name: "Main" + +steps: + - name: Docker build Git SHA + image: plugins/docker:20 + pull: if-not-exists + environment: + DOCKER_BUILDKIT: 1 + settings: + username: + from_secret: quay_username + password: + from_secret: quay_password + repo: quay.io/openware/gotrue + registry: quay.io + tag: ${DRONE_COMMIT:0:7} + purge: false + when: + event: + - push + branch: + - stable/ow + - feature/asymmetric-auth From f943b0fb3465c3db559674a2f493187ce8b8fa61 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 3 May 2022 16:23:39 +0300 Subject: [PATCH 102/102] feature: update Kaigara version in the Dockerfile --- Dockerfile | 2 +- api/admin_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 708a0a699a..c1eafc6daf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN make deps COPY . /go/src/github.com/netlify/gotrue RUN make build -ARG KAIGARA_VERSION=0.1.34 +ARG KAIGARA_VERSION=v1.0.10 RUN curl -Lo ./kaigara https://github.com/openware/kaigara/releases/download/${KAIGARA_VERSION}/kaigara \ && chmod +x ./kaigara diff --git a/api/admin_test.go b/api/admin_test.go index 5f0dae6a34..40471924c7 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -54,7 +54,6 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - key, err := models.FindMainAsymmetricKeyByUser(ts.API.db, u) require.NoError(ts.T(), err, "Error finding keys")