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 diff --git a/Dockerfile b/Dockerfile index 3430f97797..71f22591ef 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=v1.0.10 +RUN curl -Lo ./kaigara https://github.com/openware/kaigara/releases/download/${KAIGARA_VERSION}/kaigara \ + && chmod +x ./kaigara + FROM alpine:3.17 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 diff --git a/README.md b/README.md index bd376adf2c..9896ab1808 100644 --- a/README.md +++ b/README.md @@ -395,13 +395,17 @@ by default. ```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` diff --git a/api/admin_test.go b/api/admin_test.go index 9e0964344e..940b61cab4 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -11,11 +11,12 @@ import ( "time" jwt "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type AdminTestSuite struct { diff --git a/api/api.go b/api/api.go index 07f2081adf..7b29288b48 100644 --- a/api/api.go +++ b/api/api.go @@ -9,13 +9,16 @@ import ( "github.com/didip/tollbooth/v5" "github.com/didip/tollbooth/v5/limiter" "github.com/go-chi/chi" + "github.com/sirupsen/logrus" + "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/mailer" - "github.com/netlify/gotrue/observability" "github.com/netlify/gotrue/storage" + "github.com/rs/cors" "github.com/sebest/xff" - "github.com/sirupsen/logrus" + + "github.com/netlify/gotrue/mailer" + "github.com/netlify/gotrue/observability" ) const ( diff --git a/api/audit_test.go b/api/audit_test.go index ca3cc36e8c..4fba7ea558 100644 --- a/api/audit_test.go +++ b/api/audit_test.go @@ -9,11 +9,12 @@ import ( "time" jwt "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type AuditTestSuite struct { @@ -49,12 +50,12 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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 eeec1f0fc9..1587bf3493 100644 --- a/api/auth.go +++ b/api/auth.go @@ -8,6 +8,7 @@ import ( "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt" + "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" ) @@ -68,9 +69,9 @@ func (a *API) parseJWTClaims(bearer string, r *http.Request) (context.Context, e ctx := r.Context() config := a.config - 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 { return nil, unauthorizedError("invalid JWT: unable to parse or verify signature, %v", err) diff --git a/api/external.go b/api/external.go index db6f44297f..230e4de9ce 100644 --- a/api/external.go +++ b/api/external.go @@ -65,7 +65,7 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e log := observability.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(), @@ -77,7 +77,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.GetSigningKey()) if err != nil { return internalServerError("Error creating state").WithInternalError(err) } @@ -424,9 +424,9 @@ func (a *API) processInvite(r *http.Request, ctx context.Context, tx *storage.Co func (a *API) loadExternalState(ctx context.Context, state string) (context.Context, error) { config := a.config 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 eda895b628..917c4930f8 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/invite_test.go b/api/invite_test.go index a1a2e2b74d..6940f2e670 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -12,11 +12,12 @@ import ( "time" jwt "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type InviteTestSuite struct { @@ -59,13 +60,12 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - + token, err = generateAccessToken(ts.API.db, u, nil, 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/logout_test.go b/api/logout_test.go index 47b6a65bce..1b5f7f4aa3 100644 --- a/api/logout_test.go +++ b/api/logout_test.go @@ -7,10 +7,11 @@ import ( "testing" "time" - "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/models" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type LogoutTestSuite struct { @@ -42,7 +43,7 @@ func (ts *LogoutTestSuite) SetupTest() { // generate access token to use for logout var t string - t, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + t, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) ts.token = t } diff --git a/api/mfa_test.go b/api/mfa_test.go index 3214499096..84189b49b1 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -11,10 +11,11 @@ import ( "testing" "time" + "github.com/pquerna/otp" + "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/utilities" - "github.com/pquerna/otp" "github.com/jackc/pgx/v4" @@ -124,7 +125,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { user, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) - token, err := generateAccessToken(ts.API.db, user, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(ts.API.db, user, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -161,7 +162,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { require.NoError(ts.T(), err) f := factors[0] - token, err := generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(ts.API.db, u, nil, 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") var buffer bytes.Buffer @@ -222,7 +223,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { secondarySession.FactorID = &f.ID require.NoError(ts.T(), ts.API.db.Create(secondarySession), "Error saving test session") - token, err := generateAccessToken(ts.API.db, user, r.SessionId, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(ts.API.db, user, r.SessionId, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) @@ -318,7 +319,7 @@ func (ts *MFATestSuite) TestUnenrollVerifiedFactor() { var buffer bytes.Buffer - token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -360,7 +361,7 @@ func (ts *MFATestSuite) TestUnenrollUnverifiedFactor() { var buffer bytes.Buffer - token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ "factor_id": f.ID, diff --git a/api/phone_test.go b/api/phone_test.go index 7057746e73..27b479da9c 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -11,12 +11,13 @@ import ( "testing" "time" - "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type PhoneTestSuite struct { @@ -127,7 +128,7 @@ func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() { require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) cases := []struct { diff --git a/api/signup.go b/api/signup.go index 25a9cc6eff..406625a901 100644 --- a/api/signup.go +++ b/api/signup.go @@ -9,12 +9,13 @@ import ( "github.com/fatih/structs" "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/netlify/gotrue/api/provider" "github.com/netlify/gotrue/api/sms_provider" "github.com/netlify/gotrue/metering" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" - "github.com/pkg/errors" ) // SignupParams are the parameters the Signup endpoint accepts @@ -315,11 +316,25 @@ 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.SetRole(tx, "superadmin") + 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, config); terr != nil { return terr diff --git a/api/signup_test.go b/api/signup_test.go index 03d9fb7505..2f167cc258 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -13,11 +13,12 @@ import ( "github.com/gofrs/uuid" jwt "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type SignupTestSuite struct { @@ -111,7 +112,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) 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/api/token.go b/api/token.go index b9ddac19c3..6f51a63507 100644 --- a/api/token.go +++ b/api/token.go @@ -14,6 +14,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/gofrs/uuid" "github.com/golang-jwt/jwt" + "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/metering" "github.com/netlify/gotrue/models" @@ -358,8 +359,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return internalServerError(terr.Error()) } } - tokenString, terr = generateAccessToken(tx, user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) - + tokenString, terr = generateAccessToken(tx, user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.GetSigningMethod(), config.JWT.GetSigningKey()) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } @@ -562,7 +562,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return sendJSON(w, http.StatusOK, token) } -func generateAccessToken(tx *storage.Connection, user *models.User, sessionId *uuid.UUID, expiresIn time.Duration, secret string) (string, error) { +func generateAccessToken(tx *storage.Connection, user *models.User, sessionId *uuid.UUID, expiresIn time.Duration, algorithm jwt.SigningMethod, secret interface{}) (string, error) { aal, amr := models.AAL1.String(), []models.AMREntry{} sid := "" if sessionId != nil { @@ -593,8 +593,8 @@ func generateAccessToken(tx *storage.Connection, user *models.User, sessionId *u AuthenticationMethodReference: amr, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(secret)) + token := jwt.NewWithClaims(algorithm, claims) + return token.SignedString(secret) } func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, authenticationMethod models.AuthenticationMethod, grantParams models.GrantParams) (*AccessTokenResponse, error) { @@ -624,7 +624,7 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u } // TODO(Joel): Replace when feature flag is lifted - tokenString, terr = generateAccessToken(tx, user, refreshToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + tokenString, terr = generateAccessToken(tx, user, refreshToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.GetSigningMethod(), config.JWT.GetSigningKey()) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } @@ -687,7 +687,7 @@ func (a *API) updateMFASessionAndClaims(r *http.Request, tx *storage.Connection, return err } - tokenString, terr = generateAccessToken(tx, user, &sessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + tokenString, terr = generateAccessToken(tx, user, &sessionId, 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 acecc256d4..48db509b6a 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -2,18 +2,30 @@ 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/netlify/gotrue/conf" - "github.com/netlify/gotrue/models" + "github.com/golang-jwt/jwt" + + "github.com/netlify/gotrue/storage" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type TokenTestSuite struct { @@ -299,3 +311,61 @@ func (ts *TokenTestSuite) TestTokenRefreshWithUnexpiredSession() { ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusOK, w.Code) } + +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 := context.Background() + + 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", + }, false) + if terr != nil { + return terr + } + + token, err = ts.API.issueRefreshToken(ctx, tx, user, models.PasswordGrant, models.GrantParams{}) + 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 f1a9c429fb..136f9848d4 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -10,10 +10,11 @@ import ( "testing" "time" - "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/models" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type UserTestSuite struct { @@ -48,7 +49,7 @@ func (ts *UserTestSuite) TestUserGet() { u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err, "Error finding user") var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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") @@ -114,7 +115,7 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user") var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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") @@ -175,7 +176,7 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { for _, c := range cases { ts.Run(c.desc, func() { var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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") var buffer bytes.Buffer @@ -250,7 +251,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { req.Header.Set("Content-Type", "application/json") var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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)) @@ -280,7 +281,7 @@ func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() { require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.GetSigningMethod(), ts.Config.JWT.GetSigningKey()) require.NoError(ts.T(), err) // request for reauthentication nonce diff --git a/api/verify_test.go b/api/verify_test.go index ad16ed6def..7391c505be 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -12,11 +12,12 @@ import ( "testing" "time" - "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" + + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" ) type VerifyTestSuite struct { @@ -104,7 +105,7 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { // Generate access token for request var token string - token, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err = generateAccessToken(ts.API.db, u, nil, 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/cmd/serve_cmd.go b/cmd/serve_cmd.go index 448c15abc8..674b8173ab 100644 --- a/cmd/serve_cmd.go +++ b/cmd/serve_cmd.go @@ -4,12 +4,13 @@ import ( "context" "net" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/netlify/gotrue/api" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/utilities" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) var serveCmd = cobra.Command{ diff --git a/conf/configuration.go b/conf/configuration.go index 1df70c58c2..c86501b57e 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -1,6 +1,8 @@ package conf import ( + "crypto/rsa" + "encoding/base64" "errors" "fmt" "net/url" @@ -9,6 +11,7 @@ import ( "time" "github.com/gobwas/glob" + jwt "github.com/golang-jwt/jwt" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "github.com/sirupsen/logrus" @@ -51,12 +54,14 @@ func (c *DBConfiguration) Validate() error { // 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 } // MFAConfiguration holds all the MFA related Configuration @@ -106,18 +111,19 @@ type GlobalConfiguration struct { RateLimitTokenRefresh float64 `split_words:"true" default:"30"` RateLimitSso float64 `split_words:"true" default:"30"` - 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"` - Mailer MailerConfiguration `json:"mailer"` - Sms SmsProviderConfiguration `json:"sms"` - DisableSignup bool `json:"disable_signup" split_words:"true"` - Webhook WebhookConfig `json:"webhook" split_words:"true"` - Security SecurityConfiguration `json:"security"` - MFA MFAConfiguration `json:"MFA"` - 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"` + Mailer MailerConfiguration `json:"mailer"` + 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"` + MFA MFAConfiguration `json:"MFA"` + Cookie struct { Key string `json:"key"` Domain string `json:"domain"` Duration int `json:"duration"` @@ -409,6 +415,7 @@ func (config *GlobalConfiguration) ApplyDefaults() error { if config.MFA.ChallengeExpiryDuration < defaultChallengeExpiryDuration { config.MFA.ChallengeExpiryDuration = defaultChallengeExpiryDuration } + config.JWT.InitializeSigningSecret() return nil } @@ -497,3 +504,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/example.env b/example.env index 9deae6ee0b..6cb812b89e 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 8158d2e5d8..92f939a5cd 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ require ( github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b - github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 + github.com/beevik/etree v1.1.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/coreos/go-oidc/v3 v3.0.0 github.com/didip/tollbooth/v5 v5.1.1 + github.com/fatih/color v1.10.0 // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/gobuffalo/pop/v5 v5.3.4 github.com/gobuffalo/validate/v3 v3.3.0 // indirect @@ -24,12 +25,13 @@ require ( github.com/kelseyhightower/envconfig v1.4.0 github.com/lestrrat-go/jwx v0.9.0 github.com/microcosm-cc/bluemonday v1.0.19 // 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/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 - github.com/rs/cors v1.6.0 + github.com/rs/cors v1.7.0 + github.com/russellhaering/goxmldsig v1.1.1 // indirect github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 github.com/sethvargo/go-password v0.2.0 github.com/sirupsen/logrus v1.8.1 @@ -72,13 +74,11 @@ require ( require ( github.com/aymerick/douceur v0.2.0 // indirect - github.com/beevik/etree v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.10.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -121,7 +121,6 @@ require ( github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect - github.com/russellhaering/goxmldsig v1.1.1 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect @@ -131,10 +130,10 @@ require ( go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba // indirect - golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect google.golang.org/grpc v1.46.2 // indirect diff --git a/go.sum b/go.sum index 9d628da164..e7243fff34 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,6 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 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= -github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -537,8 +535,9 @@ 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -659,8 +658,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= @@ -918,8 +918,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ 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-20201020160332-67f06af15bc9/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-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= @@ -994,8 +995,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/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-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-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/hack/test.env b/hack/test.env index 3dd1f10905..953e3edfa5 100644 --- a/hack/test.env +++ b/hack/test.env @@ -98,3 +98,4 @@ GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s" GOTRUE_SAML_ENABLED="true" GOTRUE_SAML_PRIVATE_KEY="MIIEowIBAAKCAQEAszrVveMQcSsa0Y+zN1ZFb19cRS0jn4UgIHTprW2tVBmO2PABzjY3XFCfx6vPirMAPWBYpsKmXrvm1tr0A6DZYmA8YmJd937VUQ67fa6DMyppBYTjNgGEkEhmKuszvF3MARsIKCGtZqUrmS7UG4404wYxVppnr2EYm3RGtHlkYsXu20MBqSDXP47bQP+PkJqC3BuNGk3xt5UHl2FSFpTHelkI6lBynw16B+lUT1F96SERNDaMqi/TRsZdGe5mB/29ngC/QBMpEbRBLNRir5iUevKS7Pn4aph9Qjaxx/97siktK210FJT23KjHpgcUfjoQ6BgPBTLtEeQdRyDuc/CgfwIDAQABAoIBAGYDWOEpupQPSsZ4mjMnAYJwrp4ZISuMpEqVAORbhspVeb70bLKonT4IDcmiexCg7cQBcLQKGpPVM4CbQ0RFazXZPMVq470ZDeWDEyhoCfk3bGtdxc1Zc9CDxNMs6FeQs6r1beEZug6weG5J/yRn/qYxQife3qEuDMl+lzfl2EN3HYVOSnBmdt50dxRuX26iW3nqqbMRqYn9OHuJ1LvRRfYeyVKqgC5vgt/6Tf7DAJwGe0dD7q08byHV8DBZ0pnMVU0bYpf1GTgMibgjnLjK//EVWafFHtN+RXcjzGmyJrk3+7ZyPUpzpDjO21kpzUQLrpEkkBRnmg6bwHnSrBr8avECgYEA3pq1PTCAOuLQoIm1CWR9/dhkbJQiKTJevlWV8slXQLR50P0WvI2RdFuSxlWmA4xZej8s4e7iD3MYye6SBsQHygOVGc4efvvEZV8/XTlDdyj7iLVGhnEmu2r7AFKzy8cOvXx0QcLg+zNd7vxZv/8D3Qj9Jje2LjLHKM5n/dZ3RzUCgYEAzh5Lo2anc4WN8faLGt7rPkGQF+7/18ImQE11joHWa3LzAEy7FbeOGpE/vhOv5umq5M/KlWFIRahMEQv4RusieHWI19ZLIP+JwQFxWxS+cPp3xOiGcquSAZnlyVSxZ//dlVgaZq2o2MfrxECcovRlaknl2csyf+HjFFwKlNxHm2MCgYAr//R3BdEy0oZeVRndo2lr9YvUEmu2LOihQpWDCd0fQw0ZDA2kc28eysL2RROte95r1XTvq6IvX5a0w11FzRWlDpQ4J4/LlcQ6LVt+98SoFwew+/PWuyLmxLycUbyMOOpm9eSc4wJJZNvaUzMCSkvfMtmm5jgyZYMMQ9A2Ul/9SQKBgB9mfh9mhBwVPIqgBJETZMMXOdxrjI5SBYHGSyJqpT+5Q0vIZLfqPrvNZOiQFzwWXPJ+tV4Mc/YorW3rZOdo6tdvEGnRO6DLTTEaByrY/io3/gcBZXoSqSuVRmxleqFdWWRnB56c1hwwWLqNHU+1671FhL6pNghFYVK4suP6qu4BAoGBAMk+VipXcIlD67mfGrET/xDqiWWBZtgTzTMjTpODhDY1GZck1eb4CQMP5j5V3gFJ4cSgWDJvnWg8rcz0unz/q4aeMGl1rah5WNDWj1QKWMS6vJhMHM/rqN1WHWR0ZnV83svYgtg0zDnQKlLujqW4JmGXLMU7ur6a+e6lpa1fvLsP" GOTRUE_MAX_VERIFIED_FACTORS=10 +GOTRUE_FIRST_USER_SUPER_ADMIN="true" diff --git a/mailer/template.go b/mailer/template.go index 65b5fd1253..95835ec710 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -2,10 +2,10 @@ package mailer import ( "fmt" + "net/mail" "net/url" "strings" - "github.com/badoux/checkmail" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" ) @@ -75,7 +75,8 @@ const defaultReauthenticateMail = `

Confirm reauthentication

// ValidateEmail returns nil if the email is valid, // otherwise an error indicating the reason it is invalid func (m TemplateMailer) ValidateEmail(email string) error { - return checkmail.ValidateFormat(email) + _, err := mail.ParseAddress(email) + return err } // InviteMail sends a invite mail to a new user diff --git a/mailer/template_test.go b/mailer/template_test.go new file mode 100644 index 0000000000..c30e439805 --- /dev/null +++ b/mailer/template_test.go @@ -0,0 +1,43 @@ +package mailer + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTemplateMailer_ValidateEmail(t *testing.T) { + testCases := []struct { + Email string + IsValid bool + }{ + {Email: "test@gmail.com", IsValid: true}, + {Email: " test@gmail.com", IsValid: true}, + {Email: "__test@gmail.com__", IsValid: true}, + {Email: "test@openware.com", IsValid: true}, + {Email: "test@relay.firefox.com", IsValid: true}, + {Email: "test@abc", IsValid: true}, + {Email: "test", IsValid: false}, + {Email: "a@b@c@example.com", IsValid: false}, + {Email: "abc.example.com", IsValid: false}, + {Email: "@example.com", IsValid: false}, + {Email: "~@example.com", IsValid: true}, + {Email: ".@example.com", IsValid: false}, + {Email: "te st@example.com", IsValid: false}, + {Email: "customer/department@example.com", IsValid: true}, + {Email: "$A12345@example.com", IsValid: true}, + } + + for _, tt := range testCases { + t.Run(tt.Email, func(t *testing.T) { + mailer := &TemplateMailer{} + err := mailer.ValidateEmail(tt.Email) + + if tt.IsValid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go index 4f3e6b16d4..ce6653fa6d 100644 --- a/models/refresh_token_test.go +++ b/models/refresh_token_test.go @@ -4,11 +4,12 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" ) type RefreshTokenTestSuite struct { diff --git a/models/user.go b/models/user.go index 0cbe7f04e3..87f06868eb 100644 --- a/models/user.go +++ b/models/user.go @@ -10,9 +10,10 @@ import ( "github.com/gobuffalo/pop/v5" "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" - "github.com/pkg/errors" ) // User respresents a registered user with email/password authentication @@ -378,6 +379,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 = ? and is_sso_user = false", token)