diff --git a/README.md b/README.md index 617c3927..9d08c92c 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ _this is the recommended way to run remark42_ | auth.yandex.cid | AUTH_YANDEX_CID | | Yandex OAuth client ID | | auth.yandex.csec | AUTH_YANDEX_CSEC | | Yandex OAuth client secret | | auth.dev | AUTH_DEV | `false` | local oauth2 server, development mode only | +| auth.anon | AUTH_ANON | `false` | enable anonymous login | | notify.type | NOTIFY_TYPE | none | type of notification (none or telegram) | | notify.queue | NOTIFY_QUEUE | `100` | size of notification queue | | notify.telegram.token | NOTIFY_TELEGRAM_TOKEN | | telegram token | @@ -475,6 +476,11 @@ type User struct { _currently supported providers are `google`, `facebook`, `github` and `yandex`_ +Optionally, anonymous access can be turned on. In this case and extra `anonymous` provider will allow logins without any social login with any name satisfying 2 conditions: + +- name should be at least 3 characters long +- name has to start from the letter and contains letters, numbers, underscores and spaces only. + ### Commenting * `POST /api/v1/comment` - add a comment. _auth required_ diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 80325d6e..6579398a 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path" + "regexp" "strings" "syscall" "time" @@ -66,11 +67,12 @@ type ServerCommand struct { JWT time.Duration `long:"jwt" env:"JWT" default:"5m" description:"jwt TTL"` Cookie time.Duration `long:"cookie" env:"COOKIE" default:"200h" description:"auth cookie TTL"` } `group:"ttl" namespace:"ttl" env-namespace:"TTL"` - Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` - Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` - Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` - Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"` - Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"` + Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"` + Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"` + Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"` + Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"` + Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"` + Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"` } `group:"auth" namespace:"auth" env-namespace:"AUTH"` CommonOpts @@ -477,6 +479,24 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) { providers++ } + if s.Auth.Anonymous { + log.Print("[INFO] anonymous access enabled") + var isValidAnonName = regexp.MustCompile(`^[a-zA-Z][\w ]+$`).MatchString + authenticator.AddDirectProvider("anonymous", provider.CredCheckerFunc(func(user, _ string) (ok bool, err error) { + user = strings.TrimSpace(user) + if len(user) < 3 { + log.Printf("[WARN] name %q is too short, should be at least 3 characters", user) + return false, nil + } + + if !isValidAnonName(user) { + log.Printf("[WARN] name %q should have letters, digits, underscores and spaces only", user) + return false, nil + } + return true, nil + })) + } + if providers == 0 { log.Printf("[WARN] no auth providers defined") } diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 4140693b..9e0f2309 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -83,6 +83,48 @@ func TestServerApp_DevMode(t *testing.T) { app.Wait() } +func TestServerApp_AnonMode(t *testing.T) { + app, ctx := prepServerApp(t, 500*time.Millisecond, func(o ServerCommand) ServerCommand { + o.Port = 18085 + o.Auth.Anonymous = true + return o + }) + + go func() { _ = app.run(ctx) }() + time.Sleep(100 * time.Millisecond) // let server start + + assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers()), "extra auth provider for anon") + assert.Equal(t, "anonymous", app.restSrv.Authenticator.Providers()[4].Name(), "anon auth provider") + + // send ping + resp, err := http.Get("http://localhost:18085/api/v1/ping") + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 200, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + assert.Equal(t, "pong", string(body)) + + // try to login with good name + resp, err = http.Get("http://localhost:18085/auth/anonymous/login?user=blah123&aud=remark42") + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 200, resp.StatusCode) + + // try to login with bad name + resp, err = http.Get("http://localhost:18085/auth/anonymous/login?user=**blah123&aud=remark42") + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 403, resp.StatusCode) + + // try to login with short name + resp, err = http.Get(`http://localhost:18085/auth/anonymous/login?user=bl%20%20&aud=remark42`) + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 403, resp.StatusCode) + + app.Wait() +} func TestServerApp_WithMongo(t *testing.T) { mongoURL := os.Getenv("MONGO_TEST")