diff --git a/.travis.yml b/.travis.yml index 9fa5315..03f5672 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,14 @@ services: - mongodb go: - - "1.11.x" + - "1.12.x" install: true before_install: - export TZ=America/Chicago - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.13.2 + - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.17.1 + - golangci-lint --version - go get github.com/mattn/goveralls - export MONGO_TEST=mongodb://127.0.0.1:27017 - export PATH=$(pwd)/bin:$PATH diff --git a/README.md b/README.md index 2109af1..c76bf5d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ This library provides "social login" with Github, Google, Facebook and Yandex as - JWT stored in a secure cookie with XSRF protection. Cookies can be session-only - Minimal scopes with user name, id and picture (avatar) only - Direct authentication with user's provided credential checker -- Integrated avatar proxy with FS, boltdb and gridfs storages +- Confirmed authentication with user's provided sender (email, im, etc) +- Integrated avatar proxy with FS, boltdb and gridfs storage - Support of user-defined storage for avatars - Identicon for default avatars - Black list with user-defined validator @@ -146,6 +147,40 @@ Such provider acts like any other, i.e. will be registered as `/auth/local/login The API for this provider - `GET /auth//login?user=&passwd=&aud=&session=[1|0]` _note: password parameter doesn't have to be naked/real password and can be any kind of password hash prepared by caller._ + +### Verified authentication + +Another non-oauth2 provider allowing user-confirmed authentication, for example by email or slack or telegram. This is +done by adding confirmed provider with `auth.AddVerifProvider`. + +```go + msgTemplate := "Confirmation email, token: {{.Token}}" + service.AddVerifProvider("email", msgTemplate, sender) +``` + +Message template may use the follow elements: + +- `{{.Address}}` - user address, for example email +- `{{.User}}` - user name +- `{{.Token}}` - confirmation token +- `{{.Site}}` - site ID + +Sender should be provided by end-user and implements a single function interface + +```go +type Sender interface { + Send(address string, text string) error +} +``` + +For convenience a functional wrapper `SenderFunc` provided. + +The API for this provider: + + - `GET /auth//login?user=&address=&aud=&from=` - send confirmation request to user + - `GET /auth//login?token=&sess=[1|0]` - authorize with confirmation token + +The provider acts like any other, i.e. will be registered as `/auth/email/login`. ### Customization diff --git a/_example/go.mod b/_example/go.mod index 0a12417..57e7116 100644 --- a/_example/go.mod +++ b/_example/go.mod @@ -5,8 +5,6 @@ replace github.com/go-pkgz/auth => ../ require ( github.com/go-chi/chi v4.0.1+incompatible github.com/go-pkgz/auth v0.4.1 - github.com/go-pkgz/lgr v0.4.0 - github.com/go-pkgz/rest v1.4.0 - github.com/hashicorp/golang-lru v0.5.1 // indirect - github.com/stretchr/objx v0.1.1 // indirect + github.com/go-pkgz/lgr v0.6.2 + github.com/go-pkgz/rest v1.4.1 ) diff --git a/_example/go.sum b/_example/go.sum index a5bca7d..b120f15 100644 --- a/_example/go.sum +++ b/_example/go.sum @@ -1,32 +1,62 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI= github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/eefret/gravatar v0.0.0-20181201135945-2163a437cdca/go.mod h1:INXlE8NSNzQOzkK8ycoVKV8V+zQi70Ms3Bx/KyOuYgs= +github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 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-chi/chi v4.0.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA= github.com/go-chi/chi v4.0.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= github.com/go-pkgz/lgr v0.3.2 h1:bS4y0Px8cj6yce1JG6tpemLNRzBUA3Ph3UzvbcBn+cg= github.com/go-pkgz/lgr v0.3.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= github.com/go-pkgz/lgr v0.4.0 h1:s4490VXkaepbkMZBNZgr3rgfUg0G4nOLVa/Yp0hlwyc= github.com/go-pkgz/lgr v0.4.0/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= +github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= 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/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0= github.com/go-pkgz/rest v1.2.0 h1:75GVv25NmkV2l4dBr/io/ZApJ6zWQu5aZ4wFJA6QQCw= github.com/go-pkgz/rest v1.2.0/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk= github.com/go-pkgz/rest v1.4.0 h1:xNkdMjEL2rNZSHouWjFTH22ncaZ77fopm34RN+eXAwk= github.com/go-pkgz/rest v1.4.0/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk= +github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 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= @@ -35,19 +65,102 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/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/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0 h1:1DW40AJQ7AP4nY6ORUGUdkpXyEC9W2GAXcOPaMZK0K8= golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190530171427-2b03ca6e44eb/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190612180059-59534d075a87/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190607181801-497c8f037f5a/go.mod h1:JlmFZigtG9vBVR3QGIQ9g/Usz4BzH+Xm6Z8iHQWRYUw= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/auth.go b/auth.go index 6f9b3af..86a1dd3 100644 --- a/auth.go +++ b/auth.go @@ -235,6 +235,21 @@ func (s *Service) AddDirectProvider(name string, credChecker provider.CredChecke s.authMiddleware.Providers = s.providers } +// AddVerifProvider adds provider user's verification sent by sender +func (s *Service) AddVerifProvider(name string, msgTmpl string, sender provider.Sender) { + dh := provider.VerifyHandler{ + L: s.logger, + ProviderName: name, + Issuer: s.issuer, + TokenService: s.jwtService, + AvatarSaver: s.avatarProxy, + Sender: sender, + Template: msgTmpl, + } + s.providers = append(s.providers, provider.NewService(dh)) + s.authMiddleware.Providers = s.providers +} + // DevAuth makes dev oauth2 server, for testing and development only! func (s *Service) DevAuth() (*provider.DevAuthServer, error) { p, err := s.Provider("dev") // peak dev provider diff --git a/auth_test.go b/auth_test.go index 59fffcc..adcfc11 100644 --- a/auth_test.go +++ b/auth_test.go @@ -170,7 +170,7 @@ func TestIntegrationList(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - assert.Equal(t, `["dev","github","direct"]`+"\n", string(b)) + assert.Equal(t, `["dev","github","direct","email"]`+"\n", string(b)) } func TestIntegrationUserInfo(t *testing.T) { @@ -192,7 +192,7 @@ func TestIntegrationUserInfo(t *testing.T) { assert.Equal(t, 200, resp.StatusCode) defer resp.Body.Close() - //get user info + // get user info req, err := http.NewRequest("GET", "http://127.0.0.1:8080/auth/user", nil) require.NoError(t, err) t.Log(resp.Cookies()) @@ -285,12 +285,56 @@ func TestDirectProvider(t *testing.T) { defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + t.Logf("resp %s", string(body)) + t.Logf("headers: %+v", resp.Header) + require.Equal(t, 2, len(resp.Cookies())) + assert.Equal(t, "JWT", resp.Cookies()[0].Name) + assert.NotEqual(t, "", resp.Cookies()[0].Value, "token set") + assert.Equal(t, 86400, resp.Cookies()[0].MaxAge) + assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name) + assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set") + resp, err = client.Get("http://127.0.0.1:8080/private") require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) defer resp.Body.Close() } +func TestVerifProvider(t *testing.T) { + teardown := prepService(t) + defer teardown() + + // login + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get("http://127.0.0.1:8080/auth/email/login?user=dev&address=email") + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 200, resp.StatusCode) + + tkn := sender.text + jar, err := cookiejar.New(nil) + require.Nil(t, err) + client = &http.Client{Jar: jar, Timeout: 5 * time.Second} + resp, err = client.Get("http://127.0.0.1:8080/auth/email/login?token=" + tkn) + require.Nil(t, err) + defer resp.Body.Close() + assert.Equal(t, 200, resp.StatusCode) + + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + t.Logf("resp %s", string(body)) + t.Logf("headers: %+v", resp.Header) + require.Equal(t, 2, len(resp.Cookies())) + assert.Equal(t, "JWT", resp.Cookies()[0].Name) + assert.NotEqual(t, "", resp.Cookies()[0].Value, "token set") + assert.Equal(t, 86400, resp.Cookies()[0].MaxAge) + assert.Equal(t, "XSRF-TOKEN", resp.Cookies()[1].Name) + assert.NotEqual(t, "", resp.Cookies()[1].Value, "xsrf cookie set") + +} + func prepService(t *testing.T) (teardown func()) { options := Opts{ @@ -320,6 +364,8 @@ func prepService(t *testing.T) (teardown func()) { return user == "dev_direct" && password == "password", nil })) + svc.AddVerifProvider("email", "{{.Token}}", &sender) + // run dev/test oauth2 server on :8084 devAuth, err := svc.DevAuth() require.NoError(t, err) @@ -354,3 +400,21 @@ func prepService(t *testing.T) (teardown func()) { os.RemoveAll("/tmp/auth-pkgz") } } + +var sender = mockSender{} + +type mockSender struct { + err error + + to string + text string +} + +func (m *mockSender) Send(to string, text string) error { + if m.err != nil { + return m.err + } + m.to = to + m.text = text + return nil +} diff --git a/go.mod b/go.mod index e6b5dd8..e30a71a 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,22 @@ 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 + cloud.google.com/go v0.40.0 // indirect + github.com/coreos/bbolt v1.3.3 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 - github.com/go-pkgz/mongo v1.0.0 - github.com/go-pkgz/rest v1.2.0 + github.com/go-pkgz/lgr v0.6.2 // indirect + github.com/go-pkgz/mongo v1.1.2 + github.com/go-pkgz/rest v1.4.1 github.com/kr/pretty v0.1.0 // indirect github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.3.0 - golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b - golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0 // 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-20190109145017-48ac38b7c8cb // indirect - google.golang.org/appengine v1.4.0 // indirect + go.etcd.io/bbolt v1.3.3 // indirect + golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff + golang.org/x/net v0.0.0-20190611141213-3f473d35a33a // indirect + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae // indirect + google.golang.org/appengine v1.6.1 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index d49f877..920e5a2 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,45 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.40.0 h1:FjSY7bOj+WzJe6TZRVtXI2b9kAYvtNg4lMbcH2+MUkk= +cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 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-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.2.0 h1:75GVv25NmkV2l4dBr/io/ZApJ6zWQu5aZ4wFJA6QQCw= -github.com/go-pkgz/rest v1.2.0/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk= +github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= +github.com/go-pkgz/lgr v0.6.2 h1:Twf2YIe2J5tg7mKs+IkDDxrDF7GWlTCl/LzqELWjT5o= +github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8= +github.com/go-pkgz/mongo v1.1.2 h1:2Vqn3CWQJkkx4gxxDiQUitAW2FN/CH26lKHkipmpKcc= +github.com/go-pkgz/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0= +github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw= +github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 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/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 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= @@ -33,21 +54,78 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ= +golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0 h1:1DW40AJQ7AP4nY6ORUGUdkpXyEC9W2GAXcOPaMZK0K8= -golang.org/x/net v0.0.0-20190107210223-45ffb0cd1ba0/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/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a h1:+KkCgOMgnKSgenxTBoiwkMqTiouMIy/3o8RLdmSbGoY= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190109145017-48ac38b7c8cb h1:1w588/yEchbPNpa9sEvOcMZYbWHedwJjg4VOAdDHWHk= -golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae h1:xiXzMMEQdQcric9hXtr1QU98MHunKK7OTtsoU6bYWs4= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/provider/direct.go b/provider/direct.go index 6546518..0303489 100644 --- a/provider/direct.go +++ b/provider/direct.go @@ -69,9 +69,16 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { return } + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "can't make token id") + return + } + claims := token.Claims{ User: &u, StandardClaims: jwt.StandardClaims{ + Id: cid, Issuer: p.Issuer, Audience: aud, }, diff --git a/provider/direct_test.go b/provider/direct_test.go index ae35b10..46ba038 100644 --- a/provider/direct_test.go +++ b/provider/direct_test.go @@ -87,8 +87,8 @@ func TestDirect_LoginHandlerFailed(t *testing.T) { handler.ServeHTTP(rr, req) assert.Equal(t, 403, rr.Code) assert.Equal(t, `{"error":"incorrect user or password"}`+"\n", rr.Body.String()) - } + func TestDirect_Logout(t *testing.T) { d := DirectHandler{ ProviderName: "test", diff --git a/provider/service.go b/provider/service.go index d5a7ee1..8efc3ed 100644 --- a/provider/service.go +++ b/provider/service.go @@ -1,11 +1,14 @@ package provider import ( + "crypto/md5" "crypto/rand" "crypto/sha1" + "encoding/hex" "fmt" "net/http" "strings" + "time" "github.com/pkg/errors" @@ -89,3 +92,21 @@ func randToken() (string, error) { } return fmt.Sprintf("%x", s.Sum(nil)), nil } + +func getGravatarURL(email string) (res string, err error) { + + hash := md5.Sum([]byte(email)) + hexHash := hex.EncodeToString(hash[:]) + + client := http.Client{Timeout: 1 * time.Second} + res = "https://www.gravatar.com/avatar/" + hexHash + ".jpg" + resp, err := client.Get(res + "?d=404&s=80") + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return "", errors.New(resp.Status) + } + return res, nil +} diff --git a/provider/service_test.go b/provider/service_test.go index d155674..c386dfa 100644 --- a/provider/service_test.go +++ b/provider/service_test.go @@ -69,6 +69,14 @@ func TestSetAvatar(t *testing.T) { assert.Error(t, err, "some error") } +func TestService_getGravatarURL(t *testing.T) { + r, err := getGravatarURL("eefretsoul@gmail.com") + require.NoError(t, err) + assert.Equal(t, "https://www.gravatar.com/avatar/c82739de14cf64affaf30856ca95b851.jpg", r) + r, err = getGravatarURL("umputun-xyz@example.com") + require.EqualError(t, err, "404 Not Found") +} + type mockAva struct { ok bool res string diff --git a/provider/verify.go b/provider/verify.go new file mode 100644 index 0000000..8b2f926 --- /dev/null +++ b/provider/verify.go @@ -0,0 +1,198 @@ +package provider + +import ( + "bytes" + "crypto/sha1" + "net/http" + "strings" + "text/template" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/go-pkgz/rest" + "github.com/pkg/errors" + + "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/token" +) + +// VerifyHandler implements non-oauth2 provider authorizing users with some confirmation. +// can be email, IM or anything else implementing Sender interface +type VerifyHandler struct { + logger.L + ProviderName string + TokenService VerifTokenService + Issuer string + AvatarSaver AvatarSaver + Sender Sender + Template string +} + +// Sender defines interface to send emails +type Sender interface { + Send(address string, text string) error +} + +// SenderFunc type is an adapter to allow the use of ordinary functions as Sender. +type SenderFunc func(address string, text string) error + +// Send calls f(address,text) to implement Sender interface +func (f SenderFunc) Send(address string, text string) error { + return f(address, text) +} + +// TokenService defines interface accessing tokens +type VerifTokenService interface { + Token(claims token.Claims) (string, error) + Parse(tokenString string) (claims token.Claims, err error) + Set(w http.ResponseWriter, claims token.Claims) (token.Claims, error) + Reset(w http.ResponseWriter) +} + +// Name of the handler +func (e VerifyHandler) Name() string { return e.ProviderName } + +// LoginHandler gets name and address from query, makes confirmation token and sends it to user. +// In case if confirmation token presented in the query uses it to create auth token +func (e VerifyHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { + + // GET /login?site=site&&user=name&address=someone@example.com + tkn := r.URL.Query().Get("token") + if tkn == "" { // no token, ask confirmation via email + e.sendConfirmation(w, r) + return + } + + // confirmation token presented + // GET /login?token=confirmation-jwt&sess=1 + confClaims, err := e.TokenService.Parse(tkn) + if err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusForbidden, err, "failed to verify confirmation token") + return + } + + elems := strings.Split(confClaims.Handshake.ID, "::") + if len(elems) != 2 { + rest.SendErrorJSON(w, r, e.L, http.StatusBadRequest, errors.New(confClaims.Handshake.ID), "invalid handshake token") + return + } + user, address := elems[0], elems[1] + sessOnly := r.URL.Query().Get("sess") == "1" + + u := token.User{ + Name: user, + ID: e.ProviderName + "_" + token.HashID(sha1.New(), address), + } + // try to get gravatar for email + if strings.Contains(address, "@") { // TODO: better email check to avoid silly hits to gravatar api + if picURL, err := getGravatarURL(address); err == nil { + u.Picture = picURL + } + } + + if u, err = setAvatar(e.AvatarSaver, u); err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to save avatar to proxy") + return + } + + cid, err := randToken() + if err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't make token id") + return + } + + claims := token.Claims{ + User: &u, + StandardClaims: jwt.StandardClaims{ + Id: cid, + Issuer: e.Issuer, + Audience: confClaims.Audience, + }, + SessionOnly: sessOnly, + } + + if _, err = e.TokenService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to set token") + return + } + if confClaims.Handshake != nil && confClaims.Handshake.From != "" { + http.Redirect(w, r, confClaims.Handshake.From, http.StatusTemporaryRedirect) + return + } + rest.RenderJSON(w, r, claims.User) +} + +// GET /login?site=site&&user=name&address=someone@example.com +func (e VerifyHandler) sendConfirmation(w http.ResponseWriter, r *http.Request) { + user, address := r.URL.Query().Get("user"), r.URL.Query().Get("address") + if user == "" || address == "" { + rest.SendErrorJSON(w, r, e.L, http.StatusBadRequest, errors.New("wrong request"), "can't get user and address") + return + } + claims := token.Claims{ + Handshake: &token.Handshake{ + State: "", + From: r.URL.Query().Get("from"), + ID: user + "::" + address, + }, + SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0", + StandardClaims: jwt.StandardClaims{ + Audience: r.URL.Query().Get("site"), + ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), + NotBefore: time.Now().Add(-1 * time.Minute).Unix(), + Issuer: e.Issuer, + }, + } + + tkn, err := e.TokenService.Token(claims) + if err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusForbidden, err, "failed to make login token") + return + } + + tmpl := emailTemplate + if e.Template != "" { + tmpl = e.Template + } + emailTmpl, err := template.New("confirm").Parse(tmpl) + if err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't parse confirmation template") + return + } + + tmplData := struct { + User string + Address string + Token string + Site string + }{ + User: user, + Address: address, + Token: tkn, + Site: r.URL.Query().Get("site"), + } + buf := bytes.Buffer{} + if err = emailTmpl.Execute(&buf, tmplData); err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't execute confirmation template") + return + } + + if err := e.Sender.Send(address, buf.String()); err != nil { + rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to send confirmation") + return + } +} + +// AuthHandler doesn't do anything for direct login as it has no callbacks +func (e VerifyHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {} + +// LogoutHandler - GET /logout +func (e VerifyHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + e.TokenService.Reset(w) +} + +var emailTemplate = ` +Remark42 confirmation for {{.User}} {{.Address}}, site {{.Site}} + +Token: {{.Token}} +` diff --git a/provider/verify_test.go b/provider/verify_test.go new file mode 100644 index 0000000..c425ba2 --- /dev/null +++ b/provider/verify_test.go @@ -0,0 +1,258 @@ +package provider + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/token" +) + +var ( + testConfirmedToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTg2MDMwNzQxMiwibmJmIjoxNTYwMzA1NTUyLCJoYW5kc2hha2UiOnsiaWQiOiJ0ZXN0MTIzOjpibGFoQHVzZXIuY29tIn19.D8AvAunK7Tj-P6P56VyaoZ-hyA6U8duZ9HV8-ACEya8` + testConfirmedBadIDToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTg2MDMwNzQxMiwibmJmIjoxNTYwMzA1NTUyLCJoYW5kc2hha2UiOnsiaWQiOiJibGFoQHVzZXIuY29tIn19.hB91-kyY9-Q2Ln6IJGR9StQi-QQiXYu8SV31YhOoTbc` + testConfirmedGravatar = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTg2MDMwNzQxMiwibmJmIjoxNTYwMzA1NTUyLCJoYW5kc2hha2UiOnsiaWQiOiJncmF2YTo6ZWVmcmV0c291bEBnbWFpbC5jb20ifX0.yQTtG7neX3YjLZ-SGeiiNmwNfJWA7nR50KAxDw834XE` +) + +func TestVerifyHandler_LoginSendConfirm(t *testing.T) { + + emailer := mockSender{} + e := VerifyHandler{ + ProviderName: "test", + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + Sender: SenderFunc(emailer.Send), + Template: "{{.User}} {{.Address}} {{.Site}} token:{{.Token}}", + } + + handler := http.HandlerFunc(e.LoginHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/login?address=blah@user.com&user=test123&site=remark42", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 200, rr.Code) + assert.Equal(t, "blah@user.com", emailer.to) + assert.Contains(t, emailer.text, "test123 blah@user.com remark42 token:") + + tknStr := strings.Split(emailer.text, " token:")[1] + tkn, err := e.TokenService.Parse(tknStr) + assert.NoError(t, err) + t.Logf("%s %+v", tknStr, tkn) + assert.Equal(t, "test123::blah@user.com", tkn.Handshake.ID) + assert.Equal(t, "remark42", tkn.Audience) + assert.True(t, tkn.ExpiresAt > tkn.NotBefore) + + assert.Equal(t, "test", e.Name()) +} + +func TestVerifyHandler_LoginAcceptConfirm(t *testing.T) { + e := VerifyHandler{ + ProviderName: "test", + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + } + + handler := http.HandlerFunc(e.LoginHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", fmt.Sprintf("/login?token=%s&sess=1", testConfirmedToken), nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 200, rr.Code) + assert.Equal(t, `{"name":"test123","id":"test_63c1017838e567a526800790805eae4dc975402b","picture":""}`+"\n", rr.Body.String()) + + request := &http.Request{Header: http.Header{"Cookie": rr.Header()["Set-Cookie"]}} + c, err := request.Cookie("JWT") + require.NoError(t, err) + claims, err := e.TokenService.Parse(c.Value) + require.NoError(t, err) + t.Logf("%+v", claims) + assert.Equal(t, "remark42", claims.Audience) + assert.Equal(t, "iss-test", claims.Issuer) + assert.True(t, claims.ExpiresAt > time.Now().Unix()) + assert.Equal(t, "test123", claims.User.Name) + assert.Equal(t, true, claims.SessionOnly) +} + +func TestVerifyHandler_LoginAcceptConfirmWithAvatar(t *testing.T) { + e := VerifyHandler{ + ProviderName: "test", + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + } + + handler := http.HandlerFunc(e.LoginHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", fmt.Sprintf("/login?token=%s&sess=1", testConfirmedGravatar), nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 200, rr.Code) + assert.Equal(t, `{"name":"grava","id":"test_47dbf92d92954b1297cae73a864c159b4d847b9f","picture":"https://www.gravatar.com/avatar/c82739de14cf64affaf30856ca95b851.jpg"}`+"\n", rr.Body.String()) +} + +func TestVerifyHandler_LoginHandlerFailed(t *testing.T) { + emailer := mockSender{} + d := VerifyHandler{ + ProviderName: "test", + Sender: &emailer, + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + } + + handler := http.HandlerFunc(d.LoginHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/login?user=myuser&aud=xyz123", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 400, rr.Code) + assert.Equal(t, `{"error":"can't get user and address"}`+"\n", rr.Body.String()) + + d.Sender = &mockSender{err: errors.New("some err")} + handler = http.HandlerFunc(d.LoginHandler) + rr = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/login?user=myuser&address=pppp&aud=xyz123", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 500, rr.Code) + assert.Equal(t, `{"error":"failed to send confirmation"}`+"\n", rr.Body.String()) + + rr = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/login?token=bad", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusForbidden, rr.Code) + assert.Equal(t, `{"error":"failed to verify confirmation token"}`+"\n", rr.Body.String()) + + rr = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/login?token="+testConfirmedBadIDToken, nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Equal(t, `{"error":"invalid handshake token"}`+"\n", rr.Body.String()) + + d.Template = `{{.Blah}}` + d.Sender = &mockSender{} + handler = http.HandlerFunc(d.LoginHandler) + rr = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/login?user=myuser&address=pppp&aud=xyz123", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + assert.Equal(t, `{"error":"can't execute confirmation template"}`+"\n", rr.Body.String()) +} + +func TestVerifyHandler_LoginHandlerAvatarFailed(t *testing.T) { + emailer := mockSender{} + d := VerifyHandler{ + ProviderName: "test", + Sender: &emailer, + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + AvatarSaver: mockAvatarSaverVerif{err: errors.New("avatar save error")}, + } + + handler := http.HandlerFunc(d.LoginHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/login?token="+testConfirmedToken, nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 500, rr.Code) + assert.Equal(t, `{"error":"failed to save avatar to proxy"}`+"\n", rr.Body.String()) +} + +func TestVerifyHandler_AuthHandler(t *testing.T) { + d := VerifyHandler{} + handler := http.HandlerFunc(d.AuthHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/callback", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 200, rr.Code) +} + +func TestVerifyHandler_Logout(t *testing.T) { + d := VerifyHandler{ + ProviderName: "test", + TokenService: token.NewService(token.Opts{ + SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }), + TokenDuration: time.Hour, + CookieDuration: time.Hour * 24 * 31, + }), + Issuer: "iss-test", + L: logger.Std, + } + + handler := http.HandlerFunc(d.LogoutHandler) + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/logout", nil) + require.NoError(t, err) + handler.ServeHTTP(rr, req) + assert.Equal(t, 200, rr.Code) + assert.Equal(t, 2, len(rr.Header()["Set-Cookie"])) + + request := &http.Request{Header: http.Header{"Cookie": rr.Header()["Set-Cookie"]}} + c, err := request.Cookie("JWT") + require.NoError(t, err) + assert.Equal(t, time.Time{}, c.Expires) + + c, err = request.Cookie("XSRF-TOKEN") + require.NoError(t, err) + assert.Equal(t, time.Time{}, c.Expires) +} + +type mockSender struct { + err error + + to string + text string +} + +func (m *mockSender) Send(to string, text string) error { + if m.err != nil { + return m.err + } + m.to = to + m.text = text + return nil +} + +type mockAvatarSaverVerif struct { + err error + url string +} + +func (a mockAvatarSaverVerif) Put(u token.User) (avatarURL string, err error) { + return a.url, a.err +}