diff --git a/.env.example b/.env.example index 98c11c80..7e4aba5a 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,8 @@ # General PORT=8080 REEARTH_DB=mongodb://localhost +REEARTH_HOST=https://localhost:8080 +REEARTH_HOST_WEB=https://localhost:3000 REEARTH_DEV=false # GCP @@ -35,13 +37,13 @@ REEARTH_AUTHSRV_KEY=abcdefghijklmnopqrstuvwxyz # Available mailers: [log, smtp, sendgrid] REEARTH_MAILER=log -#SendGrid config +# SendGrid config #REEARTH_MAILER=sendgrid #REEARTH_SENDGRID_EMAIL=noreplay@test.com #REEARTH_SENDGRID_NAME= #REEARTH_SENDGRID_API= -#SMTP config +# SMTP config #REEARTH_MAILER=smtp #REEARTH_SMTP_EMAIL=noreplay@test.com #REEARTH_SMTP_HOST=smtp.sendgrid.net diff --git a/CHANGELOG.md b/CHANGELOG.md index 2367b725..c22e0f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,56 @@ # Changelog All notable changes to this project will be documented in this file. -## 0.5.0 - 2022-02-22 +## 0.6.0 - 2022-04-08 + +### 🚀 Features + +- Authentication system ([#108](https://github.com/reearth/reearth-backend/pull/108)) [`b89c32`](https://github.com/reearth/reearth-backend/commit/b89c32) +- Default mailer that outputs mails into stdout [`aab26c`](https://github.com/reearth/reearth-backend/commit/aab26c) +- Assets filtering & pagination ([#81](https://github.com/reearth/reearth-backend/pull/81)) [`739943`](https://github.com/reearth/reearth-backend/commit/739943) +- Support sign up with information provided by OIDC providers ([#130](https://github.com/reearth/reearth-backend/pull/130)) [`fef60e`](https://github.com/reearth/reearth-backend/commit/fef60e) + +### 🔧 Bug Fixes + +- Load auth client domain from config ([#124](https://github.com/reearth/reearth-backend/pull/124)) [`9bde8a`](https://github.com/reearth/reearth-backend/commit/9bde8a) +- Signup fails when password is not set [`27c2f0`](https://github.com/reearth/reearth-backend/commit/27c2f0) +- Logger panics [`d1e3a8`](https://github.com/reearth/reearth-backend/commit/d1e3a8) +- Set auth server dev mode automatically [`83a66a`](https://github.com/reearth/reearth-backend/commit/83a66a) +- Auth server bugs and auth client bugs ([#125](https://github.com/reearth/reearth-backend/pull/125)) [`ce2309`](https://github.com/reearth/reearth-backend/commit/ce2309) +- Auth0 setting is not used by JWT verification middleware [`232e75`](https://github.com/reearth/reearth-backend/commit/232e75) +- Invalid mongo queries of pagination [`7caf68`](https://github.com/reearth/reearth-backend/commit/7caf68) +- Auth config not loaded expectedly [`570fe7`](https://github.com/reearth/reearth-backend/commit/570fe7) +- Users cannot creates a new team and scene [`5df25f`](https://github.com/reearth/reearth-backend/commit/5df25f) +- Auth server certificate is not saved as pem format [`982a71`](https://github.com/reearth/reearth-backend/commit/982a71) +- Repo filters are not merged expectedly [`f4cc3f`](https://github.com/reearth/reearth-backend/commit/f4cc3f) +- Auth is no longer required for GraphQL endpoint [`58a6d1`](https://github.com/reearth/reearth-backend/commit/58a6d1) +- Rename auth srv default client ID ([#128](https://github.com/reearth/reearth-backend/pull/128)) [`89adc3`](https://github.com/reearth/reearth-backend/commit/89adc3) +- Signup API is disabled when auth server is disabled, users and auth requests in mongo cannot be deleted ([#132](https://github.com/reearth/reearth-backend/pull/132)) [`47be6a`](https://github.com/reearth/reearth-backend/commit/47be6a) +- Auth to work with zero config ([#131](https://github.com/reearth/reearth-backend/pull/131)) [`3cbb45`](https://github.com/reearth/reearth-backend/commit/3cbb45) +- Property.SchemaListMap.List test fails [`3e6dff`](https://github.com/reearth/reearth-backend/commit/3e6dff) +- Errors when auth srv domain is not specified [`10691a`](https://github.com/reearth/reearth-backend/commit/10691a) +- Errors when auth srv domain is not specified [`648073`](https://github.com/reearth/reearth-backend/commit/648073) +- Login redirect does not work [`cb6ca4`](https://github.com/reearth/reearth-backend/commit/cb6ca4) +- Enable auth srv dev mode when no domain is specified [`0c0e28`](https://github.com/reearth/reearth-backend/commit/0c0e28) +- Add a trailing slash to jwt audiences [`e96f78`](https://github.com/reearth/reearth-backend/commit/e96f78) +- Allow separate auth server ui domain [`0ce79f`](https://github.com/reearth/reearth-backend/commit/0ce79f) + +### ⚡️ Performance + +- Reduce database queries to obtain scene IDs ([#119](https://github.com/reearth/reearth-backend/pull/119)) [`784332`](https://github.com/reearth/reearth-backend/commit/784332) + +### ✨ Refactor + +- Remove filter args from repos to prevent implementation errors in the use case layer ([#122](https://github.com/reearth/reearth-backend/pull/122)) [`82cf28`](https://github.com/reearth/reearth-backend/commit/82cf28) +- Http api to export layers [`3f2582`](https://github.com/reearth/reearth-backend/commit/3f2582) + +### Miscellaneous Tasks + +- Update dependencies ([#117](https://github.com/reearth/reearth-backend/pull/117)) [`d1a38e`](https://github.com/reearth/reearth-backend/commit/d1a38e) +- Update docker-compose config [`83f9b1`](https://github.com/reearth/reearth-backend/commit/83f9b1) +- Add log for GraphQL Playground endpoint ([#133](https://github.com/reearth/reearth-backend/pull/133)) [`adeda4`](https://github.com/reearth/reearth-backend/commit/adeda4) + +## 0.5.0 - 2022-02-24 ### 🚀 Features diff --git a/internal/app/app.go b/internal/app/app.go index ccd40e16..8b007d4f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -67,6 +67,7 @@ func initEcho(ctx context.Context, cfg *ServerConfig) *echo.Echo { e.GET("/graphql", echo.WrapHandler( playground.Handler("reearth-backend", "/api/graphql"), )) + log.Infof("gql: GraphQL Playground is available") } // init usecases diff --git a/internal/app/auth_server.go b/internal/app/auth_server.go index 7d80907c..9430fa3c 100644 --- a/internal/app/auth_server.go +++ b/internal/app/auth_server.go @@ -4,11 +4,11 @@ import ( "context" "crypto/sha256" "encoding/json" + "errors" "net/http" "net/url" "os" "strconv" - "strings" "github.com/caos/oidc/pkg/op" "github.com/golang/gddo/httputil/header" @@ -17,6 +17,7 @@ import ( "github.com/reearth/reearth-backend/internal/usecase/interactor" "github.com/reearth/reearth-backend/internal/usecase/interfaces" "github.com/reearth/reearth-backend/pkg/log" + "github.com/reearth/reearth-backend/pkg/user" ) const ( @@ -29,16 +30,14 @@ const ( func authEndPoints(ctx context.Context, e *echo.Echo, r *echo.Group, cfg *ServerConfig) { userUsecase := interactor.NewUser(cfg.Repos, cfg.Gateways, cfg.Config.SignupSecret, cfg.Config.Host_Web) - d := cfg.Config.AuthSrv.Domain - if d == "" { - d = cfg.Config.Host - } - domain, err := url.Parse(d) - if err != nil { - log.Panicf("auth: not valid auth domain: %s", d) + domain := cfg.Config.AuthServeDomainURL() + if domain == nil || domain.String() == "" { + log.Panicf("auth: not valid auth domain: %s", domain) } domain.Path = "/" + uidomain := cfg.Config.AuthServeUIDomainURL() + config := &op.Config{ Issuer: domain.String(), CryptoKey: sha256.Sum256([]byte(cfg.Config.AuthSrv.Key)), @@ -95,7 +94,7 @@ func authEndPoints(ctx context.Context, e *echo.Echo, r *echo.Group, cfg *Server } // Actual login endpoint - r.POST(loginEndpoint, login(ctx, cfg, storage, userUsecase)) + r.POST(loginEndpoint, login(ctx, domain, uidomain, storage, userUsecase)) r.GET(logoutEndpoint, logout()) @@ -191,44 +190,68 @@ type loginForm struct { AuthRequestID string `json:"id" form:"id"` } -func login(ctx context.Context, cfg *ServerConfig, storage op.Storage, userUsecase interfaces.User) func(ctx echo.Context) error { +func login(ctx context.Context, url, uiurl *url.URL, storage op.Storage, userUsecase interfaces.User) func(ctx echo.Context) error { return func(ec echo.Context) error { request := new(loginForm) err := ec.Bind(request) if err != nil { log.Errorln("auth: filed to parse login request") - return ec.Redirect(http.StatusFound, redirectURL(ec.Request().Referer(), !cfg.Debug, "", "Bad request!")) + return ec.Redirect( + http.StatusFound, + redirectURL(uiurl, "/login", "", "Bad request!"), + ) } - authRequest, err := storage.AuthRequestByID(ctx, request.AuthRequestID) - if err != nil { + if _, err := storage.AuthRequestByID(ctx, request.AuthRequestID); err != nil { log.Errorf("auth: filed to parse login request: %s\n", err) - return ec.Redirect(http.StatusFound, redirectURL(ec.Request().Referer(), !cfg.Debug, "", "Bad request!")) + return ec.Redirect( + http.StatusFound, + redirectURL(uiurl, "/login", "", "Bad request!"), + ) } if len(request.Email) == 0 || len(request.Password) == 0 { log.Errorln("auth: one of credentials are not provided") - return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "Bad request!")) + return ec.Redirect( + http.StatusFound, + redirectURL(uiurl, "/login", request.AuthRequestID, "Bad request!"), + ) } // check user credentials from db - user, err := userUsecase.GetUserByCredentials(ctx, interfaces.GetUserByCredentials{ + u, err := userUsecase.GetUserByCredentials(ctx, interfaces.GetUserByCredentials{ Email: request.Email, Password: request.Password, }) + var auth *user.Auth + if err == nil { + auth = u.GetAuthByProvider(authProvider) + if auth == nil { + err = errors.New("The account is not signed up with Re:Earth") + } + } if err != nil { log.Errorf("auth: wrong credentials: %s\n", err) - return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "Login failed; Invalid user ID or password.")) + return ec.Redirect( + http.StatusFound, + redirectURL(uiurl, "/login", request.AuthRequestID, "Login failed; Invalid user ID or password."), + ) } // Complete the auth request && set the subject - err = storage.(*interactor.AuthStorage).CompleteAuthRequest(ctx, request.AuthRequestID, user.GetAuthByProvider(authProvider).Sub) + err = storage.(*interactor.AuthStorage).CompleteAuthRequest(ctx, request.AuthRequestID, auth.Sub) if err != nil { log.Errorf("auth: failed to complete the auth request: %s\n", err) - return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "Bad request!")) + return ec.Redirect( + http.StatusFound, + redirectURL(uiurl, "/login", request.AuthRequestID, "Bad request!"), + ) } - return ec.Redirect(http.StatusFound, "/authorize/callback?id="+request.AuthRequestID) + return ec.Redirect( + http.StatusFound, + redirectURL(url, "/authorize/callback", request.AuthRequestID, ""), + ) } } @@ -239,25 +262,26 @@ func logout() func(ec echo.Context) error { } } -func redirectURL(domain string, secure bool, requestID string, error string) string { - domain = strings.TrimPrefix(domain, "http://") - domain = strings.TrimPrefix(domain, "https://") - - schema := "http" - if secure { - schema = "https" +func redirectURL(u *url.URL, p string, requestID, err string) string { + v := cloneURL(u) + if p != "" { + v.Path = p } - - u := url.URL{ - Scheme: schema, - Host: domain, - Path: "login", - } - queryValues := u.Query() queryValues.Set("id", requestID) - queryValues.Set("error", error) - u.RawQuery = queryValues.Encode() + if err != "" { + queryValues.Set("error", err) + } + v.RawQuery = queryValues.Encode() + return v.String() +} - return u.String() +func cloneURL(u *url.URL) *url.URL { + return &url.URL{ + Scheme: u.Scheme, + Opaque: u.Opaque, + User: u.User, + Host: u.Host, + Path: u.Path, + } } diff --git a/internal/app/config.go b/internal/app/config.go index 354f1cc4..62d6daac 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -60,6 +60,7 @@ type AuthSrvConfig struct { Dev bool Disabled bool Domain string + UIDomain string Key string DN *AuthSrvDNConfig } @@ -75,7 +76,7 @@ func (c AuthSrvConfig) AuthConfig(debug bool, host string) *AuthConfig { } var aud []string - if debug && host != "" && c.Domain != "" { + if debug && host != "" && c.Domain != "" && c.Domain != host { aud = []string{host, c.Domain} } else { aud = []string{domain} @@ -139,19 +140,35 @@ func ReadConfig(debug bool) (*Config, error) { var c Config err := envconfig.Process(configPrefix, &c) - // defailt values + // overwrite env vars + if !c.AuthSrv.Disabled && (c.Dev || c.AuthSrv.Dev || c.AuthSrv.Domain == "") { + if _, ok := os.LookupEnv(op.OidcDevMode); !ok { + _ = os.Setenv(op.OidcDevMode, "1") + } + } + + // default values if debug { c.Dev = true } + c.Host = addHTTPScheme(c.Host) if c.Host_Web == "" { c.Host_Web = c.Host + } else { + c.Host_Web = addHTTPScheme(c.Host_Web) } - - // overwrite env vars - if !c.AuthSrv.Disabled && (c.Dev || c.AuthSrv.Dev || c.AuthSrv.Domain == "") { - if _, ok := os.LookupEnv(op.OidcDevMode); !ok { - _ = os.Setenv(op.OidcDevMode, "1") - } + if c.AuthSrv.Domain == "" { + c.AuthSrv.Domain = c.Host + } else { + c.AuthSrv.Domain = addHTTPScheme(c.AuthSrv.Domain) + } + if c.Host_Web == "" { + c.Host_Web = c.Host + } + if c.AuthSrv.UIDomain == "" { + c.AuthSrv.UIDomain = c.Host_Web + } else { + c.AuthSrv.UIDomain = addHTTPScheme(c.AuthSrv.UIDomain) } return &c, err @@ -242,3 +259,45 @@ func (ipd *AuthConfigs) Decode(value string) error { *ipd = providers return nil } + +func (c Config) HostURL() *url.URL { + u, err := url.Parse(c.Host) + if err != nil { + u = nil + } + return u +} + +func (c Config) HostWebURL() *url.URL { + u, err := url.Parse(c.Host_Web) + if err != nil { + u = nil + } + return u +} + +func (c Config) AuthServeDomainURL() *url.URL { + u, err := url.Parse(c.AuthSrv.Domain) + if err != nil { + u = nil + } + return u +} + +func (c Config) AuthServeUIDomainURL() *url.URL { + u, err := url.Parse(c.AuthSrv.UIDomain) + if err != nil { + u = nil + } + return u +} + +func addHTTPScheme(host string) string { + if host == "" { + return "" + } + if !strings.HasPrefix(host, "https://") && !strings.HasPrefix(host, "http://") { + host = "http://" + host + } + return host +} diff --git a/internal/app/config_test.go b/internal/app/config_test.go index d2405902..40b5a2fa 100644 --- a/internal/app/config_test.go +++ b/internal/app/config_test.go @@ -32,3 +32,9 @@ func TestReadConfig(t *testing.T) { assert.Equal(t, "hoge", cfg.Auth_ISS) assert.Equal(t, "foo", cfg.Auth_AUD) } + +func Test_AddHTTPScheme(t *testing.T) { + assert.Equal(t, "http://a", addHTTPScheme("a")) + assert.Equal(t, "http://a", addHTTPScheme("http://a")) + assert.Equal(t, "https://a", addHTTPScheme("https://a")) +} diff --git a/internal/app/jwt.go b/internal/app/jwt.go index 94476673..84b72c73 100644 --- a/internal/app/jwt.go +++ b/internal/app/jwt.go @@ -59,11 +59,20 @@ func NewMultiValidator(providers []AuthConfig) (MultiValidator, error) { } algorithm := validator.SignatureAlgorithm(alg) + // add a trailing slash (auth0-spa-js adds a trailing slash to audiences) + aud := append([]string{}, p.AUD...) + for i, a := range aud { + if !strings.HasSuffix(a, "/") { + a += "/" + } + aud[i] = a + } + v, err := validator.New( provider.KeyFunc, algorithm, issuerURL.String(), - p.AUD, + aud, validator.WithCustomClaims(func() validator.CustomClaims { return &customClaims{} }),