adjusted tests for new auth lib

This commit is contained in:
Umputun
2018-12-30 13:31:03 -06:00
parent acf19e73cf
commit 195e65ca60
24 changed files with 598 additions and 407 deletions
+1 -1
View File
@@ -133,8 +133,8 @@ _this is the recommended way to run remark42_
| edit-time | EDIT_TIME | `5m` | edit window |
| read-age | READONLY_AGE | | read-only age of comments, days |
| img-proxy | IMG_PROXY | `false` | enable http->https proxy for images |
| admin-passwd | ADMIN_PASSWD | | password for `admin` basic auth |
| dbg | DEBUG | `false` | debug mode |
| dev-passwd | DEV_PASSWD | | password for `dev` user |
* command line parameters are long form `--<key>=value`, i.e. `--site=https://demo.remark42.com`
* _multi_ parameters separated by `,` in the environment or repeated with command line key, like `--site=s1 --site=s2 ...`
+5 -5
View File
@@ -113,7 +113,7 @@
[[projects]]
branch = "master"
digest = "1:5ef69525e5e62fb771f3f6910c94030a86b542eff1cb9d9350803b6dae147144"
digest = "1:b117a0a0b46dad26254a48c11a511d6c697038e591a1d7ce11a229e1c8e0a237"
name = "github.com/go-pkgz/auth"
packages = [
".",
@@ -123,7 +123,7 @@
"token",
]
pruneopts = "UT"
revision = "8d5238712a320d972f9d658e2fd1d4468ef81c3e"
revision = "b95cb645615503dba4d5fced3b77d97d4f0dcc81"
[[projects]]
digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574"
@@ -145,7 +145,7 @@
version = "v1.0.0"
[[projects]]
digest = "1:71dc1e5b19e179495d2e2ca63454a9204753c5ecb3faa4a842ea5859355a968f"
digest = "1:e133aa7be09588b02198e4ddb98df5033b0319b56533881d0163ee51b903305b"
name = "github.com/go-pkgz/rest"
packages = [
".",
@@ -153,8 +153,8 @@
"logger",
]
pruneopts = "UT"
revision = "c0e09a7a640e54001aed8bad117d60ad8971958e"
version = "v1.1.1"
revision = "553c0e1b55b215f8f55da4682ac57aff9aec8b6d"
version = "v1.1.5"
[[projects]]
digest = "1:ffc060c551980d37ee9e428ef528ee2813137249ccebb0bfc412ef83071cac91"
+4 -4
View File
@@ -43,7 +43,7 @@ type ServerCommand struct {
SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"`
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"`
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"`
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
ImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"enable image proxy"`
@@ -293,8 +293,8 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
// Run all application objects
func (a *serverApp) run(ctx context.Context) error {
if a.DevPasswd != "" {
log.Printf("[WARN] running in dev mode")
if a.AdminPasswd != "" {
log.Printf("[WARN] admin basic auth enabled")
}
go func() {
@@ -537,7 +537,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto
c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID))
return c
}),
DevPasswd: s.DevPasswd,
AdminPasswd: s.AdminPasswd,
Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { // check on each auth call (in middleware)
if claims.User == nil {
return false
+5 -5
View File
@@ -42,7 +42,7 @@ func TestServerApp(t *testing.T) {
client := http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("POST", "http://localhost:18080/api/v1/comment",
strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`))
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
require.Nil(t, err)
resp, err = client.Do(req)
require.Nil(t, err)
@@ -58,7 +58,7 @@ func TestServerApp(t *testing.T) {
func TestServerApp_DevMode(t *testing.T) {
app, ctx := prepServerApp(t, 500*time.Millisecond, func(o ServerCommand) ServerCommand {
o.Port = 18085
o.DevPasswd = "password"
o.AdminPasswd = "password"
o.Auth.Dev = true
return o
})
@@ -95,7 +95,7 @@ func TestServerApp_WithMongo(t *testing.T) {
// prepare options
p := flags.NewParser(&opts, flags.Default)
_, err := p.ParseArgs([]string{"--dev-passwd=password", "--cache.type=none", "--store.type=mongo",
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none", "--store.type=mongo",
"--avatar.type=mongo", "--mongo.url=" + mongoURL, "--mongo.db=test_remark", "--port=12345", "--admin.type=mongo"})
require.Nil(t, err)
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
@@ -142,7 +142,7 @@ func TestServerApp_WithSSL(t *testing.T) {
// prepare options
p := flags.NewParser(&opts, flags.Default)
_, err := p.ParseArgs([]string{"--dev-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none",
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none",
"--ssl.type=static", "--ssl.cert=testdata/cert.pem", "--ssl.key=testdata/key.pem", "--ssl.port=18443"})
require.Nil(t, err)
@@ -318,7 +318,7 @@ func prepServerApp(t *testing.T, duration time.Duration, fn func(o ServerCommand
// prepare options
p := flags.NewParser(&cmd, flags.Default)
_, err := p.ParseArgs([]string{"--dev-passwd=password"})
_, err := p.ParseArgs([]string{"--admin-passwd=password"})
require.Nil(t, err)
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation = "/tmp", "fs", "/tmp"
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
+1 -1
View File
@@ -120,7 +120,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) {
return
}
if claims.User.Picture != "" {
if claims.User.Picture != "" && a.authenticator.AvatarProxy() != nil {
avatartStore := a.authenticator.AvatarProxy().Store
if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar")
+67 -73
View File
@@ -23,9 +23,8 @@ import (
)
func TestAdmin_Delete(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", User: store.User{ID: "id", Name: "name"},
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
@@ -39,12 +38,12 @@ func TestAdmin_Delete(t *testing.T) {
req, err := http.NewRequest(http.MethodDelete,
fmt.Sprintf("%s/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
assert.Equal(t, 200, code)
cr := store.Comment{}
err = json.Unmarshal([]byte(body), &cr)
@@ -54,9 +53,8 @@ func TestAdmin_Delete(t *testing.T) {
}
func TestAdmin_DeleteUser(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Orig: "o test test #1", User: store.User{ID: "id1", Name: "name"},
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
@@ -76,7 +74,7 @@ func TestAdmin_DeleteUser(t *testing.T) {
client := http.Client{}
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t", ts.URL, "id2"), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
@@ -108,9 +106,8 @@ func TestAdmin_DeleteUser(t *testing.T) {
}
func TestAdmin_Pin(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
@@ -125,7 +122,7 @@ func TestAdmin_Pin(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", ts.URL, id1, val), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
return resp.StatusCode
@@ -152,9 +149,8 @@ func TestAdmin_Pin(t *testing.T) {
}
func TestAdmin_Block(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -174,7 +170,7 @@ func TestAdmin_Block(t *testing.T) {
}
req, e := http.NewRequest(http.MethodPut, url, nil)
assert.Nil(t, e)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, e := client.Do(req)
require.Nil(t, e)
body, e = ioutil.ReadAll(resp.Body)
@@ -233,9 +229,8 @@ func TestAdmin_Block(t *testing.T) {
}
func TestAdmin_BlockedList(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
client := http.Client{}
@@ -243,7 +238,7 @@ func TestAdmin_BlockedList(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", 1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
_, err = client.Do(req)
require.Nil(t, err)
@@ -251,33 +246,40 @@ func TestAdmin_BlockedList(t *testing.T) {
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d&ttl=50ms", ts.URL, "user2", 1), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
_, err = client.Do(req)
require.Nil(t, err)
res, code := getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t")
require.Equal(t, 200, code, res)
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
res, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, res.StatusCode)
users := []store.BlockedUser{}
err = json.Unmarshal([]byte(res), &users)
err = json.NewDecoder(res.Body).Decode(&users)
assert.Nil(t, err)
assert.Equal(t, 2, len(users), "two users blocked")
assert.Equal(t, "user1", users[0].ID)
assert.Equal(t, "user2", users[1].ID)
time.Sleep(50 * time.Millisecond)
res, code = getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t")
require.Equal(t, 200, code, res)
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
res, err = client.Do(req)
require.Equal(t, 200, res.StatusCode)
users = []store.BlockedUser{}
err = json.Unmarshal([]byte(res), &users)
err = json.NewDecoder(res.Body).Decode(&users)
assert.Nil(t, err)
assert.Equal(t, 1, len(users), "one user left blocked")
}
func TestAdmin_ReadOnly(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -299,7 +301,7 @@ func TestAdmin_ReadOnly(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
@@ -314,7 +316,7 @@ func TestAdmin_ReadOnly(t *testing.T) {
assert.Nil(t, err, "can't marshal comment %+v", c)
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
@@ -323,7 +325,7 @@ func TestAdmin_ReadOnly(t *testing.T) {
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
require.Nil(t, err)
@@ -338,16 +340,15 @@ func TestAdmin_ReadOnly(t *testing.T) {
assert.Nil(t, err, "can't marshal comment %+v", c)
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
func TestAdmin_ReadOnlyWithAge(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"},
@@ -365,7 +366,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
@@ -377,7 +378,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) {
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Equal(t, 403, resp.StatusCode)
require.Nil(t, err)
@@ -387,9 +388,8 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) {
}
func TestAdmin_Verify(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -408,7 +408,7 @@ func TestAdmin_Verify(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
_, err = client.Do(req)
require.Nil(t, err)
verified = srv.DataService.IsVerified("radio-t", "user1")
@@ -426,7 +426,7 @@ func TestAdmin_Verify(t *testing.T) {
req, err = http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
_, err = client.Do(req)
require.Nil(t, err)
verified = srv.DataService.IsVerified("radio-t", "user1")
@@ -443,9 +443,8 @@ func TestAdmin_Verify(t *testing.T) {
}
func TestAdmin_ExportStream(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -455,7 +454,7 @@ func TestAdmin_ExportStream(t *testing.T) {
addComment(t, c1, ts)
addComment(t, c2, ts)
body, code := getWithAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream")
body, code := getWithAdminAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream")
assert.Equal(t, 200, code)
assert.Equal(t, 3, strings.Count(body, "\n"))
assert.Equal(t, 2, strings.Count(body, "\"text\""))
@@ -463,9 +462,8 @@ func TestAdmin_ExportStream(t *testing.T) {
}
func TestAdmin_ExportFile(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -478,7 +476,7 @@ func TestAdmin_ExportFile(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&mode=file", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
require.Nil(t, err)
@@ -495,9 +493,8 @@ func TestAdmin_ExportFile(t *testing.T) {
}
func TestAdmin_DeleteMeRequest(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -531,9 +528,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
},
}
_ = os.MkdirAll("/tmp/42", 0700)
defer func() { _ = os.RemoveAll("/tmp/42") }()
require.NoError(t, ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600))
require.NoError(t, os.MkdirAll("/tmp/ava-remark42/42", 0700))
require.NoError(t, ioutil.WriteFile("/tmp/ava-remark42/42/pic.image", []byte("some image data"), 0600))
tkn, err := srv.Authenticator.TokenService().Token(claims)
assert.Nil(t, err)
@@ -541,9 +537,9 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
_, err = srv.DataService.User("radio-t", "user1", 0, 0)
@@ -551,9 +547,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
}
func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -569,7 +564,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, "bad token"), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, resp.StatusCode)
@@ -596,7 +591,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "bad-password")
req.SetBasicAuth("admin", "bad-password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 401, resp.StatusCode)
@@ -608,7 +603,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, resp.StatusCode, resp.Status)
@@ -620,7 +615,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
assert.Nil(t, err)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 403, resp.StatusCode)
@@ -630,9 +625,8 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) {
}
func TestAdmin_GetUserInfo(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}}
@@ -644,7 +638,7 @@ func TestAdmin_GetUserInfo(t *testing.T) {
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL))
body, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL))
assert.Equal(t, 200, code)
u := store.User{}
err = json.Unmarshal([]byte(body), &u)
@@ -655,6 +649,6 @@ func TestAdmin_GetUserInfo(t *testing.T) {
_, code = get(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL))
assert.Equal(t, 401, code, "no auth")
_, code = getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL))
_, code = getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL))
assert.Equal(t, 400, code, "no info about user")
}
+29 -20
View File
@@ -5,7 +5,6 @@ import (
"compress/gzip"
"encoding/json"
"fmt"
"github.com/go-pkgz/auth/token"
"io"
"io/ioutil"
"mime/multipart"
@@ -16,6 +15,8 @@ import (
"testing"
"time"
"github.com/go-pkgz/auth/token"
bolt "github.com/coreos/bbolt"
"github.com/go-chi/chi"
"github.com/go-pkgz/auth"
@@ -39,7 +40,8 @@ func TestMigrator_Import(t *testing.T) {
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r)
req.SetBasicAuth("admin", "password")
assert.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
@@ -51,7 +53,7 @@ func TestMigrator_Import(t *testing.T) {
client = &http.Client{Timeout: 10 * time.Second}
req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
assert.NoError(t, err)
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
@@ -74,7 +76,8 @@ func TestMigrator_ImportForm(t *testing.T) {
contentType := bodyWriter.FormDataContentType()
require.NoError(t, bodyWriter.Close())
resp, err := http.Post(ts.URL+"/import/form?site=radio-t&provider=native&secret=123456", contentType, bodyBuf)
authts := strings.Replace(ts.URL, "http://", "http://admin:password@", 1)
resp, err := http.Post(authts+"/import/form?site=radio-t&provider=native", contentType, bodyBuf)
assert.Nil(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
@@ -84,7 +87,7 @@ func TestMigrator_ImportForm(t *testing.T) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
assert.NoError(t, err)
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
@@ -97,9 +100,10 @@ func TestMigrator_ImportFromWP(t *testing.T) {
r := strings.NewReader(strings.Replace(xmlTestWP, "'", "`", -1))
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress&secret=123456", r)
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress", r)
assert.Nil(t, err)
req.Header.Add("Content-Type", "application/xml; charset=utf-8")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
@@ -110,16 +114,15 @@ func TestMigrator_ImportFromWP(t *testing.T) {
client = &http.Client{Timeout: 10 * time.Second}
req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
assert.NoError(t, err)
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
assert.NoError(t, ds.Interface.Close())
srvAccess, tsAccess := prep(t)
require.NotNil(t, srvAccess)
defer cleanup(ts, srvAccess)
tsAccess, _, teardownAccess := startupT(t)
defer teardownAccess()
res, code := get(t, tsAccess.URL+"/api/v1/last/10?site=radio-t")
require.Equal(t, 200, code)
@@ -160,14 +163,16 @@ func TestMigrator_ImportDouble(t *testing.T) {
}
r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r)
req.SetBasicAuth("admin", "password")
assert.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
client = &http.Client{Timeout: 1 * time.Second}
req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r)
req.SetBasicAuth("admin", "password")
assert.Nil(t, err)
resp, err = client.Do(req)
assert.Nil(t, err)
@@ -187,7 +192,8 @@ func TestMigrator_ImportWaitExpired(t *testing.T) {
}
r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r)
req.SetBasicAuth("admin", "password")
require.Nil(t, err)
resp, err := client.Do(req)
assert.Nil(t, err)
@@ -195,7 +201,7 @@ func TestMigrator_ImportWaitExpired(t *testing.T) {
client = &http.Client{Timeout: 10 * time.Second}
req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t&timeout=100ms", nil)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
assert.NoError(t, err)
resp, err = client.Do(req)
assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode)
@@ -211,21 +217,23 @@ func TestMigrator_Export(t *testing.T) {
// import comments first
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, http.StatusAccepted, resp.StatusCode)
client = &http.Client{Timeout: 10 * time.Second}
req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
assert.NoError(t, err)
resp, err = client.Do(req)
assert.Equal(t, 200, resp.StatusCode)
// check file mode
req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t&secret=123456", nil)
req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
@@ -240,8 +248,9 @@ func TestMigrator_Export(t *testing.T) {
t.Logf("%s", string(ungzBody))
// check stream mode
req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t&secret=123456", nil)
req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
resp, err = client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
@@ -253,7 +262,7 @@ func TestMigrator_Export(t *testing.T) {
assert.Equal(t, 2, strings.Count(string(body), "\"text\""))
t.Logf("%s", string(body))
req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t&secret=bad", nil)
req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t", nil)
require.Nil(t, err)
resp, err = client.Do(req)
require.Nil(t, err)
@@ -275,7 +284,7 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http
}
a := auth.NewService(auth.Opts{
DevPasswd: "password",
AdminPasswd: "password",
SecretReader: token.SecretFunc(func(id string) (string, error) { return "123456", nil }),
Issuer: "test",
})
+51 -54
View File
@@ -18,9 +18,8 @@ import (
)
func TestRest_Create(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
resp, err := post(t, ts.URL+"/api/v1/comment",
`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
@@ -29,6 +28,7 @@ func TestRest_Create(t *testing.T) {
assert.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
t.Log(string(b))
c := R.JSON{}
err = json.Unmarshal(b, &c)
assert.Nil(t, err)
@@ -39,9 +39,8 @@ func TestRest_Create(t *testing.T) {
}
func TestRest_CreateOldPost(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
// make old, but not too old comment
old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5),
@@ -73,9 +72,8 @@ func TestRest_CreateOldPost(t *testing.T) {
}
func TestRest_CreateTooBig(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ")
@@ -105,9 +103,8 @@ func TestRest_CreateTooBig(t *testing.T) {
func TestRest_CreateRejected(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
body := `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`
// try to create without auth
@@ -117,9 +114,8 @@ func TestRest_CreateRejected(t *testing.T) {
}
func TestRest_CreateAndGet(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
// create comment
resp, err := post(t, ts.URL+"/api/v1/comment",
@@ -134,24 +130,31 @@ func TestRest_CreateAndGet(t *testing.T) {
id := c["id"].(string)
// get created comment by id
res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
// get created comment by id as admin
res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
comment := store.Comment{}
err = json.Unmarshal([]byte(res), &comment)
assert.Nil(t, err)
assert.Equal(t, "<p><strong>test</strong> <em>123</em></p>\n\n<p><a href=\"http://radio-t.com\" rel=\"nofollow\">http://radio-t.com</a></p>\n", comment.Text)
assert.Equal(t, "**test** *123*\n\n http://radio-t.com", comment.Orig)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Admin: true, Blocked: false,
assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false,
IP: "dbc7c999343f003f189f70aaf52cc04443f90790"},
comment.User)
t.Logf("%+v", comment)
// get created comment by id as non-admin
res, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
comment = store.Comment{}
err = json.Unmarshal([]byte(res), &comment)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: ""}, comment.User, "no ip")
}
func TestRest_Update(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -161,7 +164,7 @@ func TestRest_Update(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1",
strings.NewReader(`{"text":"updated text", "summary":"my edit"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
b, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(b.Body)
@@ -179,7 +182,7 @@ func TestRest_Update(t *testing.T) {
assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second)
// read updated comment
res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
c3 := store.Comment{}
err = json.Unmarshal([]byte(res), &c3)
@@ -188,9 +191,8 @@ func TestRest_Update(t *testing.T) {
}
func TestRest_UpdateDelete(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -200,7 +202,7 @@ func TestRest_UpdateDelete(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1",
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
b, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(b.Body)
@@ -215,7 +217,7 @@ func TestRest_UpdateDelete(t *testing.T) {
assert.True(t, c2.Deleted)
// read updated comment
res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
res, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id))
assert.Equal(t, 200, code)
c3 := store.Comment{}
err = json.Unmarshal([]byte(res), &c3)
@@ -223,13 +225,11 @@ func TestRest_UpdateDelete(t *testing.T) {
assert.Equal(t, "", c3.Text)
assert.Equal(t, "", c3.Orig)
assert.True(t, c3.Deleted)
}
func TestRest_UpdateNotOwner(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}}
@@ -240,7 +240,7 @@ func TestRest_UpdateNotOwner(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
b, err := client.Do(req)
assert.Nil(t, err)
body, err := ioutil.ReadAll(b.Body)
@@ -252,16 +252,15 @@ func TestRest_UpdateNotOwner(t *testing.T) {
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
b, err = client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 400, b.StatusCode, string(body), "update is not json")
}
func TestRest_Vote(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}}
@@ -276,7 +275,7 @@ func TestRest_Vote(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
resp, err := client.Do(req)
assert.Nil(t, err)
return resp.StatusCode
@@ -290,7 +289,7 @@ func TestRest_Vote(t *testing.T) {
err := json.Unmarshal([]byte(body), &cr)
assert.Nil(t, err)
assert.Equal(t, 1, cr.Score)
assert.Equal(t, map[string]bool{"dev": true}, cr.Votes)
assert.Equal(t, map[string]bool{"admin": true}, cr.Votes)
assert.Equal(t, 200, vote(-1), "opposite vote allowed")
body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1))
@@ -303,9 +302,8 @@ func TestRest_Vote(t *testing.T) {
}
func TestRest_UserAllData(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
// write 3 comments
user := store.User{ID: "dev", Name: "user name 1"}
@@ -325,7 +323,7 @@ func TestRest_UserAllData(t *testing.T) {
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
@@ -336,7 +334,7 @@ func TestRest_UserAllData(t *testing.T) {
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(ungzBody),
`{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`))
`{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`))
assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside")
t.Logf("%s", string(ungzBody))
@@ -347,7 +345,8 @@ func TestRest_UserAllData(t *testing.T) {
err = json.Unmarshal(ungzBody, &parsed)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true}, parsed.Info)
assert.Equal(t, store.User{Name: "developer one", ID: "dev",
Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, parsed.Info)
assert.Equal(t, 3, len(parsed.Comments))
req, err = http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
@@ -358,9 +357,8 @@ func TestRest_UserAllData(t *testing.T) {
}
func TestRest_UserAllDataManyComments(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
user := store.User{ID: "dev", Name: "user name 1"}
c := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
@@ -376,7 +374,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) {
client := &http.Client{Timeout: 1 * time.Second}
req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, 200, resp.StatusCode)
@@ -387,19 +385,18 @@ func TestRest_UserAllDataManyComments(t *testing.T) {
ungzBody, err := ioutil.ReadAll(ungzReader)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(ungzBody),
`{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`))
`{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`))
assert.Equal(t, 478, strings.Count(string(ungzBody), `"text":`), "478 comments inside")
}
func TestRest_DeleteMe(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
assert.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
+33 -48
View File
@@ -18,9 +18,8 @@ import (
)
func TestRest_Ping(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
res, code := get(t, ts.URL+"/api/v1/ping")
assert.Equal(t, "pong", res)
@@ -28,9 +27,8 @@ func TestRest_Ping(t *testing.T) {
}
func TestRest_Preview(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
assert.Nil(t, err)
@@ -41,9 +39,8 @@ func TestRest_Preview(t *testing.T) {
}
func TestRest_PreviewWithMD(t *testing.T) {
srv, ts := prep(t)
require.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
text := `
# h1
@@ -69,9 +66,8 @@ BKT
}
func TestRest_Find(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
_, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1")
assert.Equal(t, 400, code, "nothing in")
@@ -123,9 +119,8 @@ func TestRest_Find(t *testing.T) {
}
func TestRest_FindAge(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
@@ -155,9 +150,8 @@ func TestRest_FindAge(t *testing.T) {
}
func TestRest_FindReadOnly(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1),
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}}
@@ -175,7 +169,7 @@ func TestRest_FindReadOnly(t *testing.T) {
req, err := http.NewRequest(http.MethodPut,
fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil)
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
_, err = client.Do(req)
require.Nil(t, err)
@@ -197,9 +191,8 @@ func TestRest_FindReadOnly(t *testing.T) {
}
func TestRest_Last(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1", ParentID: "p1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -243,9 +236,8 @@ func TestRest_Last(t *testing.T) {
}
func TestRest_FindUserComments(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -280,22 +272,20 @@ func TestRest_FindUserComments(t *testing.T) {
}
func TestRest_UserInfo(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t")
body, code := getWithDevAuth(t, ts.URL+"/api/v1/user?site=radio-t")
assert.Equal(t, 200, code)
user := store.User{}
err := json.Unmarshal([]byte(body), &user)
assert.Nil(t, err)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true, Blocked: false, IP: ""}, user)
assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, user)
}
func TestRest_Count(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -323,9 +313,8 @@ func TestRest_Count(t *testing.T) {
}
func TestRest_Counts(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -353,9 +342,8 @@ func TestRest_Counts(t *testing.T) {
}
func TestRest_List(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
c1 := store.Comment{Text: "test test #1",
Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}}
@@ -380,9 +368,8 @@ func TestRest_List(t *testing.T) {
}
func TestRest_Config(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
body, code := get(t, ts.URL+"/api/v1/config?site=radio-t")
assert.Equal(t, 200, code)
@@ -400,9 +387,8 @@ func TestRest_Config(t *testing.T) {
}
func TestRest_Info(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only
@@ -438,9 +424,8 @@ func TestRest_Info(t *testing.T) {
}
func TestRest_Robots(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
body, code := get(t, ts.URL+"/robots.txt")
assert.Equal(t, 200, code)
+38 -18
View File
@@ -33,10 +33,11 @@ var testDb = "/tmp/test-remark.db"
var testHTML = "/tmp/test-remark.html"
var getStartedHTML = "/tmp/getstarted.html"
var devToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg"
func TestRest_FileServer(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
body, code := get(t, ts.URL+"/web/test-remark.html")
assert.Equal(t, 200, code)
@@ -44,9 +45,8 @@ func TestRest_FileServer(t *testing.T) {
}
func TestRest_GetStarted(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
err := ioutil.WriteFile(getStartedHTML, []byte("some html blah"), 0700)
assert.Nil(t, err)
@@ -175,7 +175,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) {
srv.Shutdown()
}
func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
require.Nil(t, err)
@@ -192,10 +192,9 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
srv = &Rest{
DataService: dataStore,
Authenticator: auth.NewService(auth.Opts{
DevPasswd: "password",
SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }),
AvatarStore: avatar.NewLocalFS("/tmp"),
AvatarResizeLimit: 300,
AdminPasswd: "password",
SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }),
AvatarStore: avatar.NewLocalFS("/tmp/ava-remark42"),
}),
Cache: &cache.Nop{},
WebRoot: "/tmp",
@@ -217,8 +216,18 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
err = ioutil.WriteFile(testHTML, []byte("some html"), 0700)
assert.Nil(t, err)
ts = httptest.NewServer(srv.routes())
return srv, ts
teardown = func() {
ts.Close()
srv.DataService.Close()
os.Remove(testDb)
os.Remove(testHTML)
os.RemoveAll("/tmp/ava-remark42")
}
return ts, srv, teardown
}
func get(t *testing.T, url string) (string, int) {
@@ -230,11 +239,24 @@ func get(t *testing.T, url string) (string, int) {
return string(body), r.StatusCode
}
func getWithAuth(t *testing.T, url string) (string, int) {
func getWithDevAuth(t *testing.T, url string) (body string, code int) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", url, nil)
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
r, err := client.Do(req)
require.Nil(t, err)
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
assert.Nil(t, err)
return string(b), r.StatusCode
}
func getWithAdminAuth(t *testing.T, url string) (string, int) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", url, nil)
require.Nil(t, err)
req.SetBasicAuth("admin", "password")
r, err := client.Do(req)
require.Nil(t, err)
defer r.Body.Close()
@@ -242,24 +264,22 @@ func getWithAuth(t *testing.T, url string) (string, int) {
assert.Nil(t, err)
return string(body), r.StatusCode
}
func post(t *testing.T, url string, body string) (*http.Response, error) {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("POST", url, strings.NewReader(body))
assert.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.SetBasicAuth("admin", "password")
return client.Do(req)
}
func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string {
b, err := json.Marshal(c)
require.Nil(t, err, "can't marshal comment %+v", c)
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b))
require.Nil(t, err)
req.SetBasicAuth("dev", "password")
req.Header.Add("X-JWT", devToken)
resp, err := client.Do(req)
require.Nil(t, err)
require.Equal(t, http.StatusCreated, resp.StatusCode)
+8 -12
View File
@@ -12,9 +12,8 @@ import (
)
func TestServer_RssPost(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
waitOnSecChange()
@@ -53,9 +52,8 @@ func TestServer_RssPost(t *testing.T) {
}
func TestServer_RssSite(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
waitOnSecChange()
@@ -107,9 +105,8 @@ func TestServer_RssSite(t *testing.T) {
}
func TestServer_RssWithReply(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, _, teardown := startupT(t)
defer teardown()
waitOnSecChange()
@@ -159,9 +156,8 @@ func TestServer_RssWithReply(t *testing.T) {
}
func TestServer_RssReplies(t *testing.T) {
srv, ts := prep(t)
assert.NotNil(t, srv)
defer cleanup(ts, srv)
ts, srv, teardown := startupT(t)
defer teardown()
waitOnSecChange()
+1 -1
View File
@@ -19,5 +19,5 @@ script:
- GO111MODULE=on go get ./...
- GO111MODULE=on go mod vendor
- GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
- ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1;
- ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --exclude=_example --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1;
- $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci
+125 -22
View File
@@ -1,17 +1,20 @@
# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master)
# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/auth?status.svg)](https://godoc.org/github.com/go-pkgz/auth)
This library provides "social login" with Github, Google, Facebook and Yandex.
- Multiple oauth2 providers can be used at the same time
- Special `dev` provider allows local testing and development
- JWT stored in a secure cookie and with XSRF protection. Cookies can be session-only
- JWT stored in a secure cookie with XSRF protection. Cookies can be session-only
- Minimal scopes with user name, id and picture (avatar) only
- Integrated avatar proxy with FS, boltdb or gridfs storage
- Support of user-defined storages
- Integrated avatar proxy with FS, boltdb and gridfs storages
- Support of user-defined storages for avatars
- Black list with user-defined validator
- Multiple aud (audience) supported
- Secure key with customizable `SecretReader`
- Ability to store extra information to token and retrieve on login
- Pre-auth and post-auth hooks to handle custom use cases.
- Middleware for easy integration into http routers
## Install
@@ -23,25 +26,26 @@ This library provides "social login" with Github, Google, Facebook and Yandex.
Example with chi router:
```go
func main() {
/// define options
options := auth.Opts{
SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), // secret key for JWT
SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT
return "secret", nil
}),
TokenDuration: time.Hour,
CookieDuration: time.Hour * 24,
Issuer: "my-test-app",
URL: "http://127.0.0.1:8080",
AvatarStore: avatar.NewLocalFS("/tmp", 120),
Validator: middleware.ValidatorFunc(func(_ string, claims token.Claims) bool {
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") // allow only dev_ names
}),
AvatarStore: avatar.NewLocalFS("/tmp"),
Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool {
// allow only dev_* names
return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_")
}),
}
// create auth service
service, err := auth.NewService(options)
if err != nil {
log.Fatal(err)
}
// create auth service with providers
service := auth.NewService(options)
service.AddProvider("github", "<Client ID>", "<Client Secret>") // add github provider
service.AddProvider("facebook", "<Client ID>", "<Client Secret>") // add facebook provider
@@ -69,7 +73,104 @@ func main() {
- `middleware.Auth` - requires authenticated user
- `middleware.Admin` - requires authenticated and admin user
- `middleware.Trace` - doesn't require authenticated user, but adds user info to request
## Details
Generally, adding support of `auth` includes a few relatively simple steps:
1. Setup `auth.Opts` structure with all parameters. Each of them [documented](https://github.com/go-pkgz/auth/blob/master/auth.go#L29) and most of parameters are optional and have sane defaults.
2. [Create](https://github.com/go-pkgz/auth/blob/master/auth.go#L56) the new `auth.Service` with provided options.
3. [Add all](https://github.com/go-pkgz/auth/blob/master/auth.go#L149) desirable authentication providers. Currently supported Github, Google, Facebook and Yandex
4. Retrieve [middleware](https://github.com/go-pkgz/auth/blob/master/auth.go#L144) and [http handlers](https://github.com/go-pkgz/auth/blob/master/auth.go#L105) from `auth.Service`
5. Wire auth and avatar handlers into http router as subroutes.
### API
For the example above authentication handlers wired as `/auth` and provides:
- `/auth/<provider>/login?id=<site_id>&from=<redirect_url>` - site_id used as `aud` claim for the token and can be processed by `SecretReader` to load/retrieve/define different secrets. redirect_url is the url to redirect after successful login.
- `/avatar/<avatar_id>` - returns the avatar (image). Links to those pictures added into user info automatically, for details see "Avatar proxy"
- `/auth/<provider>/logout` and `/auth/logout` - invalidate "session" by removing JWT cookie
- `/auth/list` - gives a json list of active providers
- `/auth/user` - returns `token.User` (json)
### User info
Middleware populates `token.User` to request's context. It can be loaded with `token.GetUserInfo(r *http.Request) (user User, err error)` or `token.MustGetUserInfo(r *http.Request) User` functions.
`token.User` object includes all fields retrieved from oauth2 provider:
- `Name` - user name
- `ID` - hash of user id
- `Picture` - full link to proxied avatar (see "Avatar proxy")
It also has placeholders for fields application can populate with custom `token.ClaimsUpdater` (see "Customization")
- `IP` - hash of user's IP address
- `Email` - user's email
- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details.
### Avatar proxy
Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy
- On each login the proxy will retrieve user's picture and save it to `AvatarStore`
- Local (proxied) link to avatar included in user's info (jwt token)
- API for avatar removal provided as a part of `AvatarStore`
- User can leverage one of provided stores:
- `avatar.LocalFS` - file system, each avatar in a separate file
- `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store).
- `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db).
- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25).
- All avatar-related setup done as a part of `auth.Opts` and needs:
- `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")`
- `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info")
- `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing.
### Customization
There are several ways to adjust functionality of the library:
1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`.
### Implementing black list logic or some other filters
Restricting some users or some tokens is two step process:
- `ClaimsUpdater` sets an attribute, like `blocked` (or `allowed`)
- `Validator` checks the attribute and returns true/false
_This technic used in the [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go#L36) code_
The process can be simplified by doing all checks directly in `Validator`, but depends on particular case such solution
can be too expensive because `Validator` runs on each request as a part of auth middleware. In contrast, `ClaimsUpdater` called on token creation/refresh only.
### Dev provider
Working with oauth2 providers can be a pain, especially during development phase. A special, development-only provider `dev` can make it less painful. This one can be registered directly, i.e. `service.AddProvider("dev", "", "")` and should be activated like this:
```go
// runs dev oauth2 server on :8084
go func() {
p, err := service.Provider("dev")
if err != nil {
log.Fatal(err)
}
devAuthServer := provider.DevAuthServer{Provider: p}
devAuthServer.Run()
}()
```
It will run fake aouth2 "server" on port :8084 and user could login with any user name. See [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go) for more details.
_Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use `dev` provider with any production code._
## Register oauth2 providers
Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured.
@@ -77,11 +178,11 @@ Authentication handled by external providers. You should setup oauth2 for all (o
#### Google Auth Provider
1. Create a new project: https://console.developers.google.com/project
1. Choose the new project from the top right project dropdown (only if another project is selected)
1. In the project Dashboard center pane, choose **"API Manager"**
1. In the left Nav pane, choose **"Credentials"**
1. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
1. In the center pane, choose **"Credentials"** tab.
2. Choose the new project from the top right project dropdown (only if another project is selected)
3. In the project Dashboard center pane, choose **"API Manager"**
4. In the left Nav pane, choose **"Credentials"**
5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save.
6. In the center pane, choose **"Credentials"** tab.
* Open the **"New credentials"** drop down
* Choose **"OAuth client ID"**
* Choose **"Web application"**
@@ -89,7 +190,7 @@ Authentication handled by external providers. You should setup oauth2 for all (o
* Authorized origins is your domain ex: `https://example.mysite.com`
* Authorized redirect URIs is the location of oauth2/callback constructed as domain + `/auth/google/callback`, ex: `https://example.mysite.com/auth/google/callback`
* Choose **"Create"**
2. Take note of the **Client ID** and **Client Secret**
7. Take note of the **Client ID** and **Client Secret**
_instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_
@@ -125,4 +226,6 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co
## Status
The library extracted from [remark42](https://github.com/umputun/remark) project. The code in production use on multiple sites and seems to work fine.
The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine.
`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible.
+29 -13
View File
@@ -27,7 +27,7 @@ type Service struct {
// Opts is a full set of all parameters to initialize Service
type Opts struct {
SecretReader token.Secret // reader returns secret for given site id (aud)
SecretReader token.Secret // reader returns secret for given site id (aud), required
ClaimsUpd token.ClaimsUpdater // updater for jwt to add/modify values stored in the token
SecureCookies bool // makes jwt cookie secure
TokenDuration time.Duration // token's TTL, refreshed automatically
@@ -42,14 +42,14 @@ type Opts struct {
Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth"
URL string // root url for the rest service, i.e. http://blah.example.com
URL string // root url for the rest service, i.e. http://blah.example.com, required
Validator token.Validator // validator allows to reject some valid tokens with user-defined logic
AvatarStore avatar.Store // store to save/load avatars
AvatarStore avatar.Store // store to save/load avatars, required
AvatarResizeLimit int // resize avatar's limit in pixels
AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar"
AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar`
DevPasswd string // if presented, allows basic auth with user dev and given password
AdminPasswd string // if presented, allows basic auth with user admin and given password
}
// NewService initializes everything
@@ -71,7 +71,7 @@ func NewService(opts Opts) *Service {
if opts.SecretReader == nil {
jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) {
return "", errors.New("secrets reader not avalibale")
return "", errors.New("secrets reader not available")
})
}
@@ -79,9 +79,9 @@ func NewService(opts Opts) *Service {
opts: opts,
jwtService: jwtService,
authMiddleware: middleware.Authenticator{
JWTService: jwtService,
Validator: opts.Validator,
DevPasswd: opts.DevPasswd,
JWTService: jwtService,
Validator: opts.Validator,
AdminPasswd: opts.AdminPasswd,
},
}
@@ -96,6 +96,9 @@ func NewService(opts Opts) *Service {
RoutePath: opts.AvatarRoutePath,
ResizeLimit: opts.AvatarResizeLimit,
}
if res.avatarProxy.RoutePath == "" {
res.avatarProxy.RoutePath = "/avatar"
}
}
return &res
@@ -104,7 +107,7 @@ func NewService(opts Opts) *Service {
// Handlers gets http.Handler for all providers and avatars
func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handler) {
providerHandler := func(w http.ResponseWriter, r *http.Request) {
ah := func(w http.ResponseWriter, r *http.Request) {
elems := strings.Split(r.URL.Path, "/")
if len(elems) < 2 {
w.WriteHeader(http.StatusBadRequest)
@@ -127,6 +130,19 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl
return
}
// show user info
if elems[len(elems)-1] == "user" {
claims, _, err := s.jwtService.Get(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
rest.RenderJSON(w, r, rest.JSON{"error": err.Error()})
return
}
rest.RenderJSON(w, r, claims.User)
return
}
// regular auth handlers
provName := elems[len(elems)-2]
p, err := s.Provider(provName)
if err != nil {
@@ -137,10 +153,10 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl
p.Handler(w, r)
}
return http.HandlerFunc(providerHandler), http.HandlerFunc(s.avatarProxy.Handler)
return http.HandlerFunc(ah), http.HandlerFunc(s.avatarProxy.Handler)
}
// Middleware returns token middleware
// Middleware returns auth middleware
func (s *Service) Middleware() middleware.Authenticator {
return s.authMiddleware
}
@@ -152,7 +168,7 @@ func (s *Service) AddProvider(name string, cid string, csecret string) {
URL: s.opts.URL,
JwtService: s.jwtService,
Issuer: s.issuer,
AvatarProxy: s.avatarProxy,
AvatarSaver: s.avatarProxy,
Cid: cid,
Csecret: csecret,
}
+1 -9
View File
@@ -2,22 +2,14 @@ module github.com/go-pkgz/auth
require (
cloud.google.com/go v0.34.0 // indirect
github.com/boltdb/bolt v1.3.1 // indirect
github.com/coreos/bbolt v1.3.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
github.com/go-errors/errors v1.0.1
github.com/go-pkgz/mongo v1.0.0
github.com/go-pkgz/rest v1.1.1
github.com/kr/pretty v0.1.0 // indirect
github.com/go-pkgz/rest v1.1.5
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022
github.com/pkg/errors v0.8.0
github.com/stretchr/testify v1.2.2
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 // indirect
google.golang.org/appengine v1.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
+6 -23
View File
@@ -1,7 +1,5 @@
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI=
github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,20 +8,15 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-pkgz/mongo v1.0.0 h1:9jijAK7prCRMetiyTu3c1rv/2lMypzuf2DWcVpTlwzw=
github.com/go-pkgz/mongo v1.0.0/go.mod h1:R9si/F2aJsjz4MUxhzuppIHY8yLV3YCeuCpgcI50cu4=
github.com/go-pkgz/rest v1.1.1 h1:YuLe+wOJwcE+Y0SkJ+AtvUOPGjTMe4Q4vg98Uqs9CKc=
github.com/go-pkgz/rest v1.1.1/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/go-pkgz/rest v1.1.3 h1:rMf+xJn8i1Ip9OKohusZsRxwntM0BwYu8OX8BuEwN80=
github.com/go-pkgz/rest v1.1.3/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
github.com/go-pkgz/rest v1.1.4 h1:/Lrg9kBWBjNah7nmCDHLszRAfVVBIy5ajf0vVgpHPi0=
github.com/go-pkgz/rest v1.1.4/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
github.com/go-pkgz/rest v1.1.5 h1:5br4mnscfLb27yxv5hJFLBVmAt09PrmIBP+meA3CfHc=
github.com/go-pkgz/rest v1.1.5/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
@@ -34,17 +27,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 h1:IcgEB62HYgAhX0Nd/QrVgZlxlcyxbGQHElLUhW2X4Fo=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+23 -54
View File
@@ -13,20 +13,12 @@ import (
"github.com/go-pkgz/auth/token"
)
// Authenticator is top level token object providing middlewares
// Authenticator is top level auth object providing middlewares
type Authenticator struct {
JWTService *token.Service
Providers []provider.Service
Validator token.Validator
DevPasswd string
}
var devUser = token.User{
ID: "dev",
Name: "developer one",
Attributes: map[string]interface{}{
"admin": true,
},
JWTService *token.Service
Providers []provider.Service
Validator token.Validator
AdminPasswd string
}
var adminUser = token.User{
@@ -37,7 +29,7 @@ var adminUser = token.User{
},
}
// Auth middleware adds token from session and populates user info
// Auth middleware adds auth from session and populates user info
func (a *Authenticator) Auth(next http.Handler) http.Handler {
return a.auth(true)(next)
}
@@ -47,6 +39,7 @@ func (a *Authenticator) Trace(next http.Handler) http.Handler {
return a.auth(false)(next)
}
// auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false)
func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler {
onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) {
@@ -57,27 +50,20 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler {
h.ServeHTTP(w, r)
return
}
log.Printf("[DEBUG] failed token, %s", err)
log.Printf("[DEBUG] auth failed, %s", err)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
f := func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// if secret key matches for given site (from request) return admin user
if a.checkSecretKey(r) {
// use admin user basic auth if enabled
if a.basicAdminUser(r) {
r = token.SetUserInfo(r, adminUser)
h.ServeHTTP(w, r)
return
}
// use dev user basic token if enabled
if a.basicDevUser(r) {
r = token.SetUserInfo(r, devUser)
h.ServeHTTP(w, r)
return
}
claims, tkn, err := a.JWTService.Get(r)
if err != nil {
onError(h, w, r, errors.Wrap(err, "can't get token"))
@@ -85,12 +71,12 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler {
}
if claims.Handshake != nil { // handshake in token indicate special use cases, not for login
onError(h, w, r, errors.Errorf("invalid kind of token for %s/%s", claims.User.Name, claims.User.ID))
onError(h, w, r, errors.New("invalid kind of token"))
return
}
if claims.User == nil {
onError(h, w, r, errors.New("failed token, no user info presented in the claim"))
onError(h, w, r, errors.New("failed auth, no user info presented in the claim"))
return
}
@@ -121,29 +107,11 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler {
return f
}
func (a *Authenticator) checkSecretKey(r *http.Request) bool {
if a.JWTService.SecretReader == nil {
return false
}
aud := r.URL.Query().Get("aud")
secret := r.URL.Query().Get("secret")
skey, err := a.JWTService.SecretReader.Get(aud)
if err != nil {
return false
}
if strings.TrimSpace(secret) == "" || secret != skey {
return false
}
return true
}
// refreshExpiredToken makes new token with passed claims, but only if permission allowed
// refreshExpiredToken makes a new token with passed claims
func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token.Claims) (token.Claims, error) {
// refresh token
if err := a.JWTService.Set(w, claims, false); err != nil {
claims.ExpiresAt = 0 // this will cause now+duration for refreshed token
if err := a.JWTService.Set(w, claims); err != nil {
return token.Claims{}, err
}
return claims, nil
@@ -168,9 +136,10 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(fn)
}
func (a *Authenticator) basicDevUser(r *http.Request) bool {
// basic auth for admin user
func (a *Authenticator) basicAdminUser(r *http.Request) bool {
if a.DevPasswd == "" {
if a.AdminPasswd == "" {
return false
}
@@ -181,18 +150,18 @@ func (a *Authenticator) basicDevUser(r *http.Request) bool {
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
log.Printf("[WARN] dev user token failed, failed to decode %s, %s", s[1], err)
log.Printf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err)
return false
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
log.Printf("[WARN] dev user token failed, failed to split %s", string(b))
log.Printf("[WARN] admin user auth failed, can't split basic auth %s", string(b))
return false
}
if pair[0] != "dev" || pair[1] != a.DevPasswd {
log.Printf("[WARN] dev user token failed, user/passwd mismatch %+v", pair)
if pair[0] != "admin" || pair[1] != a.AdminPasswd {
log.Printf("[WARN] dev user auth failed, user/passwd mismatch %+v", pair)
return false
}
+124 -17
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"strings"
"sync"
"text/template"
"time"
"github.com/nullrocks/identicon"
@@ -46,6 +47,12 @@ func (d *DevAuthServer) Run() {
log.Printf("[WARN] can't create identicon, %s", err)
}
userFormTmpl, err := template.New("page").Parse(devUserFormTmpl)
if err != nil {
log.Printf("[WARN] can't parse user form template, %s", err)
return
}
d.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", devAuthPort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -56,7 +63,10 @@ func (d *DevAuthServer) Run() {
// first time it will be called without username and will ask for one
if !d.Automatic && (r.ParseForm() != nil || r.Form.Get("username") == "") {
if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil {
formData := struct{ Query string }{Query: r.URL.RawQuery}
if err = userFormTmpl.Execute(w, formData); err != nil {
log.Printf("[WARN] can't write, %s", err)
}
return
@@ -176,24 +186,121 @@ func (d *DevAuthServer) genAvatar(user string) ([]byte, error) {
return buf.Bytes(), err
}
var devUserForm = `
var devUserFormTmpl = `
<html>
<head>
<title>Dev User</title>
<style>
form {
margin: 100 auto;
width: 300px;
padding: 1em;
border: 1px solid #CCC;
}
</style>
</head>
<head>
<title>Dev OAuth</title>
<style>
body {
text-align: center;
}
a {
color: hsl(200, 50%, 50%);
text-decoration-color: hsla(200, 50%, 50%, 0.5);
}
a:hover {
color: hsl(200, 50%, 70%);
text-decoration-color: hsla(200, 50%, 70%, 0.5);
}
form {
font-family: Helvetica, Arial, sans-serif;
margin: 100px auto;
display: inline-block;
padding: 1em;
box-shadow: 0 0 0.1rem rgba(0, 0, 0, 0.2), 0 0 0.4rem rgba(0, 0, 0, 0.1);
}
.form-header {
text-align: center;
}
.form-header h1 {
margin: 0;
}
.form-header h1 a:not(:hover) {
text-decoration: none;
}
.form-header p {
opacity: 0.6;
margin-top: 0;
margin-bottom: 2rem;
}
.username-label {
opacity: 0.6;
font-size: 0.8em;
}
.username-input {
font-size: inherit;
margin: 0;
width: 100%;
text-align: inherit;
}
.form-submit {
border: none;
background: hsl(200, 50%, 50%);
color: white;
font: inherit;
padding: 0.4em 0.8em 0.3em 0.8em;
border-radius: 0.2em;
width: 100%;
}
.form-submit:hover,
.form-submit:focus {
background-color: hsl(200, 50%, 70%);
}
.form-submit:active {
background-color: hsl(200, 80%, 70%);
}
.username-label,
.username-input,
.form-submit {
display: block;
margin-bottom: 0.4rem;
}
.notice {
margin: 0;
margin-top: 2rem;
font-size: 0.8em;
opacity: 0.6;
}
</style>
</head>
<body>
<form action="/login/oauth/authorize?%s" method="post">
username: <input type="text" name="username" value="dev_user">
<input type="submit" value="Login">
<form action="/login/oauth/authorize?{{.Query}}" method="post">
<header class="form-header">
<h1><a href="https://github.com/go-pkgz/auth">GO-PKGZ/AUTH</a></h1>
<p>Dev Provider</p>
</header>
<label>
<span class="username-label">Username</span>
<input
class="username-input"
type="text"
name="username"
value="dev_user"
autofocus
/>
</label>
<input type="submit" class="form-submit" value="Authorize" />
<p class="notice">Not for production use</p>
</form>
</body>
</body>
<script>
var input = document.querySelector(".username-input");
input.focus();
input.setSelectionRange(0, input.value.length)
</script>
</html>
`
+5 -5
View File
@@ -18,8 +18,8 @@ func NewGoogle(p Params) Service {
return initService(p, Service{
Name: "google",
Endpoint: google.Endpoint,
RedirectURL: p.URL + "/token/google/callback",
Scopes: []string{"https://www.googleapis.com/token/userinfo.profile"},
RedirectURL: p.URL + "/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"},
InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
MapUser: func(data userData, _ []byte) token.User {
userInfo := token.User{
@@ -41,7 +41,7 @@ func NewGithub(p Params) Service {
return initService(p, Service{
Name: "github",
Endpoint: github.Endpoint,
RedirectURL: p.URL + "/token/github/callback",
RedirectURL: p.URL + "/auth/github/callback",
Scopes: []string{},
InfoURL: "https://api.github.com/user",
MapUser: func(data userData, _ []byte) token.User {
@@ -76,7 +76,7 @@ func NewFacebook(p Params) Service {
return initService(p, Service{
Name: "facebook",
Endpoint: facebook.Endpoint,
RedirectURL: p.URL + "/token/facebook/callback",
RedirectURL: p.URL + "/auth/facebook/callback",
Scopes: []string{"public_profile"},
InfoURL: "https://graph.facebook.com/me?fields=id,name,picture",
MapUser: func(data userData, bdata []byte) token.User {
@@ -102,7 +102,7 @@ func NewYandex(p Params) Service {
return initService(p, Service{
Name: "yandex",
Endpoint: yandex.Endpoint,
RedirectURL: p.URL + "/token/yandex/callback",
RedirectURL: p.URL + "/auth/yandex/callback",
Scopes: []string{},
// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
InfoURL: "https://login.yandex.ru/info?format=json",
+21 -13
View File
@@ -17,7 +17,6 @@ import (
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/token"
)
@@ -37,12 +36,17 @@ type Service struct {
type Params struct {
URL string
JwtService *token.Service
AvatarProxy *avatar.Proxy
AvatarSaver AvatarSaver
Cid string
Csecret string
Issuer string
}
// AvatarSaver defines minimal interface to save avatar
type AvatarSaver interface {
Put(u token.User) (avatarURL string, err error)
}
type userData map[string]interface{}
func (u userData) value(key string) string {
@@ -53,9 +57,9 @@ func (u userData) value(key string) string {
return ""
}
// initService makes token service for given provider
// initService makes oauth2 service for given provider
func initService(p Params, service Service) Service {
log.Printf("[INFO] init token service %s", service.Name)
log.Printf("[INFO] init oauth2 service %s", service.Name)
service.Params = p
service.conf = oauth2.Config{
ClientID: service.Cid,
@@ -65,7 +69,7 @@ func initService(p Params, service Service) Service {
Endpoint: service.Endpoint,
}
log.Printf("[DEBUG] created %s token, id=%s, redir=%s, endpoint=%s",
log.Printf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s",
service.Name, service.Cid, service.Endpoint, service.RedirectURL)
return service
}
@@ -123,7 +127,7 @@ func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) {
},
}
if err := p.JwtService.Set(w, claims, false); err != nil {
if err := p.JwtService.Set(w, claims); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
return
}
@@ -144,9 +148,14 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) {
return
}
if oauthClaims.Handshake == nil {
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token")
return
}
retrievedState := oauthClaims.Handshake.State
if retrievedState == "" || retrievedState != r.URL.Query().Get("state") {
http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized)
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state")
return
}
@@ -160,7 +169,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) {
client := p.conf.Client(context.Background(), tok)
uinfo, err := client.Get(p.InfoURL)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, fmt.Sprintf("failed to get client info via %s", p.InfoURL))
rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info")
return
}
@@ -201,8 +210,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) {
SessionOnly: oauthClaims.SessionOnly,
}
if err = p.JwtService.Set(w, claims, oauthClaims.SessionOnly); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info")
if err = p.JwtService.Set(w, claims); err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
return
}
@@ -218,8 +227,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) {
// setAvatar saves avatar and puts proxied URL to u.Picture
func (p Service) setAvatar(u token.User) token.User {
if p.AvatarProxy != nil {
if avatarURL, e := p.AvatarProxy.Put(u); e == nil {
if p.AvatarSaver != nil {
if avatarURL, e := p.AvatarSaver.Put(u); e == nil {
u.Picture = avatarURL
} else {
log.Printf("[WARN] failed to set avatar for %+v, %+v", u, e)
@@ -231,7 +240,6 @@ func (p Service) setAvatar(u token.User) token.User {
// LogoutHandler - GET /logout
func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) {
p.JwtService.Reset(w)
log.Printf("[DEBUG] logout")
}
func (p Service) randToken() (string, error) {
+2 -2
View File
@@ -154,7 +154,7 @@ func (j *Service) Parse(tokenString string) (Claims, error) {
// Set creates token cookie with xsrf cookie and put it to ResponseWriter
// accepts claims and sets expiration if none defined. permanent flag means long-living cookie,
// false makes it session only.
func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) error {
func (j *Service) Set(w http.ResponseWriter, claims Claims) error {
if claims.ExpiresAt == 0 {
claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix()
}
@@ -167,7 +167,7 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) er
}
cookieExpiration := 0 // session cookie
if !sessionOnly {
if !claims.SessionOnly && claims.Handshake == nil {
cookieExpiration = int(j.CookieDuration.Seconds())
}
+2 -1
View File
@@ -1,4 +1,5 @@
## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master)
## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/rest?status.svg)](https://godoc.org/github.com/go-pkgz/rest)
## Install and update
+10 -6
View File
@@ -1,6 +1,7 @@
package rest
import (
"errors"
"fmt"
"log"
"net/http"
@@ -9,14 +10,14 @@ import (
"strings"
)
// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code
func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) {
log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details))
// SendErrorJSON sends {error: msg} with error code and logging error and caller
func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, msg string) {
log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, msg))
w.WriteHeader(code)
RenderJSON(w, r, map[string]interface{}{"error": err.Error(), "details": details})
RenderJSON(w, r, JSON{"error": msg})
}
func errDetailsMsg(r *http.Request, code int, err error, details string) string {
func errDetailsMsg(r *http.Request, code int, err error, msg string) string {
q := r.URL.String()
if qun, e := url.QueryUnescape(q); e == nil {
@@ -35,5 +36,8 @@ func errDetailsMsg(r *http.Request, code int, err error, details string) string
if pos := strings.Index(remoteIP, ":"); pos >= 0 {
remoteIP = remoteIP[:pos]
}
return fmt.Sprintf("%s - %v - %d - %s - %s%s", details, err, code, remoteIP, q, srcFileInfo)
if err == nil {
err = errors.New("no error")
}
return fmt.Sprintf("%s - %v - %d - %s - %s%s", msg, err, code, remoteIP, q, srcFileInfo)
}
+7
View File
@@ -36,6 +36,13 @@ const (
None
)
// Logger returns default logger middleware
func Logger(next http.Handler) http.Handler {
l := New(Flags(All), Prefix("[REST]"))
return l.Handler(next)
}
// New makes rest Logger with given options
func New(options ...Option) *Middleware {
res := Middleware{