From 0ecc411c5cfc98c0cb47c3b01c0bb71d1279abb3 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Tue, 4 May 2021 18:46:15 +0200 Subject: [PATCH 01/29] testing --- internal/dispatcher/notification.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 2a59a6a..bde0051 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -24,6 +24,8 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio _, err := d.client.SendFormattedText(a.MatrixID, text, formattedText) + //testing + return err } From de3f94fff3d823e7083fbb4fb8156ea5095a2808 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Tue, 4 May 2021 18:50:09 +0200 Subject: [PATCH 02/29] remove testing --- internal/dispatcher/notification.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index bde0051..2a59a6a 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -24,8 +24,6 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio _, err := d.client.SendFormattedText(a.MatrixID, text, formattedText) - //testing - return err } From 2c4400eed476587276a1a518bfa3fc616d4c8b1f Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 9 May 2021 19:26:56 +0200 Subject: [PATCH 03/29] add PoC for oauth2 added a proof of concept for oauth2 --- README.md | 30 ++++++ cmd/pushbits/main.go | 2 +- config.example.yml | 2 + go.mod | 4 + go.sum | 125 +++++++++++++++++++++++ internal/authentication/oauth/handler.go | 27 +++++ internal/authentication/oauth/init.go | 43 ++++++++ internal/configuration/configuration.go | 3 +- internal/router/router.go | 34 +++++- 9 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 internal/authentication/oauth/handler.go create mode 100644 internal/authentication/oauth/init.go diff --git a/README.md b/README.md index d72ead4..60b9cb3 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,36 @@ You can retrieve the token using [pbcli](https://github.com/PushBits/cli) by run pbcli application show $PB_APPLICATION --url https://pushbits.example.com --username $PB_USERNAME ``` +### Authentication + +Pushbits offers you two methods of authenticating against the server: + +* Basic authentication (`basic`) +* Oauth 2 (`oauth`) + +You will find the corresponding setting in the security section. + +```yaml +... +security: + ... + # The authentication method to use + authentication: basic +... +``` + +#### Basic authentication + +For [basic authentication](https://de.wikipedia.org/wiki/HTTP-Authentifizierung) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag: + +```bash +curl --username myusername:totalysecretpassword +``` + +#### Oauth 2 + +[Oauth 2](https://de.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the pushbits server. + ### Message options Messages are supporting three different syntaxes: diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index 424816a..c1ad634 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -60,7 +60,7 @@ func main() { log.Fatal(err) } - engine := router.Create(c.Debug, cm, db, dp) + engine := router.Create(c.Debug, cm, db, dp, c.Security.Authentication) runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port) } diff --git a/config.example.yml b/config.example.yml index 7d042d1..354f39e 100644 --- a/config.example.yml +++ b/config.example.yml @@ -47,6 +47,8 @@ matrix: security: # Wether or not to check for weak passwords using HIBP. checkhibp: false + # The authentication method to use + authentication: basic crypto: # Configuration of the KDF for password storage. Do not change unless you know what you are doing! diff --git a/go.mod b/go.mod index e5c9e72..bb3fbc2 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,14 @@ require ( github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b github.com/gin-contrib/location v0.0.2 github.com/gin-gonic/gin v1.6.3 + github.com/go-oauth2/gin-server v1.0.0 github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd github.com/google/go-cmp v0.5.0 // indirect + github.com/imrenagi/go-oauth2-mysql v1.1.0 github.com/jinzhu/configor v1.2.1 + github.com/jmoiron/sqlx v1.3.3 github.com/json-iterator/go v1.1.10 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd @@ -19,6 +22,7 @@ require ( github.com/modern-go/reflect2 v1.0.1 // indirect github.com/ugorji/go v1.2.4 // indirect golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect + gopkg.in/oauth2.v3 v3.12.0 gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/mysql v1.0.4 gorm.io/driver/sqlite v1.1.4 diff --git a/go.sum b/go.sum index c30c298..c11e0f0 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,30 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b h1:jEg+fE+POnmUy40B+aSKEPqZDmsdl55hZU0YKXEzz1k= github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 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/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v0.0.0-20180803094507-bdde30871313/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gavv/monotime v0.0.0-20171021193802-6f8212e8d10d/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4= github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-oauth2/gin-server v1.0.0 h1:KR4YMbaK6Od/a0ZIeu1UhoIiYCKT7+2xfrHet0g6E9U= +github.com/go-oauth2/gin-server v1.0.0/go.mod h1:f08F3l5/Pbayb4pjnv5PpUdQLFejgGfHrTjA6IZb0eM= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -21,8 +35,12 @@ github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -39,25 +57,57 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/imrenagi/go-oauth2-mysql v1.1.0 h1:VcLoIB5RMfYjLg9OoOvawKZ32ErQ47BxgL96T7LDjf4= +github.com/imrenagi/go-oauth2-mysql v1.1.0/go.mod h1:mKCypNaKZroPJoeuMmA2g6q1tO4tIKD3hN7FoMUhwUA= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgx v3.5.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd h1:xVrqJK3xHREMNjwjljkAUaadalWc0rRbmVuQatzmgwg= github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -69,14 +119,46 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/buntdb v1.0.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= +github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao= +github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= +github.com/tidwall/gjson v1.1.3/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA= +github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= +github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= @@ -85,12 +167,44 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8= github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.0.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vgarvardt/go-pg-adapter v0.3.0/go.mod h1:+ogRTaGusDQb1lhZGoUxKQAGIbL+Lv43ePl/NUQArPI= +github.com/vgarvardt/pgx-helpers v0.0.0-20190703163610-cbb413594454/go.mod h1:xp2aDvL8NKu92fXxNr9kbH03+OJ+dIVu/dYfPxt3LWs= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= 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/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181217023233-e147a9138326/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-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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-20181221193216-37e7f081c4d4/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-20180909124046-d0be0721c37e/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-20190222072716-a9d3bda3a223/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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= @@ -99,8 +213,12 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -110,6 +228,13 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/oauth2.v3 v3.10.0/go.mod h1:nTG+m2PRcHR9jzGNrGdxSsUKz7vvwkqSlhFrstgZcRU= +gopkg.in/oauth2.v3 v3.12.0 h1:yOffAPoolH/i2JxwmC+pgtnY3362iPahsDpLXfDFvNg= +gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go new file mode 100644 index 0000000..3f6ec10 --- /dev/null +++ b/internal/authentication/oauth/handler.go @@ -0,0 +1,27 @@ +package oauth + +import ( + "log" + "net/http" + + "gopkg.in/oauth2.v3/server" +) + +// UserAuthHandler extracts user information from the query +func UserAuthHandler() server.UserAuthorizationHandler { + return func(w http.ResponseWriter, r *http.Request) (string, error) { + // TODO cubicroot check if we need a check here already + log.Println("UserAuthorizationHandler") + + return "1", nil + } +} + +// PasswordAuthorizationHandler handles username and password based authentication +func PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { + return func(username string, password string) (string, error) { + // TODO cubicroot get user id + log.Println("PW Handler") + return "5", nil + } +} diff --git a/internal/authentication/oauth/init.go b/internal/authentication/oauth/init.go new file mode 100644 index 0000000..adf5b1a --- /dev/null +++ b/internal/authentication/oauth/init.go @@ -0,0 +1,43 @@ +package oauth + +import ( + ginserver "github.com/go-oauth2/gin-server" + mysql "github.com/imrenagi/go-oauth2-mysql" + "github.com/jmoiron/sqlx" + + "gopkg.in/oauth2.v3/manage" + "gopkg.in/oauth2.v3/models" + "gopkg.in/oauth2.v3/server" + + "log" +) + +// InitializeOauth sets up the basics for oauth authentication +func InitializeOauth() error { + // Initialize the database + dbOauth, err := sqlx.Connect("mysql", "root:FqqVnitR8jkuZZeq8j94@tcp(db-pushbitsdev:3306)/pushbits?parseTime=true") // TODO cubicroot add more options and move to settings + if err != nil { + log.Fatal(err) + } + + manager := manage.NewDefaultManager() + manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) + + clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) + manager.MapClientStorage(clientStore) + + // TODO cubicroot move to settings + clientStore.Create(&models.Client{ + ID: "000000", + Secret: "999999", + Domain: "http://localhost", + }) + + ginserver.InitServer(manager) + ginserver.SetAllowGetAccessRequest(true) + ginserver.SetClientInfoHandler(server.ClientFormHandler) + ginserver.SetUserAuthorizationHandler(UserAuthHandler()) + ginserver.SetPasswordAuthorizationHandler(PasswordAuthorizationHandler()) + + return nil +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index dc3dc9e..2a860ad 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -45,7 +45,8 @@ type Configuration struct { Password string `required:"true"` } Security struct { - CheckHIBP bool `default:"false"` + CheckHIBP bool `default:"false"` + Authentication string `default:"basic"` } Crypto CryptoConfig Formatting Formatting diff --git a/internal/router/router.go b/internal/router/router.go index 587c256..2f2891c 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -6,15 +6,17 @@ import ( "github.com/pushbits/server/internal/api" "github.com/pushbits/server/internal/authentication" "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/authentication/oauth" "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/dispatcher" "github.com/gin-contrib/location" "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" ) // Create a Gin engine and setup all routes. -func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine { +func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authMethod string) *gin.Engine { log.Println("Setting up HTTP routes.") if !debug { @@ -31,6 +33,36 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r := gin.Default() r.Use(location.Default()) + // Example from the library: https://github.com/go-oauth2/oauth2/blob/master/example/server/server.go + // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ + + if authMethod == "oauth" { + oauth.InitializeOauth() + + oauthGroup := r.Group("/oauth2") + { + oauthGroup.GET("/token", ginserver.HandleTokenRequest) + // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET + // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&user_id=2&username=alex&password=123" -X GET -i + oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) + } + + // TODO cubicroot remove - currently only for testing + api := r.Group("/oauthtest") + { + api.Use(ginserver.HandleTokenVerify()) + api.GET("/info", func(c *gin.Context) { + ti, exists := c.Get(ginserver.DefaultConfig.TokenKey) + if exists { + c.JSON(200, ti) + return + } + c.String(200, "not found") + }) + } + } else { + // TODO cubicroot add other auth methods here + } applicationGroup := r.Group("/application") applicationGroup.Use(auth.RequireUser()) From b79b946f81278bab3e24a77299a45b1b064f1dc1 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 9 May 2021 20:09:57 +0200 Subject: [PATCH 04/29] add password anmd user check --- internal/authentication/oauth/handler.go | 20 ++++++++++++++++---- internal/authentication/oauth/init.go | 11 ++++++++--- internal/authentication/oauth/oauth.go | 16 ++++++++++++++++ internal/router/router.go | 2 +- 4 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 internal/authentication/oauth/oauth.go diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 3f6ec10..1ab8bcc 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -1,9 +1,11 @@ package oauth import ( + "fmt" "log" "net/http" + "github.com/pushbits/server/internal/authentication/credentials" "gopkg.in/oauth2.v3/server" ) @@ -18,10 +20,20 @@ func UserAuthHandler() server.UserAuthorizationHandler { } // PasswordAuthorizationHandler handles username and password based authentication -func PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { +func (a *Authenticator) PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { return func(username string, password string) (string, error) { - // TODO cubicroot get user id - log.Println("PW Handler") - return "5", nil + log.Println("Received password based authentication request") + + user, err := a.DB.GetUserByName(username) + + if err != nil || user == nil { + return "", nil + } + + if !credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return "", nil + } + + return fmt.Sprintf("%d", user.ID), nil } } diff --git a/internal/authentication/oauth/init.go b/internal/authentication/oauth/init.go index adf5b1a..f55ebcb 100644 --- a/internal/authentication/oauth/init.go +++ b/internal/authentication/oauth/init.go @@ -4,6 +4,7 @@ import ( ginserver "github.com/go-oauth2/gin-server" mysql "github.com/imrenagi/go-oauth2-mysql" "github.com/jmoiron/sqlx" + "github.com/pushbits/server/internal/database" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/models" @@ -13,13 +14,17 @@ import ( ) // InitializeOauth sets up the basics for oauth authentication -func InitializeOauth() error { +func InitializeOauth(db *database.Database) error { // Initialize the database - dbOauth, err := sqlx.Connect("mysql", "root:FqqVnitR8jkuZZeq8j94@tcp(db-pushbitsdev:3306)/pushbits?parseTime=true") // TODO cubicroot add more options and move to settings + dbOauth, err := sqlx.Connect("mysql", "?parseTime=true") // TODO cubicroot add more options and move to settings if err != nil { log.Fatal(err) } + auth := Authenticator{ + DB: db, + } + manager := manage.NewDefaultManager() manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) @@ -37,7 +42,7 @@ func InitializeOauth() error { ginserver.SetAllowGetAccessRequest(true) ginserver.SetClientInfoHandler(server.ClientFormHandler) ginserver.SetUserAuthorizationHandler(UserAuthHandler()) - ginserver.SetPasswordAuthorizationHandler(PasswordAuthorizationHandler()) + ginserver.SetPasswordAuthorizationHandler(auth.PasswordAuthorizationHandler()) return nil } diff --git a/internal/authentication/oauth/oauth.go b/internal/authentication/oauth/oauth.go new file mode 100644 index 0000000..8d1c5be --- /dev/null +++ b/internal/authentication/oauth/oauth.go @@ -0,0 +1,16 @@ +package oauth + +import ( + "github.com/pushbits/server/internal/model" +) + +// The Database interface for encapsulating database access. +type Database interface { + GetApplicationByToken(token string) (*model.Application, error) + GetUserByName(name string) (*model.User, error) +} + +// Authenticator is the provider for authentication +type Authenticator struct { + DB Database +} diff --git a/internal/router/router.go b/internal/router/router.go index 2f2891c..9725310 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -37,7 +37,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ if authMethod == "oauth" { - oauth.InitializeOauth() + oauth.InitializeOauth(db) oauthGroup := r.Group("/oauth2") { From a247db10996471435771527a826c52a19295ab89 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Tue, 11 May 2021 20:13:38 +0200 Subject: [PATCH 05/29] started moving oauth to current authentication --- README.md | 2 +- cmd/pushbits/main.go | 2 +- config.example.yml | 11 ++++- internal/api/middleware.go | 19 ++++++++ internal/authentication/authentication.go | 56 ++++++++++++++++++++++- internal/authentication/oauth/init.go | 45 ++++++++++-------- internal/configuration/configuration.go | 20 ++++++-- internal/router/router.go | 21 ++++++--- 8 files changed, 141 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 60b9cb3..d66f82b 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ security: For [basic authentication](https://de.wikipedia.org/wiki/HTTP-Authentifizierung) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag: ```bash -curl --username myusername:totalysecretpassword +curl -u myusername:totalysecretpassword ``` #### Oauth 2 diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index c1ad634..d256ca5 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -60,7 +60,7 @@ func main() { log.Fatal(err) } - engine := router.Create(c.Debug, cm, db, dp, c.Security.Authentication) + engine := router.Create(c.Debug, cm, db, dp, c.Authentication) runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port) } diff --git a/config.example.yml b/config.example.yml index 354f39e..4efc69a 100644 --- a/config.example.yml +++ b/config.example.yml @@ -47,8 +47,6 @@ matrix: security: # Wether or not to check for weak passwords using HIBP. checkhibp: false - # The authentication method to use - authentication: basic crypto: # Configuration of the KDF for password storage. Do not change unless you know what you are doing! @@ -62,3 +60,12 @@ crypto: formatting: # Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red). coloredtitle: false + +authentication: + # The authentication method to use + method: basic + oauth: + # The storage used for tokens + storage: "file" + # The connection to the storage + connection: "pushbits_token.db" diff --git a/internal/api/middleware.go b/internal/api/middleware.go index a6507e1..973fdc5 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -1,7 +1,13 @@ package api import ( + "fmt" + "log" + "github.com/gin-gonic/gin" + "gopkg.in/oauth2.v3" + + ginserver "github.com/go-oauth2/gin-server" ) type idInURI struct { @@ -20,3 +26,16 @@ func RequireIDInURI() gin.HandlerFunc { ctx.Set("id", requestModel.ID) } } + +// RequireIDFromToken returns a Gin middleware which requires an ID to be supplied by the oauth token +func RequireIDFromToken() gin.HandlerFunc { + return func(ctx *gin.Context) { + ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) + ti2, ok := ti.(oauth2.TokenInfo) + log.Println(fmt.Sprintf("USER ID: %s", ti2.GetUserID())) + if exists && ok { + ctx.Set("id", ti2.GetUserID) + return + } + } +} diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index 8cd0635..ac9e943 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -3,9 +3,13 @@ package authentication import ( "errors" "net/http" + "strconv" + ginserver "github.com/go-oauth2/gin-server" "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/model" + "gopkg.in/oauth2.v3" "github.com/gin-gonic/gin" ) @@ -18,11 +22,13 @@ const ( type Database interface { GetApplicationByToken(token string) (*model.Application, error) GetUserByName(name string) (*model.User, error) + GetUserByID(id uint) (*model.User, error) } // Authenticator is the provider for authentication middleware. type Authenticator struct { - DB Database + DB Database + Config configuration.Authentication } type hasUserProperty func(user *model.User) bool @@ -41,9 +47,42 @@ func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) return nil, errors.New("no credentials were supplied") } +func (a *Authenticator) userFromToken(ctx *gin.Context) (*model.User, error) { + ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) + if !exists { + return nil, errors.New("No token available") + } + + token, ok := ti.(oauth2.TokenInfo) + if !ok { + return nil, errors.New("Wrong token format") + } + + userID, err := strconv.ParseUint(token.GetUserID(), 10, 64) + if err != nil { + return nil, errors.New("User information of wrong format") + } + + user, err := a.DB.GetUserByID(uint(userID)) + if err != nil { + return nil, err + } + + return user, nil +} + func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc { return func(ctx *gin.Context) { - user, err := a.userFromBasicAuth(ctx) + var user *model.User + err := errors.New("No authentication method") + + switch a.Config.Method { + case "oauth": + user, err = a.userFromToken(ctx) + default: + user, err = a.userFromBasicAuth(ctx) + } + if err != nil { ctx.AbortWithError(http.StatusForbidden, err) return @@ -104,3 +143,16 @@ func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc { ctx.Set("app", app) } } + +// RequireValidAuthentication returns a Gin middleware which requires a valid authentication +func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc { + switch a.Config.Method { + case "oauth": + return ginserver.HandleTokenVerify() // TODO cubicroot move to own config and set error handler to display same errors as for basic auth + default: + // TODO cubicroot: not very nice to have duplicated code here - but we need the HandleTokenVerify somewhere + return a.requireUserProperty(func(user *model.User) bool { + return true + }) + } +} diff --git a/internal/authentication/oauth/init.go b/internal/authentication/oauth/init.go index f55ebcb..2ce8ee4 100644 --- a/internal/authentication/oauth/init.go +++ b/internal/authentication/oauth/init.go @@ -4,40 +4,49 @@ import ( ginserver "github.com/go-oauth2/gin-server" mysql "github.com/imrenagi/go-oauth2-mysql" "github.com/jmoiron/sqlx" + + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/models" "gopkg.in/oauth2.v3/server" + "errors" "log" ) // InitializeOauth sets up the basics for oauth authentication -func InitializeOauth(db *database.Database) error { - // Initialize the database - dbOauth, err := sqlx.Connect("mysql", "?parseTime=true") // TODO cubicroot add more options and move to settings - if err != nil { - log.Fatal(err) +func InitializeOauth(db *database.Database, config configuration.Authentication) error { + // TODO cubicroot move that to the authenticator? + manager := manage.NewDefaultManager() + + if config.Oauth.Storage == "mysql" { + dbOauth, err := sqlx.Connect("mysql", config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings + if err != nil { + log.Fatal(err) + } + + manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) + + clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) + manager.MapClientStorage(clientStore) + + // TODO cubicroot better only store the secret as hashed value and autogenerate? + clientStore.Create(&models.Client{ + ID: "000000", + Secret: "999999", + Domain: "http://localhost", + }) + } else { + // TODO cubicroot add more storage options + return errors.New("Unknown oauth storage") } auth := Authenticator{ DB: db, } - manager := manage.NewDefaultManager() - manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) - - clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) - manager.MapClientStorage(clientStore) - - // TODO cubicroot move to settings - clientStore.Create(&models.Client{ - ID: "000000", - Secret: "999999", - Domain: "http://localhost", - }) - ginserver.InitServer(manager) ginserver.SetAllowGetAccessRequest(true) ginserver.SetClientInfoHandler(server.ClientFormHandler) diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 2a860ad..6c4e563 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -23,6 +23,18 @@ type Formatting struct { ColoredTitle bool `default:"false"` } +// Authentication holds the settings for the oauth server +type Authentication struct { + Method string `default:"basic"` + Oauth Oauth +} + +// Oauth holds information about the oauth server +type Oauth struct { + Connection string `default:""` + Storage string `default:"file"` +} + // Configuration holds values that can be configured by the user. type Configuration struct { Debug bool `default:"false"` @@ -45,11 +57,11 @@ type Configuration struct { Password string `required:"true"` } Security struct { - CheckHIBP bool `default:"false"` - Authentication string `default:"basic"` + CheckHIBP bool `default:"false"` } - Crypto CryptoConfig - Formatting Formatting + Crypto CryptoConfig + Formatting Formatting + Authentication Authentication } func configFiles() []string { diff --git a/internal/router/router.go b/internal/router/router.go index 9725310..4fda699 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -7,6 +7,7 @@ import ( "github.com/pushbits/server/internal/authentication" "github.com/pushbits/server/internal/authentication/credentials" "github.com/pushbits/server/internal/authentication/oauth" + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/dispatcher" @@ -16,14 +17,17 @@ import ( ) // Create a Gin engine and setup all routes. -func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authMethod string) *gin.Engine { +func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authConfig configuration.Authentication) *gin.Engine { log.Println("Setting up HTTP routes.") if !debug { gin.SetMode(gin.ReleaseMode) } - auth := authentication.Authenticator{DB: db} + auth := authentication.Authenticator{ + DB: db, + Config: authConfig, + } applicationHandler := api.ApplicationHandler{DB: db, DP: dp} healthHandler := api.HealthHandler{DB: db} @@ -36,8 +40,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // Example from the library: https://github.com/go-oauth2/oauth2/blob/master/example/server/server.go // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ - if authMethod == "oauth" { - oauth.InitializeOauth(db) + if authConfig.Method == "oauth" { + oauth.InitializeOauth(db, authConfig) oauthGroup := r.Group("/oauth2") { @@ -48,10 +52,12 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp } // TODO cubicroot remove - currently only for testing - api := r.Group("/oauthtest") + oauthtest := r.Group("/oauthtest") + + oauthtest.Use(auth.RequireValidAuthentication()) + oauthtest.Use(auth.RequireUser()) { - api.Use(ginserver.HandleTokenVerify()) - api.GET("/info", func(c *gin.Context) { + oauthtest.GET("/info", func(c *gin.Context) { ti, exists := c.Get(ginserver.DefaultConfig.TokenKey) if exists { c.JSON(200, ti) @@ -59,6 +65,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp } c.String(200, "not found") }) + oauthtest.GET("", api.RequireIDFromToken(), applicationHandler.GetApplications) } } else { // TODO cubicroot add other auth methods here From 7c5ce31b3a19af80404982a543d6b267df869ab9 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 13 May 2021 17:55:26 +0200 Subject: [PATCH 06/29] integrate oauth in current auth --- internal/api/middleware.go | 19 ------------- internal/authentication/oauth/handler.go | 34 ++++++++++++++++++++++-- internal/authentication/oauth/init.go | 11 +++++++- internal/authentication/oauth/oauth.go | 4 +-- internal/router/router.go | 21 ++++++--------- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 973fdc5..a6507e1 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -1,13 +1,7 @@ package api import ( - "fmt" - "log" - "github.com/gin-gonic/gin" - "gopkg.in/oauth2.v3" - - ginserver "github.com/go-oauth2/gin-server" ) type idInURI struct { @@ -26,16 +20,3 @@ func RequireIDInURI() gin.HandlerFunc { ctx.Set("id", requestModel.ID) } } - -// RequireIDFromToken returns a Gin middleware which requires an ID to be supplied by the oauth token -func RequireIDFromToken() gin.HandlerFunc { - return func(ctx *gin.Context) { - ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) - ti2, ok := ti.(oauth2.TokenInfo) - log.Println(fmt.Sprintf("USER ID: %s", ti2.GetUserID())) - if exists && ok { - ctx.Set("id", ti2.GetUserID) - return - } - } -} diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 1ab8bcc..75b341a 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/pushbits/server/internal/authentication/credentials" "gopkg.in/oauth2.v3/server" @@ -19,8 +20,8 @@ func UserAuthHandler() server.UserAuthorizationHandler { } } -// PasswordAuthorizationHandler handles username and password based authentication -func (a *Authenticator) PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { +// PasswordAuthorizationHandler returns a PasswordAuthorizationHandler that handles username and password based authentication for access tokens +func (a *Oauth) PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { return func(username string, password string) (string, error) { log.Println("Received password based authentication request") @@ -37,3 +38,32 @@ func (a *Authenticator) PasswordAuthorizationHandler() server.PasswordAuthorizat return fmt.Sprintf("%d", user.ID), nil } } + +// ClientScopeHandler returns a ClientScopeHandler that allows or disallows scopes for access tokens +func ClientScopeHandler() server.ClientScopeHandler { + return func(clientID, scope string) (allowed bool, err error) { + if scope == "all" || scope == "" { // For now only allow generic scopes so there is place for future expansion + return true, nil + } + + return false, nil + } +} + +// AccessTokenExpHandler returns an AccessTokenExpHandler that sets the expiration time of access tokens +func AccessTokenExpHandler() server.AccessTokenExpHandler { + return func(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { + tokenTypeRaw, ok := r.URL.Query()["token_type"] + + if ok && len(tokenTypeRaw[0]) > 0 { + tokenType := tokenTypeRaw[0] + + switch tokenType { + case "longterm", "long": + return time.Duration(24*365*2) * time.Hour, nil + } + } + + return time.Duration(24) * time.Hour, nil // TODO cubicroot -> that is not displayed correctly? + } +} diff --git a/internal/authentication/oauth/init.go b/internal/authentication/oauth/init.go index 2ce8ee4..324d432 100644 --- a/internal/authentication/oauth/init.go +++ b/internal/authentication/oauth/init.go @@ -8,6 +8,7 @@ import ( "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" + "gopkg.in/oauth2.v3" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/models" "gopkg.in/oauth2.v3/server" @@ -43,7 +44,7 @@ func InitializeOauth(db *database.Database, config configuration.Authentication) return errors.New("Unknown oauth storage") } - auth := Authenticator{ + auth := Oauth{ DB: db, } @@ -52,6 +53,14 @@ func InitializeOauth(db *database.Database, config configuration.Authentication) ginserver.SetClientInfoHandler(server.ClientFormHandler) ginserver.SetUserAuthorizationHandler(UserAuthHandler()) ginserver.SetPasswordAuthorizationHandler(auth.PasswordAuthorizationHandler()) + ginserver.SetAllowedGrantType( + //oauth2.AuthorizationCode, + oauth2.PasswordCredentials, + //oauth2.ClientCredentials, + oauth2.Refreshing, + ) + ginserver.SetClientScopeHandler(ClientScopeHandler()) + ginserver.SetAccessTokenExpHandler(AccessTokenExpHandler()) return nil } diff --git a/internal/authentication/oauth/oauth.go b/internal/authentication/oauth/oauth.go index 8d1c5be..2ab3624 100644 --- a/internal/authentication/oauth/oauth.go +++ b/internal/authentication/oauth/oauth.go @@ -10,7 +10,7 @@ type Database interface { GetUserByName(name string) (*model.User, error) } -// Authenticator is the provider for authentication -type Authenticator struct { +// Oauth is the oauth provider for authentication +type Oauth struct { DB Database } diff --git a/internal/router/router.go b/internal/router/router.go index 4fda699..687ccb4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -47,17 +47,10 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp { oauthGroup.GET("/token", ginserver.HandleTokenRequest) // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET - // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&user_id=2&username=alex&password=123" -X GET -i + // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&user_id=2&username=admin&password=123" -X GET -i + // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&user_id=1&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) - } - - // TODO cubicroot remove - currently only for testing - oauthtest := r.Group("/oauthtest") - - oauthtest.Use(auth.RequireValidAuthentication()) - oauthtest.Use(auth.RequireUser()) - { - oauthtest.GET("/info", func(c *gin.Context) { + oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), func(c *gin.Context) { ti, exists := c.Get(ginserver.DefaultConfig.TokenKey) if exists { c.JSON(200, ti) @@ -65,13 +58,14 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp } c.String(200, "not found") }) - oauthtest.GET("", api.RequireIDFromToken(), applicationHandler.GetApplications) + + // TODO cubicroot: refresh handling + // revoking } - } else { - // TODO cubicroot add other auth methods here } applicationGroup := r.Group("/application") + applicationGroup.Use(auth.RequireValidAuthentication()) applicationGroup.Use(auth.RequireUser()) { applicationGroup.POST("", applicationHandler.CreateApplication) @@ -87,6 +81,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification) userGroup := r.Group("/user") + userGroup.Use(auth.RequireValidAuthentication()) userGroup.Use(auth.RequireAdmin()) { userGroup.POST("", userHandler.CreateUser) From 609eb663bdf4455504c8a76f26e2bacc7cbe145e Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Mon, 17 May 2021 20:34:05 +0200 Subject: [PATCH 07/29] move things again and introduce jwt tokens --- go.mod | 1 + internal/authentication/authentication.go | 26 +++--- internal/authentication/basicauth/handler.go | 56 +++++++++++ internal/authentication/oauth/handler.go | 28 ++---- internal/authentication/oauth/init.go | 66 ------------- internal/authentication/oauth/oauth.go | 97 +++++++++++++++++++- internal/router/router.go | 19 +++- 7 files changed, 188 insertions(+), 105 deletions(-) create mode 100644 internal/authentication/basicauth/handler.go delete mode 100644 internal/authentication/oauth/init.go diff --git a/go.mod b/go.mod index bb3fbc2..a5faa4f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gin-contrib/location v0.0.2 github.com/gin-gonic/gin v1.6.3 github.com/go-oauth2/gin-server v1.0.0 diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index ac9e943..a87db81 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -18,6 +18,10 @@ const ( headerName = "X-Gotify-Key" ) +type ( + AuthenticationValidator func() gin.HandlerFunc +) + // The Database interface for encapsulating database access. type Database interface { GetApplicationByToken(token string) (*model.Application, error) @@ -27,8 +31,9 @@ type Database interface { // Authenticator is the provider for authentication middleware. type Authenticator struct { - DB Database - Config configuration.Authentication + DB Database + Config configuration.Authentication + AuthenticationValidator AuthenticationValidator } type hasUserProperty func(user *model.User) bool @@ -44,7 +49,7 @@ func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) } } - return nil, errors.New("no credentials were supplied") + return nil, errors.New("no credentials were supplied 1") } func (a *Authenticator) userFromToken(ctx *gin.Context) (*model.User, error) { @@ -146,13 +151,10 @@ func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc { // RequireValidAuthentication returns a Gin middleware which requires a valid authentication func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc { - switch a.Config.Method { - case "oauth": - return ginserver.HandleTokenVerify() // TODO cubicroot move to own config and set error handler to display same errors as for basic auth - default: - // TODO cubicroot: not very nice to have duplicated code here - but we need the HandleTokenVerify somewhere - return a.requireUserProperty(func(user *model.User) bool { - return true - }) - } + return a.AuthenticationValidator() +} + +// SetAuthenticationValidator sets a function for handling authentication +func (a *Authenticator) SetAuthenticationValidator(f AuthenticationValidator) { + a.AuthenticationValidator = f } diff --git a/internal/authentication/basicauth/handler.go b/internal/authentication/basicauth/handler.go new file mode 100644 index 0000000..a7c7bbf --- /dev/null +++ b/internal/authentication/basicauth/handler.go @@ -0,0 +1,56 @@ +package basicauth + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/model" +) + +// The Database interface for encapsulating database access. +type Database interface { + GetApplicationByToken(token string) (*model.Application, error) + GetUserByName(name string) (*model.User, error) +} + +// Handler is the basic auth provider for authentication +type Handler struct { + DB Database +} + +func (h *Handler) AuthenticationValidator() gin.HandlerFunc { + return func(ctx *gin.Context) { + var user *model.User + err := errors.New("No authentication method") + + user, err = h.userFromBasicAuth(ctx) + + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + if user == nil { + ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + return + } + + ctx.Set("user", user) + } +} + +func (h *Handler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { + if name, password, ok := ctx.Request.BasicAuth(); ok { + if user, err := h.DB.GetUserByName(name); err != nil { + return nil, err + } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return user, nil + } else { + return nil, errors.New("credentials were invalid") + } + } + + return nil, errors.New("no credentials were supplied") +} diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 75b341a..a0f5115 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -1,12 +1,12 @@ package oauth import ( - "fmt" "log" "net/http" "time" - "github.com/pushbits/server/internal/authentication/credentials" + "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" "gopkg.in/oauth2.v3/server" ) @@ -20,25 +20,6 @@ func UserAuthHandler() server.UserAuthorizationHandler { } } -// PasswordAuthorizationHandler returns a PasswordAuthorizationHandler that handles username and password based authentication for access tokens -func (a *Oauth) PasswordAuthorizationHandler() server.PasswordAuthorizationHandler { - return func(username string, password string) (string, error) { - log.Println("Received password based authentication request") - - user, err := a.DB.GetUserByName(username) - - if err != nil || user == nil { - return "", nil - } - - if !credentials.ComparePassword(user.PasswordHash, []byte(password)) { - return "", nil - } - - return fmt.Sprintf("%d", user.ID), nil - } -} - // ClientScopeHandler returns a ClientScopeHandler that allows or disallows scopes for access tokens func ClientScopeHandler() server.ClientScopeHandler { return func(clientID, scope string) (allowed bool, err error) { @@ -67,3 +48,8 @@ func AccessTokenExpHandler() server.AccessTokenExpHandler { return time.Duration(24) * time.Hour, nil // TODO cubicroot -> that is not displayed correctly? } } + +func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { + log.Println("Oauth handling this here :D") + return ginserver.HandleTokenVerify() +} diff --git a/internal/authentication/oauth/init.go b/internal/authentication/oauth/init.go deleted file mode 100644 index 324d432..0000000 --- a/internal/authentication/oauth/init.go +++ /dev/null @@ -1,66 +0,0 @@ -package oauth - -import ( - ginserver "github.com/go-oauth2/gin-server" - mysql "github.com/imrenagi/go-oauth2-mysql" - "github.com/jmoiron/sqlx" - - "github.com/pushbits/server/internal/configuration" - "github.com/pushbits/server/internal/database" - - "gopkg.in/oauth2.v3" - "gopkg.in/oauth2.v3/manage" - "gopkg.in/oauth2.v3/models" - "gopkg.in/oauth2.v3/server" - - "errors" - "log" -) - -// InitializeOauth sets up the basics for oauth authentication -func InitializeOauth(db *database.Database, config configuration.Authentication) error { - // TODO cubicroot move that to the authenticator? - manager := manage.NewDefaultManager() - - if config.Oauth.Storage == "mysql" { - dbOauth, err := sqlx.Connect("mysql", config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings - if err != nil { - log.Fatal(err) - } - - manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) - - clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) - manager.MapClientStorage(clientStore) - - // TODO cubicroot better only store the secret as hashed value and autogenerate? - clientStore.Create(&models.Client{ - ID: "000000", - Secret: "999999", - Domain: "http://localhost", - }) - } else { - // TODO cubicroot add more storage options - return errors.New("Unknown oauth storage") - } - - auth := Oauth{ - DB: db, - } - - ginserver.InitServer(manager) - ginserver.SetAllowGetAccessRequest(true) - ginserver.SetClientInfoHandler(server.ClientFormHandler) - ginserver.SetUserAuthorizationHandler(UserAuthHandler()) - ginserver.SetPasswordAuthorizationHandler(auth.PasswordAuthorizationHandler()) - ginserver.SetAllowedGrantType( - //oauth2.AuthorizationCode, - oauth2.PasswordCredentials, - //oauth2.ClientCredentials, - oauth2.Refreshing, - ) - ginserver.SetClientScopeHandler(ClientScopeHandler()) - ginserver.SetAccessTokenExpHandler(AccessTokenExpHandler()) - - return nil -} diff --git a/internal/authentication/oauth/oauth.go b/internal/authentication/oauth/oauth.go index 2ab3624..dec092e 100644 --- a/internal/authentication/oauth/oauth.go +++ b/internal/authentication/oauth/oauth.go @@ -1,7 +1,24 @@ package oauth import ( + "errors" + "fmt" + "log" + "time" + + "github.com/dgrijalva/jwt-go" + ginserver "github.com/go-oauth2/gin-server" + mysql "github.com/imrenagi/go-oauth2-mysql" + "github.com/jmoiron/sqlx" + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/model" + "gopkg.in/oauth2.v3" + "gopkg.in/oauth2.v3/generates" + "gopkg.in/oauth2.v3/manage" + "gopkg.in/oauth2.v3/models" + "gopkg.in/oauth2.v3/server" ) // The Database interface for encapsulating database access. @@ -10,7 +27,81 @@ type Database interface { GetUserByName(name string) (*model.User, error) } -// Oauth is the oauth provider for authentication -type Oauth struct { - DB Database +// AuthHandler is the oauth provider for authentication +type AuthHandler struct { + db Database + manager *manage.Manager + config configuration.Authentication +} + +// Initialize prepares the AuthHandler +func (a *AuthHandler) Initialize(db *database.Database, config configuration.Authentication) error { + a.db = db + a.config = config + + a.manager = manage.NewDefaultManager() + a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) + // TODO cubicroot add more token configs + a.manager.SetPasswordTokenCfg(&manage.Config{ + AccessTokenExp: time.Duration(24) * time.Hour * 12, + RefreshTokenExp: time.Duration(24) * time.Hour * 24 * 30, + IsGenerateRefresh: true, + }) + a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS256)) // TODO cubicroot get RS256 to work + + if a.config.Oauth.Storage == "mysql" { + dbOauth, err := sqlx.Connect("mysql", a.config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings + if err != nil { + log.Fatal(err) + } + + a.manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) + + clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) + a.manager.MapClientStorage(clientStore) + + // TODO cubicroot better only store the secret as hashed value and autogenerate? + clientStore.Create(&models.Client{ + ID: "000000", + Secret: "999999", + Domain: "http://localhost", + }) + } else { + // TODO cubicroot add more storage options + return errors.New("Unknown oauth storage") + } + + ginserver.InitServer(a.manager) + ginserver.SetAllowGetAccessRequest(true) + ginserver.SetClientInfoHandler(server.ClientFormHandler) + ginserver.SetUserAuthorizationHandler(UserAuthHandler()) + ginserver.SetPasswordAuthorizationHandler(a.passwordAuthorizationHandler()) + ginserver.SetAllowedGrantType( + //oauth2.AuthorizationCode, + oauth2.PasswordCredentials, + //oauth2.ClientCredentials, + oauth2.Refreshing, + ) + ginserver.SetClientScopeHandler(ClientScopeHandler()) + ginserver.SetAccessTokenExpHandler(AccessTokenExpHandler()) + return nil +} + +// PasswordAuthorizationHandler returns a PasswordAuthorizationHandler that handles username and password based authentication for access tokens +func (a *AuthHandler) passwordAuthorizationHandler() server.PasswordAuthorizationHandler { + return func(username string, password string) (string, error) { + log.Println("Received password based authentication request") + + user, err := a.db.GetUserByName(username) + + if err != nil || user == nil { + return "", nil + } + + if !credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return "", nil + } + + return fmt.Sprintf("%d", user.ID), nil + } } diff --git a/internal/router/router.go b/internal/router/router.go index 687ccb4..f4de64b 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -5,6 +5,7 @@ import ( "github.com/pushbits/server/internal/api" "github.com/pushbits/server/internal/authentication" + "github.com/pushbits/server/internal/authentication/basicauth" "github.com/pushbits/server/internal/authentication/credentials" "github.com/pushbits/server/internal/authentication/oauth" "github.com/pushbits/server/internal/configuration" @@ -29,6 +30,18 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp Config: authConfig, } + switch authConfig.Method { + case "oauth": + authHandler := oauth.AuthHandler{} + authHandler.Initialize(db, authConfig) + auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) + default: + authHandler := basicauth.Handler{ + DB: db, + } + auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) + } + applicationHandler := api.ApplicationHandler{DB: db, DP: dp} healthHandler := api.HealthHandler{DB: db} notificationHandler := api.NotificationHandler{DB: db, DP: dp} @@ -41,15 +54,15 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ if authConfig.Method == "oauth" { - oauth.InitializeOauth(db, authConfig) oauthGroup := r.Group("/oauth2") { oauthGroup.GET("/token", ginserver.HandleTokenRequest) // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET - // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&user_id=2&username=admin&password=123" -X GET -i + // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&username=admin&password=123" -X GET -i // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&user_id=1&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET - oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) + oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects + // Use auth: curl "https://domain.tld/oauth2/auth?grant_type=password&client_id=000000&client_secret=999999&username=admin&password=21132&response_type=token" -X GET oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), func(c *gin.Context) { ti, exists := c.Get(ginserver.DefaultConfig.TokenKey) if exists { From 6ee285d7aa0018a29a0e1fe27d359f8ca6a719a6 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sat, 22 May 2021 19:34:50 +0200 Subject: [PATCH 08/29] move to authhandler --- internal/authentication/authentication.go | 37 ++++++----- .../basicauth/{handler.go => authhandler.go} | 29 +++++++-- .../oauth/{oauth.go => authhandler.go} | 48 ++++++++++++++- internal/authentication/oauth/handler.go | 61 ++++++------------- internal/authentication/oauth/middleware.go | 51 ++++++++++++++++ internal/authentication/oauth/responses.go | 20 ++++++ .../authentication/oauth/tokendisplayinfo.go | 25 ++++++++ internal/router/router.go | 54 ++++++++-------- 8 files changed, 234 insertions(+), 91 deletions(-) rename internal/authentication/basicauth/{handler.go => authhandler.go} (57%) rename internal/authentication/oauth/{oauth.go => authhandler.go} (67%) create mode 100644 internal/authentication/oauth/middleware.go create mode 100644 internal/authentication/oauth/responses.go create mode 100644 internal/authentication/oauth/tokendisplayinfo.go diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index a87db81..76bb60a 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -2,6 +2,7 @@ package authentication import ( "errors" + "log" "net/http" "strconv" @@ -19,7 +20,10 @@ const ( ) type ( + // AuthenticationValidator defines a type for authenticating a user AuthenticationValidator func() gin.HandlerFunc + // UserSetter defines a type for setting a user object + UserSetter func() gin.HandlerFunc ) // The Database interface for encapsulating database access. @@ -34,6 +38,7 @@ type Authenticator struct { DB Database Config configuration.Authentication AuthenticationValidator AuthenticationValidator + UserSetter UserSetter } type hasUserProperty func(user *model.User) bool @@ -78,17 +83,20 @@ func (a *Authenticator) userFromToken(ctx *gin.Context) (*model.User, error) { func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc { return func(ctx *gin.Context) { - var user *model.User - err := errors.New("No authentication method") - - switch a.Config.Method { - case "oauth": - user, err = a.userFromToken(ctx) - default: - user, err = a.userFromBasicAuth(ctx) + err := errors.New("User not found") + + u, exists := ctx.Get("user") + + if !exists { + log.Println("No user object in context") + ctx.AbortWithError(http.StatusForbidden, err) + return } - if err != nil { + user, ok := u.(*model.User) + + if !ok { + log.Println("User object from context has wrong format") ctx.AbortWithError(http.StatusForbidden, err) return } @@ -97,16 +105,12 @@ func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) return } - - ctx.Set("user", user) } } // RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request. func (a *Authenticator) RequireUser() gin.HandlerFunc { - return a.requireUserProperty(func(user *model.User) bool { - return true - }) + return a.UserSetter() } // RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request. @@ -158,3 +162,8 @@ func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc { func (a *Authenticator) SetAuthenticationValidator(f AuthenticationValidator) { a.AuthenticationValidator = f } + +// SetUserSetter sets a function that sets the user object in gin context +func (a *Authenticator) SetUserSetter(f UserSetter) { + a.UserSetter = f +} diff --git a/internal/authentication/basicauth/handler.go b/internal/authentication/basicauth/authhandler.go similarity index 57% rename from internal/authentication/basicauth/handler.go rename to internal/authentication/basicauth/authhandler.go index a7c7bbf..31f0a98 100644 --- a/internal/authentication/basicauth/handler.go +++ b/internal/authentication/basicauth/authhandler.go @@ -15,12 +15,33 @@ type Database interface { GetUserByName(name string) (*model.User, error) } -// Handler is the basic auth provider for authentication -type Handler struct { +// AuthHandler is the basic auth provider for authentication +type AuthHandler struct { DB Database } -func (h *Handler) AuthenticationValidator() gin.HandlerFunc { +// AuthenticationValidator returns a gin HandlerFunc that takes the basic auth credentials and validates them +func (h *AuthHandler) AuthenticationValidator() gin.HandlerFunc { + return func(ctx *gin.Context) { + var user *model.User + err := errors.New("No authentication method") + + user, err = h.userFromBasicAuth(ctx) + + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + if user == nil { + ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + return + } + } +} + +// UserSetter returns a gin HandlerFunc that takes the basic auth credentials and sets the corresponding user object +func (h *AuthHandler) UserSetter() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User err := errors.New("No authentication method") @@ -41,7 +62,7 @@ func (h *Handler) AuthenticationValidator() gin.HandlerFunc { } } -func (h *Handler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { +func (h *AuthHandler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { if name, password, ok := ctx.Request.BasicAuth(); ok { if user, err := h.DB.GetUserByName(name); err != nil { return nil, err diff --git a/internal/authentication/oauth/oauth.go b/internal/authentication/oauth/authhandler.go similarity index 67% rename from internal/authentication/oauth/oauth.go rename to internal/authentication/oauth/authhandler.go index dec092e..da00889 100644 --- a/internal/authentication/oauth/oauth.go +++ b/internal/authentication/oauth/authhandler.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "net/http" "time" "github.com/dgrijalva/jwt-go" @@ -25,6 +26,7 @@ import ( type Database interface { GetApplicationByToken(token string) (*model.Application, error) GetUserByName(name string) (*model.User, error) + GetUserByID(id uint) (*model.User, error) } // AuthHandler is the oauth provider for authentication @@ -39,16 +41,18 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut a.db = db a.config = config + // The manager handles the tokens a.manager = manage.NewDefaultManager() a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) // TODO cubicroot add more token configs a.manager.SetPasswordTokenCfg(&manage.Config{ - AccessTokenExp: time.Duration(24) * time.Hour * 12, - RefreshTokenExp: time.Duration(24) * time.Hour * 24 * 30, + AccessTokenExp: time.Duration(24) * time.Hour, // 1 day + RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days IsGenerateRefresh: true, }) a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS256)) // TODO cubicroot get RS256 to work + // Define a storage for the tokens if a.config.Oauth.Storage == "mysql" { dbOauth, err := sqlx.Connect("mysql", a.config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings if err != nil { @@ -71,6 +75,7 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut return errors.New("Unknown oauth storage") } + // Initialize and configure the token server ginserver.InitServer(a.manager) ginserver.SetAllowGetAccessRequest(true) ginserver.SetClientInfoHandler(server.ClientFormHandler) @@ -105,3 +110,42 @@ func (a *AuthHandler) passwordAuthorizationHandler() server.PasswordAuthorizatio return fmt.Sprintf("%d", user.ID), nil } } + +// UserAuthHandler extracts user information from the query +func UserAuthHandler() server.UserAuthorizationHandler { + return func(w http.ResponseWriter, r *http.Request) (string, error) { + // TODO cubicroot check if we need a check here already + log.Println("UserAuthorizationHandler") + + return "1", nil + } +} + +// ClientScopeHandler returns a ClientScopeHandler that allows or disallows scopes for access tokens +func ClientScopeHandler() server.ClientScopeHandler { + return func(clientID, scope string) (allowed bool, err error) { + if scope == "all" || scope == "" { // For now only allow generic scopes so there is place for future expansion + return true, nil + } + + return false, nil + } +} + +// AccessTokenExpHandler returns an AccessTokenExpHandler that sets the expiration time of access tokens +func AccessTokenExpHandler() server.AccessTokenExpHandler { + return func(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { + tokenTypeRaw, ok := r.URL.Query()["token_type"] + + if ok && len(tokenTypeRaw[0]) > 0 { + tokenType := tokenTypeRaw[0] + + switch tokenType { + case "longterm", "long": + return time.Duration(24*365*2) * time.Hour, nil + } + } + + return time.Duration(24) * time.Hour, nil // TODO cubicroot -> that is not displayed correctly? + } +} diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index a0f5115..25b78a2 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -1,55 +1,32 @@ package oauth import ( - "log" - "net/http" - "time" - "github.com/gin-gonic/gin" ginserver "github.com/go-oauth2/gin-server" - "gopkg.in/oauth2.v3/server" + "gopkg.in/oauth2.v3" ) -// UserAuthHandler extracts user information from the query -func UserAuthHandler() server.UserAuthorizationHandler { - return func(w http.ResponseWriter, r *http.Request) (string, error) { - // TODO cubicroot check if we need a check here already - log.Println("UserAuthorizationHandler") - - return "1", nil - } -} - -// ClientScopeHandler returns a ClientScopeHandler that allows or disallows scopes for access tokens -func ClientScopeHandler() server.ClientScopeHandler { - return func(clientID, scope string) (allowed bool, err error) { - if scope == "all" || scope == "" { // For now only allow generic scopes so there is place for future expansion - return true, nil +// TokenInfoHandler returns a gin middleware that answers with information about a access token +func TokenInfoHandler() gin.HandlerFunc { + return func(c *gin.Context) { + data, exists := c.Get(ginserver.DefaultConfig.TokenKey) + ti, ok := data.(oauth2.TokenInfo) + resp := JSONResponse{ + Status: StatusError, + Message: "Token not found", } - return false, nil - } -} - -// AccessTokenExpHandler returns an AccessTokenExpHandler that sets the expiration time of access tokens -func AccessTokenExpHandler() server.AccessTokenExpHandler { - return func(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { - tokenTypeRaw, ok := r.URL.Query()["token_type"] - - if ok && len(tokenTypeRaw[0]) > 0 { - tokenType := tokenTypeRaw[0] - - switch tokenType { - case "longterm", "long": - return time.Duration(24*365*2) * time.Hour, nil - } + if !ok || !exists { + c.JSON(404, resp) + return } - return time.Duration(24) * time.Hour, nil // TODO cubicroot -> that is not displayed correctly? - } -} + tdi := TokenDisplayInfo{} + tdi.ReadFromTi(ti) + resp.Status = StatusSuccess + resp.Message = "" + resp.Data = tdi -func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { - log.Println("Oauth handling this here :D") - return ginserver.HandleTokenVerify() + c.JSON(200, resp) + } } diff --git a/internal/authentication/oauth/middleware.go b/internal/authentication/oauth/middleware.go new file mode 100644 index 0000000..944550b --- /dev/null +++ b/internal/authentication/oauth/middleware.go @@ -0,0 +1,51 @@ +package oauth + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" + "gopkg.in/oauth2.v3" +) + +// AuthenticationValidator returns a gin middleware for authenticating users based on a oauth access token +func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { + return ginserver.HandleTokenVerify() +} + +// UserSetter returns a gin HandlerFunc that takes the token from the AuthenticationValidator and sets the corresponding user object +func (a *AuthHandler) UserSetter() gin.HandlerFunc { + return func(ctx *gin.Context) { + var err error + ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) + if !exists { + err = errors.New("No token available") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + token, ok := ti.(oauth2.TokenInfo) + if !ok { + err = errors.New("Wrong token format") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + userID, err := strconv.ParseUint(token.GetUserID(), 10, 64) + if err != nil { + err = errors.New("User information of wrong format") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + user, err := a.db.GetUserByID(uint(userID)) + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + ctx.Set("user", user) + } +} diff --git a/internal/authentication/oauth/responses.go b/internal/authentication/oauth/responses.go new file mode 100644 index 0000000..592522f --- /dev/null +++ b/internal/authentication/oauth/responses.go @@ -0,0 +1,20 @@ +package oauth + +const ( + // StatusError is the default generic error status + StatusError ResponseStatus = "error" + // StatusSuccess is the default generic success status + StatusSuccess ResponseStatus = "success" +) + +// ResponseStatus holds the status returned by a response struct +type ResponseStatus string + +// JSONResponse holds a struct for displaying a message to a user in JSON format +type JSONResponse struct { + Status ResponseStatus `json:"status"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// TODO cubicroot remove and use HTTP status codes instead like the rest of the application does diff --git a/internal/authentication/oauth/tokendisplayinfo.go b/internal/authentication/oauth/tokendisplayinfo.go new file mode 100644 index 0000000..5b388bd --- /dev/null +++ b/internal/authentication/oauth/tokendisplayinfo.go @@ -0,0 +1,25 @@ +package oauth + +import ( + "time" + + "gopkg.in/oauth2.v3" +) + +// TokenDisplayInfo holds information about an access token but without sensitive, internal data +type TokenDisplayInfo struct { + Token string `json:"access_token"` + TokenExpires time.Time `json:"expires_at"` + TokenCreated time.Time `json:"created_at"` + UserID string `json:"user_id"` + RefreshExpires time.Time `json:"refresh_expires_at"` +} + +// ReadFromTi sets the TokenDisplayInfo with data from the TokenInfo +func (tdi *TokenDisplayInfo) ReadFromTi(ti oauth2.TokenInfo) { + tdi.Token = ti.GetAccess() + tdi.TokenExpires = ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()) + tdi.TokenCreated = ti.GetAccessCreateAt() + tdi.UserID = ti.GetUserID() + tdi.RefreshExpires = ti.GetRefreshCreateAt().Add(ti.GetRefreshExpiresIn()) +} diff --git a/internal/router/router.go b/internal/router/router.go index f4de64b..e2fd0ad 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -25,6 +25,11 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp gin.SetMode(gin.ReleaseMode) } + // Initialize gin + r := gin.Default() + r.Use(location.Default()) + + // Set up authentication and handler auth := authentication.Authenticator{ DB: db, Config: authConfig, @@ -32,14 +37,31 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp switch authConfig.Method { case "oauth": + // TODO cubicroot add a method to auth that takes the authhandler and magically sets all handlers authHandler := oauth.AuthHandler{} authHandler.Initialize(db, authConfig) auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) + auth.SetUserSetter(authHandler.UserSetter) + + // Register oauth endpoints + oauthGroup := r.Group("/oauth2") + { + oauthGroup.GET("/token", ginserver.HandleTokenRequest) + // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET + // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&username=admin&password=123" -X GET -i + // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&user_id=1&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET + oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects + // Use auth: curl "https://domain.tld/oauth2/auth?grant_type=password&client_id=000000&client_secret=999999&username=admin&password=21132&response_type=token" -X GET + oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.TokenInfoHandler()) + + // TODO cubicroot: revoking + } default: - authHandler := basicauth.Handler{ + authHandler := basicauth.AuthHandler{ DB: db, } auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) + auth.SetUserSetter(authHandler.UserSetter) } applicationHandler := api.ApplicationHandler{DB: db, DP: dp} @@ -47,36 +69,9 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp notificationHandler := api.NotificationHandler{DB: db, DP: dp} userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp} - r := gin.Default() - - r.Use(location.Default()) // Example from the library: https://github.com/go-oauth2/oauth2/blob/master/example/server/server.go // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ - if authConfig.Method == "oauth" { - - oauthGroup := r.Group("/oauth2") - { - oauthGroup.GET("/token", ginserver.HandleTokenRequest) - // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET - // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&username=admin&password=123" -X GET -i - // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&user_id=1&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET - oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects - // Use auth: curl "https://domain.tld/oauth2/auth?grant_type=password&client_id=000000&client_secret=999999&username=admin&password=21132&response_type=token" -X GET - oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), func(c *gin.Context) { - ti, exists := c.Get(ginserver.DefaultConfig.TokenKey) - if exists { - c.JSON(200, ti) - return - } - c.String(200, "not found") - }) - - // TODO cubicroot: refresh handling - // revoking - } - } - applicationGroup := r.Group("/application") applicationGroup.Use(auth.RequireValidAuthentication()) applicationGroup.Use(auth.RequireUser()) @@ -95,7 +90,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp userGroup := r.Group("/user") userGroup.Use(auth.RequireValidAuthentication()) - userGroup.Use(auth.RequireAdmin()) + userGroup.Use(auth.RequireUser()) + userGroup.Use(auth.RequireAdmin()) // TODO cubicroot: stack them so they depend on the lower level ones { userGroup.POST("", userHandler.CreateUser) userGroup.GET("", userHandler.GetUsers) From 2dce37761116cdb246674d02829877889e049dba Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 23 May 2021 13:05:44 +0200 Subject: [PATCH 09/29] get /auth to work properly --- README.md | 8 +++ internal/authentication/oauth/authhandler.go | 30 +++++--- internal/authentication/oauth/handler.go | 73 ++++++++++++++------ internal/router/router.go | 15 ++-- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d66f82b..8685397 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,14 @@ curl -u myusername:totalysecretpassword [Oauth 2](https://de.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the pushbits server. +The basic flow of oauth authentication: + +1. Authenticate user at `/oauth2/auth`, this will redirect you with an onetime authentication code as parameter +2. With this authentication code, the client id and the client secret get a new short lived access token together with a long lived refresh token at `/oauth2/token` +3. Whenever the access token expired, renew it with the refresh token + +Unfortunately this process is not very suitable for the command line, we need to extract the authentication code from the redirect url parameters. + ### Message options Messages are supporting three different syntaxes: diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index da00889..49c8571 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -15,6 +15,7 @@ import ( "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/model" + "gopkg.in/oauth2.v3" "gopkg.in/oauth2.v3/generates" "gopkg.in/oauth2.v3/manage" @@ -68,25 +69,27 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut clientStore.Create(&models.Client{ ID: "000000", Secret: "999999", - Domain: "http://localhost", + Domain: "https://localhost", // TODO cubicroot move redirect uri to settings }) } else { // TODO cubicroot add more storage options - return errors.New("Unknown oauth storage") + log.Panicln("Unknown oauth storage") } // Initialize and configure the token server ginserver.InitServer(a.manager) ginserver.SetAllowGetAccessRequest(true) ginserver.SetClientInfoHandler(server.ClientFormHandler) - ginserver.SetUserAuthorizationHandler(UserAuthHandler()) + ginserver.SetUserAuthorizationHandler(a.UserAuthHandler()) ginserver.SetPasswordAuthorizationHandler(a.passwordAuthorizationHandler()) ginserver.SetAllowedGrantType( - //oauth2.AuthorizationCode, - oauth2.PasswordCredentials, - //oauth2.ClientCredentials, + oauth2.AuthorizationCode, + //oauth2.PasswordCredentials, oauth2.Refreshing, ) + ginserver.SetAllowedResponseType( + oauth2.Code, + ) ginserver.SetClientScopeHandler(ClientScopeHandler()) ginserver.SetAccessTokenExpHandler(AccessTokenExpHandler()) return nil @@ -111,13 +114,18 @@ func (a *AuthHandler) passwordAuthorizationHandler() server.PasswordAuthorizatio } } -// UserAuthHandler extracts user information from the query -func UserAuthHandler() server.UserAuthorizationHandler { +// UserAuthHandler extracts user information from an auth request +func (a *AuthHandler) UserAuthHandler() server.UserAuthorizationHandler { return func(w http.ResponseWriter, r *http.Request) (string, error) { - // TODO cubicroot check if we need a check here already - log.Println("UserAuthorizationHandler") + username := r.URL.Query().Get("username") + password := r.URL.Query().Get("password") - return "1", nil + if user, err := a.db.GetUserByName(username); err != nil { + return "", err + } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return fmt.Sprint(user.ID), nil + } + return "", errors.New("No credentials provided") } } diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 25b78a2..869b126 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -1,32 +1,61 @@ package oauth import ( + "errors" + "log" + "net/http" + "github.com/gin-gonic/gin" ginserver "github.com/go-oauth2/gin-server" "gopkg.in/oauth2.v3" ) -// TokenInfoHandler returns a gin middleware that answers with information about a access token -func TokenInfoHandler() gin.HandlerFunc { - return func(c *gin.Context) { - data, exists := c.Get(ginserver.DefaultConfig.TokenKey) - ti, ok := data.(oauth2.TokenInfo) - resp := JSONResponse{ - Status: StatusError, - Message: "Token not found", - } - - if !ok || !exists { - c.JSON(404, resp) - return - } - - tdi := TokenDisplayInfo{} - tdi.ReadFromTi(ti) - resp.Status = StatusSuccess - resp.Message = "" - resp.Data = tdi - - c.JSON(200, resp) +// RevokeAccessRequest holds data required in a revoke request +type RevokeAccessRequest struct { + Access string `json:"access_token"` +} + +// GetTokenInfo answers with information about a access token +func GetTokenInfo(c *gin.Context) { + data, exists := c.Get(ginserver.DefaultConfig.TokenKey) + ti, ok := data.(oauth2.TokenInfo) + resp := JSONResponse{ + Status: StatusError, + Message: "Token not found", + } + + if !ok || !exists { + c.JSON(404, resp) + return + } + + tdi := TokenDisplayInfo{} + tdi.ReadFromTi(ti) + resp.Status = StatusSuccess + resp.Message = "" + resp.Data = tdi + + c.JSON(200, resp) +} + +// RevokeAccess revokes an access token +func (a *AuthHandler) RevokeAccess(c *gin.Context) { + var request RevokeAccessRequest + + err := c.BindJSON(&request) + if err != nil { + log.Println("Error when reading request.") + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("Missing access_token")) + return } + + err = a.manager.RemoveAccessToken(request.Access) + if err != nil { + log.Println(err) + log.Println("Error when revoking") + c.AbortWithError(http.StatusNotFound, errors.New("Unknown access token")) + return + } + + c.JSON(200, request) } diff --git a/internal/router/router.go b/internal/router/router.go index e2fd0ad..768c7be 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -37,7 +37,6 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp switch authConfig.Method { case "oauth": - // TODO cubicroot add a method to auth that takes the authhandler and magically sets all handlers authHandler := oauth.AuthHandler{} authHandler.Initialize(db, authConfig) auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) @@ -49,12 +48,13 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp oauthGroup.GET("/token", ginserver.HandleTokenRequest) // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&username=admin&password=123" -X GET -i - // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&user_id=1&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET + // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET + // GET TOKEN with code: curl "https://domain.tld/oauth2/token?grant_type=authorization_code&client_id=000000&client_secret=999999&code=4T1TJXMBPTOS4NNGILBDYW&redirect_uri=localhost" -X GET -i oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects - // Use auth: curl "https://domain.tld/oauth2/auth?grant_type=password&client_id=000000&client_secret=999999&username=admin&password=21132&response_type=token" -X GET - oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.TokenInfoHandler()) - - // TODO cubicroot: revoking + // Use auth: curl "https://domain.tld/oauth2/authclient_id=000000&username=admin&password=21132&response_type=token" -X GET + oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) + // curl "https://domain.tld/oauth2/revoke" -X POST -i -H "Authorization: Bearer $token" -d '{"access_token": "$revoke_token"}' + oauthGroup.POST("/revoke", auth.RequireValidAuthentication(), auth.RequireUser(), auth.RequireAdmin(), authHandler.RevokeAccess) } default: authHandler := basicauth.AuthHandler{ @@ -69,9 +69,6 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp notificationHandler := api.NotificationHandler{DB: db, DP: dp} userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp} - // Example from the library: https://github.com/go-oauth2/oauth2/blob/master/example/server/server.go - // Good Tutorial: https://tutorialedge.net/golang/go-oauth2-tutorial/ - applicationGroup := r.Group("/application") applicationGroup.Use(auth.RequireValidAuthentication()) applicationGroup.Use(auth.RequireUser()) From e78ffda3d3996b4c9973462d5f2b68da371e1395 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 23 May 2021 13:50:15 +0200 Subject: [PATCH 10/29] stack authentication layers & add documentation --- README.md | 89 +++++++++++++++++-- internal/authentication/authentication.go | 18 ++-- .../authentication/basicauth/authhandler.go | 21 +++-- internal/authentication/oauth/authhandler.go | 2 +- internal/router/router.go | 16 ++-- 5 files changed, 118 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8685397..fd95f79 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,92 @@ curl -u myusername:totalysecretpassword [Oauth 2](https://de.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the pushbits server. -The basic flow of oauth authentication: +##### Authenticating -1. Authenticate user at `/oauth2/auth`, this will redirect you with an onetime authentication code as parameter -2. With this authentication code, the client id and the client secret get a new short lived access token together with a long lived refresh token at `/oauth2/token` -3. Whenever the access token expired, renew it with the refresh token +For authentication use the ``/oauth2/auth` endpoint. E.g.: -Unfortunately this process is not very suitable for the command line, we need to extract the authentication code from the redirect url parameters. +```bash +curl \ + --header "Content-Type: application/json" \ + --request GET \ + "https://pushbits.example.com/oauth2/auth?client_id=000000&username=admin&password=1233456&response_type=code&redirect_uri=https://myapp.example.com" +``` + +This will return a HTTP redirect with the status code `302` and an authentication code set as parameter: + +``` +HTTP/2 302 +date: Sun, 23 May 2021 10:33:27 GMT +location: https://myapp.example.com?code=4T1TJXMBPTOS4NNGILBDYW +content-length: 0 +``` + +Your app then needs to use this code to trade it for a access token. + +**Hint for command line users:** you can extract the authentication code from the redirect without the need of a running webserver. + +##### Receiving a access token + +You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. Pushbits currently supports the following one's: + +* Refresh +* Authentication code + +Oauth2 authentication is based on "clients", thus you need to provide identifieres for a client with your request. These are the `client_id` and the `client_secret`. + +For your first token you will need a authentication code, see the section above. Then use it like this: + +```bash +curl \ + --header "Content-Type: application/json" \ + --request GET \ + "https://pushbits.example.com/oauth2/token?grant_type=authorization_code&client_id=000000&client_secret=49gjg4js9&response_type=token&redirect_uri=https://myapp.example.com&code=OP1Q2UJEVL-RPR9GZAUURA" +``` + +This will then return an access token and refresh token for you. + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ", + "expires_in": 86400, + "refresh_token": "OP1Q2UJEVL-RPR9GZAUURA", + "token_type": "Bearer" +} +``` + +The access token is short lived, the refresh token is long lived, but can not be used for authentication. If your access token runs out, you can use the refresh token to generate a new access token: + +```bash +curl \ + --header "Content-Type: application/json" \ + --request GET \ + "https://pushbits.example.com/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=49gjg4js9&response_type=token&refresh_token=OP1Q2UJEVL-RPR9GZAUURA" +``` + +##### Getting information about a access token + +With a valid access token you can get information about it from `/oauth/tokeninfo`. This is ment for testing if a token is issued correctly. + +```bash +curl \ + --header "Content-Type: application/json" \ + --request GET \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + "https://pushbits.example.com/oauth2/tokeninfo" +``` + +##### Revoking a token + +Admin users are eligable to revoke tokens. This should not be necessary in normal operation, as tokens are only short lived. But there might be situations where attackers might have gotten knowledge about a token. + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --data '{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NDg1MDYsInN1YiI6IjEifQ.cO0_8fqsJDG4KswjC0CSzc_EznntH-FDQejdolPAISo"}' + "https://pushbits.example.com/oauth2/revoke" +``` ### Message options diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index 76bb60a..be4316d 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -109,15 +109,23 @@ func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc } // RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request. -func (a *Authenticator) RequireUser() gin.HandlerFunc { - return a.UserSetter() +func (a *Authenticator) RequireUser() []gin.HandlerFunc { + funcs := make([]gin.HandlerFunc, 0) + funcs = append(funcs, a.RequireValidAuthentication()) + funcs = append(funcs, a.UserSetter()) + return funcs } // RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request. -func (a *Authenticator) RequireAdmin() gin.HandlerFunc { - return a.requireUserProperty(func(user *model.User) bool { +func (a *Authenticator) RequireAdmin() []gin.HandlerFunc { + funcs := make([]gin.HandlerFunc, 0) + funcs = append(funcs, a.RequireValidAuthentication()) + funcs = append(funcs, a.UserSetter()) + funcs = append(funcs, a.requireUserProperty(func(user *model.User) bool { return user.IsAdmin - }) + })) + + return funcs } func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string { diff --git a/internal/authentication/basicauth/authhandler.go b/internal/authentication/basicauth/authhandler.go index 31f0a98..f3c3862 100644 --- a/internal/authentication/basicauth/authhandler.go +++ b/internal/authentication/basicauth/authhandler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/model" ) @@ -17,16 +18,22 @@ type Database interface { // AuthHandler is the basic auth provider for authentication type AuthHandler struct { - DB Database + db Database +} + +// Initialize prepares the AuthHandler +func (a *AuthHandler) Initialize(db *database.Database) error { + a.db = db + return nil } // AuthenticationValidator returns a gin HandlerFunc that takes the basic auth credentials and validates them -func (h *AuthHandler) AuthenticationValidator() gin.HandlerFunc { +func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User err := errors.New("No authentication method") - user, err = h.userFromBasicAuth(ctx) + user, err = a.userFromBasicAuth(ctx) if err != nil { ctx.AbortWithError(http.StatusForbidden, err) @@ -41,12 +48,12 @@ func (h *AuthHandler) AuthenticationValidator() gin.HandlerFunc { } // UserSetter returns a gin HandlerFunc that takes the basic auth credentials and sets the corresponding user object -func (h *AuthHandler) UserSetter() gin.HandlerFunc { +func (a *AuthHandler) UserSetter() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User err := errors.New("No authentication method") - user, err = h.userFromBasicAuth(ctx) + user, err = a.userFromBasicAuth(ctx) if err != nil { ctx.AbortWithError(http.StatusForbidden, err) @@ -62,9 +69,9 @@ func (h *AuthHandler) UserSetter() gin.HandlerFunc { } } -func (h *AuthHandler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { +func (a *AuthHandler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { if name, password, ok := ctx.Request.BasicAuth(); ok { - if user, err := h.DB.GetUserByName(name); err != nil { + if user, err := a.db.GetUserByName(name); err != nil { return nil, err } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { return user, nil diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 49c8571..e3d3f6c 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -46,7 +46,7 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut a.manager = manage.NewDefaultManager() a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) // TODO cubicroot add more token configs - a.manager.SetPasswordTokenCfg(&manage.Config{ + a.manager.SetAuthorizeCodeTokenCfg(&manage.Config{ AccessTokenExp: time.Duration(24) * time.Hour, // 1 day RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days IsGenerateRefresh: true, diff --git a/internal/router/router.go b/internal/router/router.go index 768c7be..5525b56 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -51,15 +51,14 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET // GET TOKEN with code: curl "https://domain.tld/oauth2/token?grant_type=authorization_code&client_id=000000&client_secret=999999&code=4T1TJXMBPTOS4NNGILBDYW&redirect_uri=localhost" -X GET -i oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects - // Use auth: curl "https://domain.tld/oauth2/authclient_id=000000&username=admin&password=21132&response_type=token" -X GET + // Use auth: curl "https://domain.tld/oauth2/auth?client_id=000000&username=admin&password=21132&response_type=token" -X GET oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) // curl "https://domain.tld/oauth2/revoke" -X POST -i -H "Authorization: Bearer $token" -d '{"access_token": "$revoke_token"}' - oauthGroup.POST("/revoke", auth.RequireValidAuthentication(), auth.RequireUser(), auth.RequireAdmin(), authHandler.RevokeAccess) + oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) } default: - authHandler := basicauth.AuthHandler{ - DB: db, - } + authHandler := basicauth.AuthHandler{} + authHandler.Initialize(db) auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) auth.SetUserSetter(authHandler.UserSetter) } @@ -70,8 +69,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp} applicationGroup := r.Group("/application") - applicationGroup.Use(auth.RequireValidAuthentication()) - applicationGroup.Use(auth.RequireUser()) + applicationGroup.Use(auth.RequireUser()...) { applicationGroup.POST("", applicationHandler.CreateApplication) applicationGroup.GET("", applicationHandler.GetApplications) @@ -86,9 +84,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification) userGroup := r.Group("/user") - userGroup.Use(auth.RequireValidAuthentication()) - userGroup.Use(auth.RequireUser()) - userGroup.Use(auth.RequireAdmin()) // TODO cubicroot: stack them so they depend on the lower level ones + userGroup.Use(auth.RequireAdmin()...) { userGroup.POST("", userHandler.CreateUser) userGroup.GET("", userHandler.GetUsers) From f60bb83859779097d5aedc1f5597deea2ba011d6 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 23 May 2021 13:52:26 +0200 Subject: [PATCH 11/29] add refresh config --- internal/authentication/oauth/authhandler.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index e3d3f6c..75ec810 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -51,6 +51,14 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days IsGenerateRefresh: true, }) + a.manager.SetRefreshTokenCfg(&manage.RefreshingConfig{ + AccessTokenExp: time.Duration(24) * time.Hour, // 1 day + RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days + IsGenerateRefresh: true, + IsResetRefreshTime: true, + IsRemoveAccess: false, + IsRemoveRefreshing: true, + }) a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS256)) // TODO cubicroot get RS256 to work // Define a storage for the tokens From 46027e2a3631e0c0cebc572461f2aec741eac73e Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 23 May 2021 16:37:56 +0200 Subject: [PATCH 12/29] clean up & add file support --- config.example.yml | 14 +++++++-- internal/authentication/oauth/authhandler.go | 30 +++++++++++++------- internal/authentication/oauth/handler.go | 11 ++----- internal/authentication/oauth/responses.go | 20 ------------- internal/configuration/configuration.go | 7 +++-- 5 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 internal/authentication/oauth/responses.go diff --git a/config.example.yml b/config.example.yml index 4efc69a..37742f1 100644 --- a/config.example.yml +++ b/config.example.yml @@ -62,10 +62,18 @@ formatting: coloredtitle: false authentication: - # The authentication method to use + # The authentication method to use. method: basic + # Only needed if you choose oauth method. oauth: - # The storage used for tokens + # The storage used for tokens. storage: "file" - # The connection to the storage + # The connection to the storage. For file: the filename, ending with .db. For mysql: the connection string. Check out + # https://github.com/go-sql-driver/mysql#dsn-data-source-name for details. connection: "pushbits_token.db" + # Oauth client identifier (random string). + clientid: "03MG7MZJFF4566HNSW" + # Oauth client secret. + clientsecret: "fl#djhglj#daigert054w_342" + # Oauth redirect url after successful auth. Can be overwritten in the authentication request. + clientredirect: "http://localhost" diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 75ec810..78567ff 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -21,6 +21,7 @@ import ( "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/models" "gopkg.in/oauth2.v3/server" + "gopkg.in/oauth2.v3/store" ) // The Database interface for encapsulating database access. @@ -45,7 +46,6 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut // The manager handles the tokens a.manager = manage.NewDefaultManager() a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) - // TODO cubicroot add more token configs a.manager.SetAuthorizeCodeTokenCfg(&manage.Config{ AccessTokenExp: time.Duration(24) * time.Hour, // 1 day RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days @@ -59,11 +59,12 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut IsRemoveAccess: false, IsRemoveRefreshing: true, }) - a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS256)) // TODO cubicroot get RS256 to work + a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS512)) // unfortunately only symmetric algorithms seem to be supported // Define a storage for the tokens - if a.config.Oauth.Storage == "mysql" { - dbOauth, err := sqlx.Connect("mysql", a.config.Oauth.Connection+"?parseTime=true") // TODO cubicroot add more options and move to settings + switch a.config.Oauth.Storage { + case "mysql": + dbOauth, err := sqlx.Connect("mysql", a.config.Oauth.Connection+"?parseTime=true") if err != nil { log.Fatal(err) } @@ -73,17 +74,24 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) a.manager.MapClientStorage(clientStore) - // TODO cubicroot better only store the secret as hashed value and autogenerate? clientStore.Create(&models.Client{ - ID: "000000", - Secret: "999999", - Domain: "https://localhost", // TODO cubicroot move redirect uri to settings + ID: a.config.Oauth.ClientID, + Secret: a.config.Oauth.ClientSecret, + Domain: a.config.Oauth.ClientRedirect, }) - } else { - // TODO cubicroot add more storage options + case "file": + // TODO cubicroot test it better :D + a.manager.MustTokenStorage(store.NewFileTokenStore(a.config.Oauth.Connection)) + clientStore := store.NewClientStore() // memory store + a.manager.MapClientStorage(clientStore) + clientStore.Set(a.config.Oauth.ClientID, &models.Client{ + ID: a.config.Oauth.ClientID, + Secret: a.config.Oauth.ClientSecret, + Domain: a.config.Oauth.ClientRedirect, + }) + default: log.Panicln("Unknown oauth storage") } - // Initialize and configure the token server ginserver.InitServer(a.manager) ginserver.SetAllowGetAccessRequest(true) diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 869b126..d197457 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -19,23 +19,16 @@ type RevokeAccessRequest struct { func GetTokenInfo(c *gin.Context) { data, exists := c.Get(ginserver.DefaultConfig.TokenKey) ti, ok := data.(oauth2.TokenInfo) - resp := JSONResponse{ - Status: StatusError, - Message: "Token not found", - } if !ok || !exists { - c.JSON(404, resp) + c.String(404, "Token not found") return } tdi := TokenDisplayInfo{} tdi.ReadFromTi(ti) - resp.Status = StatusSuccess - resp.Message = "" - resp.Data = tdi - c.JSON(200, resp) + c.JSON(200, tdi) } // RevokeAccess revokes an access token diff --git a/internal/authentication/oauth/responses.go b/internal/authentication/oauth/responses.go deleted file mode 100644 index 592522f..0000000 --- a/internal/authentication/oauth/responses.go +++ /dev/null @@ -1,20 +0,0 @@ -package oauth - -const ( - // StatusError is the default generic error status - StatusError ResponseStatus = "error" - // StatusSuccess is the default generic success status - StatusSuccess ResponseStatus = "success" -) - -// ResponseStatus holds the status returned by a response struct -type ResponseStatus string - -// JSONResponse holds a struct for displaying a message to a user in JSON format -type JSONResponse struct { - Status ResponseStatus `json:"status"` - Message string `json:"message"` - Data interface{} `json:"data"` -} - -// TODO cubicroot remove and use HTTP status codes instead like the rest of the application does diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 6c4e563..230e1a6 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -31,8 +31,11 @@ type Authentication struct { // Oauth holds information about the oauth server type Oauth struct { - Connection string `default:""` - Storage string `default:"file"` + Connection string `default:"pushbits_token.db"` + Storage string `default:"file"` + ClientID string `default:"000000"` + ClientSecret string `default:"123456"` + ClientRedirect string `default:"http://localhost"` } // Configuration holds values that can be configured by the user. From fc6bc82f405643a16ca1c35f1df7357d76f27cca Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 30 May 2021 12:48:59 +0200 Subject: [PATCH 13/29] move token key to config & clean up --- README.md | 4 ++- config.example.yml | 6 +++-- internal/authentication/oauth/authhandler.go | 27 +++++++++++--------- internal/configuration/configuration.go | 1 + internal/router/router.go | 8 +----- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fd95f79..de5b74b 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ curl -u myusername:totalysecretpassword [Oauth 2](https://de.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the pushbits server. +Make sure to setup the "oauth" section in the config file correctly. + ##### Authenticating For authentication use the ``/oauth2/auth` endpoint. E.g.: @@ -168,7 +170,7 @@ Your app then needs to use this code to trade it for a access token. **Hint for command line users:** you can extract the authentication code from the redirect without the need of a running webserver. -##### Receiving a access token +##### Receiving an access token You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. Pushbits currently supports the following one's: diff --git a/config.example.yml b/config.example.yml index 37742f1..ecafffb 100644 --- a/config.example.yml +++ b/config.example.yml @@ -72,8 +72,10 @@ authentication: # https://github.com/go-sql-driver/mysql#dsn-data-source-name for details. connection: "pushbits_token.db" # Oauth client identifier (random string). - clientid: "03MG7MZJFF4566HNSW" + clientid: "000000" # Oauth client secret. - clientsecret: "fl#djhglj#daigert054w_342" + clientsecret: "123456" # Oauth redirect url after successful auth. Can be overwritten in the authentication request. clientredirect: "http://localhost" + # Key used for signing the access tokens (JWT with HS512) + tokenkey: "123456" diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 78567ff..ccf657c 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -17,6 +17,7 @@ import ( "github.com/pushbits/server/internal/model" "gopkg.in/oauth2.v3" + oauth_error "gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/generates" "gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/models" @@ -59,7 +60,7 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut IsRemoveAccess: false, IsRemoveRefreshing: true, }) - a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte("dfhgdfhfg"), jwt.SigningMethodHS512)) // unfortunately only symmetric algorithms seem to be supported + a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte(a.config.Oauth.TokenKey), jwt.SigningMethodHS512)) // unfortunately only symmetric algorithms seem to be supported // Define a storage for the tokens switch a.config.Oauth.Storage { @@ -80,7 +81,6 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut Domain: a.config.Oauth.ClientRedirect, }) case "file": - // TODO cubicroot test it better :D a.manager.MustTokenStorage(store.NewFileTokenStore(a.config.Oauth.Connection)) clientStore := store.NewClientStore() // memory store a.manager.MapClientStorage(clientStore) @@ -98,6 +98,7 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut ginserver.SetClientInfoHandler(server.ClientFormHandler) ginserver.SetUserAuthorizationHandler(a.UserAuthHandler()) ginserver.SetPasswordAuthorizationHandler(a.passwordAuthorizationHandler()) + ginserver.SetInternalErrorHandler(a.InternalErrorHandler()) ginserver.SetAllowedGrantType( oauth2.AuthorizationCode, //oauth2.PasswordCredentials, @@ -159,17 +160,19 @@ func ClientScopeHandler() server.ClientScopeHandler { // AccessTokenExpHandler returns an AccessTokenExpHandler that sets the expiration time of access tokens func AccessTokenExpHandler() server.AccessTokenExpHandler { return func(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { - tokenTypeRaw, ok := r.URL.Query()["token_type"] - - if ok && len(tokenTypeRaw[0]) > 0 { - tokenType := tokenTypeRaw[0] + return time.Duration(24) * time.Hour, nil + } +} - switch tokenType { - case "longterm", "long": - return time.Duration(24*365*2) * time.Hour, nil - } - } +// InternalErrorHandler handles errors for authentication, it will always return a server_error +func (a *AuthHandler) InternalErrorHandler() server.InternalErrorHandler { + return func(err error) *oauth_error.Response { + var re oauth_error.Response + log.Println(err) - return time.Duration(24) * time.Hour, nil // TODO cubicroot -> that is not displayed correctly? + re.Error = oauth_error.ErrServerError + re.Description = oauth_error.Descriptions[oauth_error.ErrServerError] + re.StatusCode = oauth_error.StatusCodes[oauth_error.ErrServerError] + return &re } } diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 230e1a6..2ca46c7 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -36,6 +36,7 @@ type Oauth struct { ClientID string `default:"000000"` ClientSecret string `default:"123456"` ClientRedirect string `default:"http://localhost"` + TokenKey string `default:"123456"` } // Configuration holds values that can be configured by the user. diff --git a/internal/router/router.go b/internal/router/router.go index 5525b56..93ab726 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -46,14 +46,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp oauthGroup := r.Group("/oauth2") { oauthGroup.GET("/token", ginserver.HandleTokenRequest) - // GET TOKEN with client: curl "https://domain.tld/oauth2/token?grant_type=client_credentials&client_id=000000&client_secret=999999&scope=read" -X GET - // GET TOKEN with password: curl "https://domain.tld/oauth2/token?grant_type=password&client_id=000000&client_secret=999999&scope=read&username=admin&password=123" -X GET -i - // GET TOKEN with refresh token: curl "https://domain.tld/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=999999&refresh_token=OKLLQOOLWP2IFVFBLJVIAA" -X GET - // GET TOKEN with code: curl "https://domain.tld/oauth2/token?grant_type=authorization_code&client_id=000000&client_secret=999999&code=4T1TJXMBPTOS4NNGILBDYW&redirect_uri=localhost" -X GET -i - oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) // Not very convenient for cli tools as it uses redirects - // Use auth: curl "https://domain.tld/oauth2/auth?client_id=000000&username=admin&password=21132&response_type=token" -X GET + oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) - // curl "https://domain.tld/oauth2/revoke" -X POST -i -H "Authorization: Bearer $token" -d '{"access_token": "$revoke_token"}' oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) } default: From 393d58698a1956df75d9c30a88ce0625874b060a Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 30 May 2021 12:55:47 +0200 Subject: [PATCH 14/29] removed replaced code --- internal/authentication/authentication.go | 42 ----------------------- 1 file changed, 42 deletions(-) diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index be4316d..e4b7ce3 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -4,13 +4,9 @@ import ( "errors" "log" "net/http" - "strconv" - ginserver "github.com/go-oauth2/gin-server" - "github.com/pushbits/server/internal/authentication/credentials" "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/model" - "gopkg.in/oauth2.v3" "github.com/gin-gonic/gin" ) @@ -43,44 +39,6 @@ type Authenticator struct { type hasUserProperty func(user *model.User) bool -func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { - if name, password, ok := ctx.Request.BasicAuth(); ok { - if user, err := a.DB.GetUserByName(name); err != nil { - return nil, err - } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { - return user, nil - } else { - return nil, errors.New("credentials were invalid") - } - } - - return nil, errors.New("no credentials were supplied 1") -} - -func (a *Authenticator) userFromToken(ctx *gin.Context) (*model.User, error) { - ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) - if !exists { - return nil, errors.New("No token available") - } - - token, ok := ti.(oauth2.TokenInfo) - if !ok { - return nil, errors.New("Wrong token format") - } - - userID, err := strconv.ParseUint(token.GetUserID(), 10, 64) - if err != nil { - return nil, errors.New("User information of wrong format") - } - - user, err := a.DB.GetUserByID(uint(userID)) - if err != nil { - return nil, err - } - - return user, nil -} - func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc { return func(ctx *gin.Context) { err := errors.New("User not found") From cee8001e7ce08b434038d493b9e9595c72883bc6 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Wed, 2 Jun 2021 18:19:55 +0200 Subject: [PATCH 15/29] clean up errors --- internal/authentication/basicauth/authhandler.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/authentication/basicauth/authhandler.go b/internal/authentication/basicauth/authhandler.go index f3c3862..d682860 100644 --- a/internal/authentication/basicauth/authhandler.go +++ b/internal/authentication/basicauth/authhandler.go @@ -31,9 +31,7 @@ func (a *AuthHandler) Initialize(db *database.Database) error { func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User - err := errors.New("No authentication method") - - user, err = a.userFromBasicAuth(ctx) + user, err := a.userFromBasicAuth(ctx) if err != nil { ctx.AbortWithError(http.StatusForbidden, err) @@ -51,9 +49,7 @@ func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { func (a *AuthHandler) UserSetter() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User - err := errors.New("No authentication method") - - user, err = a.userFromBasicAuth(ctx) + user, err := a.userFromBasicAuth(ctx) if err != nil { ctx.AbortWithError(http.StatusForbidden, err) From 0504528e5a39eb1e1db0e0f4e52c2880d10394b8 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 3 Jun 2021 11:18:43 +0200 Subject: [PATCH 16/29] wording & consistency --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index de5b74b..1402470 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ pbcli application show $PB_APPLICATION --url https://pushbits.example.com --user Pushbits offers you two methods of authenticating against the server: * Basic authentication (`basic`) -* Oauth 2 (`oauth`) +* [Oauth 2.0](https://oauth.net/2/) (`oauth`) You will find the corresponding setting in the security section. @@ -134,15 +134,15 @@ security: #### Basic authentication -For [basic authentication](https://de.wikipedia.org/wiki/HTTP-Authentifizierung) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag: +For [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag: ```bash -curl -u myusername:totalysecretpassword +curl -u myusername:totallysecretpassword ``` -#### Oauth 2 +#### Oauth 2.0 -[Oauth 2](https://de.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the pushbits server. +[Oauth 2.0](https://en.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the PushBits server. Make sure to setup the "oauth" section in the config file correctly. @@ -172,12 +172,12 @@ Your app then needs to use this code to trade it for a access token. ##### Receiving an access token -You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. Pushbits currently supports the following one's: +You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. PushBits currently supports the following one's: * Refresh * Authentication code -Oauth2 authentication is based on "clients", thus you need to provide identifieres for a client with your request. These are the `client_id` and the `client_secret`. +Oauth 2.0 authentication is based on "clients", thus you need to provide identifieres for a client with your request. These are the `client_id` and the `client_secret`. For your first token you will need a authentication code, see the section above. Then use it like this: @@ -222,7 +222,7 @@ curl \ ##### Revoking a token -Admin users are eligable to revoke tokens. This should not be necessary in normal operation, as tokens are only short lived. But there might be situations where attackers might have gotten knowledge about a token. +Admin users are eligible to revoke tokens. This should not be necessary in normal operation, as tokens are only short lived. But there might be situations where attackers might have gotten knowledge about a token. ```bash curl \ From 364e5224f875ed90e1835ef39b0a23345bab38f7 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 3 Jun 2021 11:51:11 +0200 Subject: [PATCH 17/29] panic when no oauth secrets are set --- README.md | 4 ++-- config.example.yml | 8 ++++---- internal/authentication/oauth/authhandler.go | 6 ++++++ internal/configuration/configuration.go | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1402470..aa8ef6e 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ You can get an access token from the `/oauth/token` endpoint. There are several * Refresh * Authentication code -Oauth 2.0 authentication is based on "clients", thus you need to provide identifieres for a client with your request. These are the `client_id` and the `client_secret`. +Oauth 2.0 authentication is based on "clients", thus you need to provide identifiers for a client with your request. These are the `client_id` and the `client_secret`. For your first token you will need a authentication code, see the section above. Then use it like this: @@ -210,7 +210,7 @@ curl \ ##### Getting information about a access token -With a valid access token you can get information about it from `/oauth/tokeninfo`. This is ment for testing if a token is issued correctly. +With a valid access token you can get information about it from `/oauth/tokeninfo`. This is meant for testing if a token is issued correctly. ```bash curl \ diff --git a/config.example.yml b/config.example.yml index ecafffb..025b093 100644 --- a/config.example.yml +++ b/config.example.yml @@ -73,9 +73,9 @@ authentication: connection: "pushbits_token.db" # Oauth client identifier (random string). clientid: "000000" - # Oauth client secret. - clientsecret: "123456" + # Oauth client secret (random string). + clientsecret: "" # Oauth redirect url after successful auth. Can be overwritten in the authentication request. clientredirect: "http://localhost" - # Key used for signing the access tokens (JWT with HS512) - tokenkey: "123456" + # Key used for signing (JWT with HS512) the access tokens (random string). + tokenkey: "" diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index ccf657c..04ee30d 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -44,6 +44,12 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut a.db = db a.config = config + if len(a.config.Oauth.ClientSecret) < 5 { + panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") + } else if len(a.config.Oauth.TokenKey) < 5 { + panic("Your Oauth 2.0 token key is empty or not long enough to be secure. Please change it in the configuration file.") + } + // The manager handles the tokens a.manager = manage.NewDefaultManager() a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 2ca46c7..74b35b5 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -34,9 +34,9 @@ type Oauth struct { Connection string `default:"pushbits_token.db"` Storage string `default:"file"` ClientID string `default:"000000"` - ClientSecret string `default:"123456"` + ClientSecret string `default:""` ClientRedirect string `default:"http://localhost"` - TokenKey string `default:"123456"` + TokenKey string `default:""` } // Configuration holds values that can be configured by the user. From a06fe242dbdb78781ae97d48ae054e1c950538e8 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 3 Jun 2021 12:01:35 +0200 Subject: [PATCH 18/29] clean up & consistency --- internal/authentication/authentication.go | 6 +++--- internal/authentication/oauth/handler.go | 13 +++++++++---- internal/router/router.go | 4 +++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index e4b7ce3..6a8c791 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -39,7 +39,7 @@ type Authenticator struct { type hasUserProperty func(user *model.User) bool -func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc { +func (a *Authenticator) requireUserProperty(has hasUserProperty, errorMessage string) gin.HandlerFunc { return func(ctx *gin.Context) { err := errors.New("User not found") @@ -60,7 +60,7 @@ func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc } if !has(user) { - ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + ctx.AbortWithError(http.StatusForbidden, errors.New(errorMessage)) return } } @@ -81,7 +81,7 @@ func (a *Authenticator) RequireAdmin() []gin.HandlerFunc { funcs = append(funcs, a.UserSetter()) funcs = append(funcs, a.requireUserProperty(func(user *model.User) bool { return user.IsAdmin - })) + }, "User does not have permission: admin")) return funcs } diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index d197457..213fdc0 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -18,10 +18,16 @@ type RevokeAccessRequest struct { // GetTokenInfo answers with information about a access token func GetTokenInfo(c *gin.Context) { data, exists := c.Get(ginserver.DefaultConfig.TokenKey) - ti, ok := data.(oauth2.TokenInfo) + if !exists { + err := errors.New("Token not found") + c.AbortWithError(http.StatusNotFound, err) + return + } + ti, ok := data.(oauth2.TokenInfo) if !ok || !exists { - c.String(404, "Token not found") + err := errors.New("Token not found") + c.AbortWithError(http.StatusNotFound, err) return } @@ -44,8 +50,7 @@ func (a *AuthHandler) RevokeAccess(c *gin.Context) { err = a.manager.RemoveAccessToken(request.Access) if err != nil { - log.Println(err) - log.Println("Error when revoking") + log.Println("Error when revoking: ", err) c.AbortWithError(http.StatusNotFound, errors.New("Unknown access token")) return } diff --git a/internal/router/router.go b/internal/router/router.go index 93ab726..c544949 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -50,11 +50,13 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) } - default: + case "basic": authHandler := basicauth.AuthHandler{} authHandler.Initialize(db) auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) auth.SetUserSetter(authHandler.UserSetter) + default: + panic("Unknown authentication method set. Please use one of basic, oauth.") } applicationHandler := api.ApplicationHandler{DB: db, DP: dp} From 1e2114b430a6e3ec3d0a8d8917def6532c86dee5 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 3 Jun 2021 14:48:20 +0200 Subject: [PATCH 19/29] register auth handler --- internal/authentication/authentication.go | 18 ++++++++++-------- .../authentication/basicauth/authhandler.go | 4 ++-- internal/authentication/oauth/middleware.go | 4 ++-- internal/router/router.go | 6 ++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index 6a8c791..036d20f 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -22,6 +22,12 @@ type ( UserSetter func() gin.HandlerFunc ) +// AuthHandler defines the minimal interface for an auth handler +type AuthHandler interface { + AuthenticationValidator() gin.HandlerFunc + UserSetter() gin.HandlerFunc +} + // The Database interface for encapsulating database access. type Database interface { GetApplicationByToken(token string) (*model.Application, error) @@ -124,12 +130,8 @@ func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc { return a.AuthenticationValidator() } -// SetAuthenticationValidator sets a function for handling authentication -func (a *Authenticator) SetAuthenticationValidator(f AuthenticationValidator) { - a.AuthenticationValidator = f -} - -// SetUserSetter sets a function that sets the user object in gin context -func (a *Authenticator) SetUserSetter(f UserSetter) { - a.UserSetter = f +// RegisterHandler registers an authentication handler +func (a *Authenticator) RegisterHandler(handler AuthHandler) { + a.UserSetter = handler.UserSetter + a.AuthenticationValidator = handler.AuthenticationValidator } diff --git a/internal/authentication/basicauth/authhandler.go b/internal/authentication/basicauth/authhandler.go index d682860..ff2f415 100644 --- a/internal/authentication/basicauth/authhandler.go +++ b/internal/authentication/basicauth/authhandler.go @@ -28,7 +28,7 @@ func (a *AuthHandler) Initialize(db *database.Database) error { } // AuthenticationValidator returns a gin HandlerFunc that takes the basic auth credentials and validates them -func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { +func (a AuthHandler) AuthenticationValidator() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User user, err := a.userFromBasicAuth(ctx) @@ -46,7 +46,7 @@ func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { } // UserSetter returns a gin HandlerFunc that takes the basic auth credentials and sets the corresponding user object -func (a *AuthHandler) UserSetter() gin.HandlerFunc { +func (a AuthHandler) UserSetter() gin.HandlerFunc { return func(ctx *gin.Context) { var user *model.User user, err := a.userFromBasicAuth(ctx) diff --git a/internal/authentication/oauth/middleware.go b/internal/authentication/oauth/middleware.go index 944550b..76b5f6b 100644 --- a/internal/authentication/oauth/middleware.go +++ b/internal/authentication/oauth/middleware.go @@ -11,12 +11,12 @@ import ( ) // AuthenticationValidator returns a gin middleware for authenticating users based on a oauth access token -func (a *AuthHandler) AuthenticationValidator() gin.HandlerFunc { +func (a AuthHandler) AuthenticationValidator() gin.HandlerFunc { return ginserver.HandleTokenVerify() } // UserSetter returns a gin HandlerFunc that takes the token from the AuthenticationValidator and sets the corresponding user object -func (a *AuthHandler) UserSetter() gin.HandlerFunc { +func (a AuthHandler) UserSetter() gin.HandlerFunc { return func(ctx *gin.Context) { var err error ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) diff --git a/internal/router/router.go b/internal/router/router.go index c544949..e974b53 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -39,8 +39,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp case "oauth": authHandler := oauth.AuthHandler{} authHandler.Initialize(db, authConfig) - auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) - auth.SetUserSetter(authHandler.UserSetter) + auth.RegisterHandler(authHandler) // Register oauth endpoints oauthGroup := r.Group("/oauth2") @@ -53,8 +52,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp case "basic": authHandler := basicauth.AuthHandler{} authHandler.Initialize(db) - auth.SetAuthenticationValidator(authHandler.AuthenticationValidator) - auth.SetUserSetter(authHandler.UserSetter) + auth.RegisterHandler(authHandler) default: panic("Unknown authentication method set. Please use one of basic, oauth.") } From 43b2908dac3ade1378d918ba545ce70ccdfe3675 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 3 Jun 2021 14:51:53 +0200 Subject: [PATCH 20/29] move to POST --- internal/router/router.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/router/router.go b/internal/router/router.go index e974b53..78098eb 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -44,8 +44,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // Register oauth endpoints oauthGroup := r.Group("/oauth2") { - oauthGroup.GET("/token", ginserver.HandleTokenRequest) - oauthGroup.GET("/auth", ginserver.HandleAuthorizeRequest) + oauthGroup.POST("/token", ginserver.HandleTokenRequest) + oauthGroup.POST("/auth", ginserver.HandleAuthorizeRequest) oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) } From 48b4f71ab56da2aeb6c20fefa36bbeefacf98120 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sat, 5 Jun 2021 11:32:39 +0200 Subject: [PATCH 21/29] added longterm tokens --- internal/authentication/oauth/handler.go | 77 ++++++++++++++++--- .../authentication/oauth/tokendisplayinfo.go | 16 ++++ internal/router/router.go | 3 +- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 213fdc0..220276a 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -4,6 +4,7 @@ import ( "errors" "log" "net/http" + "time" "github.com/gin-gonic/gin" ginserver "github.com/go-oauth2/gin-server" @@ -15,18 +16,15 @@ type RevokeAccessRequest struct { Access string `json:"access_token"` } -// GetTokenInfo answers with information about a access token -func GetTokenInfo(c *gin.Context) { - data, exists := c.Get(ginserver.DefaultConfig.TokenKey) - if !exists { - err := errors.New("Token not found") - c.AbortWithError(http.StatusNotFound, err) - return - } +type LongtermTokenRequest struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} - ti, ok := data.(oauth2.TokenInfo) - if !ok || !exists { - err := errors.New("Token not found") +// GetTokenInfo answers with information about an access token +func (a *AuthHandler) GetTokenInfo(c *gin.Context) { + ti, err := a.tokenFromContext(c) + if err != nil { c.AbortWithError(http.StatusNotFound, err) return } @@ -42,7 +40,7 @@ func (a *AuthHandler) RevokeAccess(c *gin.Context) { var request RevokeAccessRequest err := c.BindJSON(&request) - if err != nil { + if err != nil || request.Access == "" { log.Println("Error when reading request.") c.AbortWithError(http.StatusUnprocessableEntity, errors.New("Missing access_token")) return @@ -57,3 +55,58 @@ func (a *AuthHandler) RevokeAccess(c *gin.Context) { c.JSON(200, request) } + +// LongtermToken handles request for longterm access tokens +func (a *AuthHandler) LongtermToken(c *gin.Context) { + var tokenGenerateRequest oauth2.TokenGenerateRequest + var request LongtermTokenRequest + var ltdi LongtermTokenDisplayInfo + + err := c.BindJSON(&request) + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("Missing or malformated request")) + return + } + + userTi, err := a.tokenFromContext(c) + + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusNotFound, err) + return + } + + tokenGenerateRequest.UserID = userTi.GetUserID() + tokenGenerateRequest.Scope = userTi.GetScope() + tokenGenerateRequest.ClientID = request.ClientID + tokenGenerateRequest.ClientSecret = request.ClientSecret + tokenGenerateRequest.AccessTokenExp = time.Hour * 24 * 365 * 5 // 5 years + + ti, err := a.manager.GenerateAccessToken(oauth2.Implicit, &tokenGenerateRequest) + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + ltdi.ReadFromTi(ti) + + c.JSON(200, ltdi) +} + +func (a *AuthHandler) tokenFromContext(c *gin.Context) (oauth2.TokenInfo, error) { + err := errors.New("Token not found") + + data, exists := c.Get(ginserver.DefaultConfig.TokenKey) + if !exists { + log.Println("Token does not exist in context.") + return nil, err + } + + ti, ok := data.(oauth2.TokenInfo) + if !ok { + log.Println("Token from context has wrong format.") + return nil, err + } + return ti, nil +} diff --git a/internal/authentication/oauth/tokendisplayinfo.go b/internal/authentication/oauth/tokendisplayinfo.go index 5b388bd..ff1471a 100644 --- a/internal/authentication/oauth/tokendisplayinfo.go +++ b/internal/authentication/oauth/tokendisplayinfo.go @@ -6,6 +6,22 @@ import ( "gopkg.in/oauth2.v3" ) +// LongtermTokenDisplayInfo holds information about an longterm access token but without sensitive, internal data +type LongtermTokenDisplayInfo struct { + Token string `json:"access_token"` + TokenExpires time.Time `json:"expires_at"` + TokenCreated time.Time `json:"created_at"` + UserID string `json:"user_id"` +} + +// ReadFromTi sets the TokenDisplayInfo with data from the TokenInfo +func (tdi *LongtermTokenDisplayInfo) ReadFromTi(ti oauth2.TokenInfo) { + tdi.Token = ti.GetAccess() + tdi.TokenExpires = ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()) + tdi.TokenCreated = ti.GetAccessCreateAt() + tdi.UserID = ti.GetUserID() +} + // TokenDisplayInfo holds information about an access token but without sensitive, internal data type TokenDisplayInfo struct { Token string `json:"access_token"` diff --git a/internal/router/router.go b/internal/router/router.go index 78098eb..5872ef8 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -46,8 +46,9 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp { oauthGroup.POST("/token", ginserver.HandleTokenRequest) oauthGroup.POST("/auth", ginserver.HandleAuthorizeRequest) - oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), oauth.GetTokenInfo) + oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), authHandler.GetTokenInfo) oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) + oauthGroup.POST("/longtermtoken", auth.RequireValidAuthentication(), authHandler.LongtermToken) } case "basic": authHandler := basicauth.AuthHandler{} From 5b0c12498e0a5b85fb133f4e2caf211f3b660fe9 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sat, 5 Jun 2021 20:44:56 +0200 Subject: [PATCH 22/29] use body data for auth --- internal/authentication/oauth/authhandler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 04ee30d..21701fe 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -140,8 +140,8 @@ func (a *AuthHandler) passwordAuthorizationHandler() server.PasswordAuthorizatio // UserAuthHandler extracts user information from an auth request func (a *AuthHandler) UserAuthHandler() server.UserAuthorizationHandler { return func(w http.ResponseWriter, r *http.Request) (string, error) { - username := r.URL.Query().Get("username") - password := r.URL.Query().Get("password") + username := r.FormValue("username") + password := r.FormValue("password") if user, err := a.db.GetUserByName(username); err != nil { return "", err From 488f426626067a328ffae8d5dc3afa27a6be5bd2 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sat, 5 Jun 2021 21:07:21 +0200 Subject: [PATCH 23/29] reflect changes in readme --- README.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index aa8ef6e..74c5bf3 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ For authentication use the ``/oauth2/auth` endpoint. E.g.: ```bash curl \ --header "Content-Type: application/json" \ - --request GET \ - "https://pushbits.example.com/oauth2/auth?client_id=000000&username=admin&password=1233456&response_type=code&redirect_uri=https://myapp.example.com" + --request POST \ + "https://pushbits.example.com/oauth2/auth" -d "client_id=000000&username=admin&password=1233456&response_type=code&redirect_uri=https://myapp.example.com" ``` This will return a HTTP redirect with the status code `302` and an authentication code set as parameter: @@ -184,8 +184,8 @@ For your first token you will need a authentication code, see the section above. ```bash curl \ --header "Content-Type: application/json" \ - --request GET \ - "https://pushbits.example.com/oauth2/token?grant_type=authorization_code&client_id=000000&client_secret=49gjg4js9&response_type=token&redirect_uri=https://myapp.example.com&code=OP1Q2UJEVL-RPR9GZAUURA" + --request POST \ + "https://pushbits.example.com/oauth2/token" -d "grant_type=authorization_code&client_id=000000&client_secret=49gjg4js9&response_type=token&redirect_uri=https://myapp.example.com&code=OP1Q2UJEVL-RPR9GZAUURA" ``` This will then return an access token and refresh token for you. @@ -204,8 +204,8 @@ The access token is short lived, the refresh token is long lived, but can not be ```bash curl \ --header "Content-Type: application/json" \ - --request GET \ - "https://pushbits.example.com/oauth2/token?grant_type=refresh_token&client_id=000000&client_secret=49gjg4js9&response_type=token&refresh_token=OP1Q2UJEVL-RPR9GZAUURA" + --request POST \ + "https://pushbits.example.com/oauth2/token" -d "grant_type=refresh_token&client_id=000000&client_secret=49gjg4js9&response_type=token&refresh_token=OP1Q2UJEVL-RPR9GZAUURA" ``` ##### Getting information about a access token @@ -216,7 +216,7 @@ With a valid access token you can get information about it from `/oauth/tokeninf curl \ --header "Content-Type: application/json" \ --request GET \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ "https://pushbits.example.com/oauth2/tokeninfo" ``` @@ -228,11 +228,23 @@ Admin users are eligible to revoke tokens. This should not be necessary in norma curl \ --header "Content-Type: application/json" \ --request POST \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ - --data '{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NDg1MDYsInN1YiI6IjEifQ.cO0_8fqsJDG4KswjC0CSzc_EznntH-FDQejdolPAISo"}' + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --data '{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NDg1MDYsInN1YiI6IjEifQ.cO0_8fqsJDG4KswjC0CSzc_EznntH-FDQejdolPAISo"}' \ "https://pushbits.example.com/oauth2/revoke" ``` +##### Requesting a longterm token + +Longterm tokens are tokens that life for multiple years. They can be used for scripts and other software that access PushBits. So the other software does not need knowledge about the actuall password of the user. However be carefull with longterm tokens, if you loose one others might be able to perform actions on your user account. + +```bash +curl \ + --request POST \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --data '{"client_id": "000000", "client_secret": "49gjg4js9"}' \ + "https://push.remote.alexanderebhart.de/oauth2/longtermtoken" +``` + ### Message options Messages are supporting three different syntaxes: From c92b783e2caed1c7a7f40680425bb19105ba609c Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sat, 19 Jun 2021 19:42:11 +0200 Subject: [PATCH 24/29] let oauth use existing db --- cmd/pushbits/main.go | 3 ++- config.example.yml | 5 ----- internal/authentication/oauth/authhandler.go | 17 +++++++---------- internal/configuration/configuration.go | 15 ++++++++------- internal/database/database.go | 5 +++++ internal/router/router.go | 8 ++++---- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index d256ca5..028e1b1 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -37,6 +37,7 @@ func main() { cm := credentials.CreateManager(c.Security.CheckHIBP, c.Crypto) + log.Println(c.Database.Dialect) db, err := database.Create(cm, c.Database.Dialect, c.Database.Connection) if err != nil { log.Fatal(err) @@ -60,7 +61,7 @@ func main() { log.Fatal(err) } - engine := router.Create(c.Debug, cm, db, dp, c.Authentication) + engine := router.Create(c.Debug, cm, db, dp, c) runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port) } diff --git a/config.example.yml b/config.example.yml index 025b093..93fcce3 100644 --- a/config.example.yml +++ b/config.example.yml @@ -66,11 +66,6 @@ authentication: method: basic # Only needed if you choose oauth method. oauth: - # The storage used for tokens. - storage: "file" - # The connection to the storage. For file: the filename, ending with .db. For mysql: the connection string. Check out - # https://github.com/go-sql-driver/mysql#dsn-data-source-name for details. - connection: "pushbits_token.db" # Oauth client identifier (random string). clientid: "000000" # Oauth client secret (random string). diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 21701fe..083128e 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -40,9 +40,9 @@ type AuthHandler struct { } // Initialize prepares the AuthHandler -func (a *AuthHandler) Initialize(db *database.Database, config configuration.Authentication) error { +func (a *AuthHandler) Initialize(db *database.Database, configAuth configuration.Authentication, configDatabase configuration.Database) error { a.db = db - a.config = config + a.config = configAuth if len(a.config.Oauth.ClientSecret) < 5 { panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") @@ -69,12 +69,9 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte(a.config.Oauth.TokenKey), jwt.SigningMethodHS512)) // unfortunately only symmetric algorithms seem to be supported // Define a storage for the tokens - switch a.config.Oauth.Storage { + switch configDatabase.Dialect { case "mysql": - dbOauth, err := sqlx.Connect("mysql", a.config.Oauth.Connection+"?parseTime=true") - if err != nil { - log.Fatal(err) - } + dbOauth := sqlx.NewDb(db.GetSqldb(), "mysql") a.manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) @@ -86,8 +83,8 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut Secret: a.config.Oauth.ClientSecret, Domain: a.config.Oauth.ClientRedirect, }) - case "file": - a.manager.MustTokenStorage(store.NewFileTokenStore(a.config.Oauth.Connection)) + case "sqlite3": + a.manager.MustTokenStorage(store.NewFileTokenStore("pushbits_tokens.db")) clientStore := store.NewClientStore() // memory store a.manager.MapClientStorage(clientStore) clientStore.Set(a.config.Oauth.ClientID, &models.Client{ @@ -96,7 +93,7 @@ func (a *AuthHandler) Initialize(db *database.Database, config configuration.Aut Domain: a.config.Oauth.ClientRedirect, }) default: - log.Panicln("Unknown oauth storage") + log.Panicln("Unknown (oauth) storage dialect") } // Initialize and configure the token server ginserver.InitServer(a.manager) diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 74b35b5..42db0e8 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -31,14 +31,18 @@ type Authentication struct { // Oauth holds information about the oauth server type Oauth struct { - Connection string `default:"pushbits_token.db"` - Storage string `default:"file"` ClientID string `default:"000000"` ClientSecret string `default:""` ClientRedirect string `default:"http://localhost"` TokenKey string `default:""` } +// Database holds information about the used database type +type Database struct { + Dialect string `default:"sqlite3"` + Connection string `default:"pushbits.db"` +} + // Configuration holds values that can be configured by the user. type Configuration struct { Debug bool `default:"false"` @@ -46,11 +50,8 @@ type Configuration struct { ListenAddress string `default:""` Port int `default:"8080"` } - Database struct { - Dialect string `default:"sqlite3"` - Connection string `default:"pushbits.db"` - } - Admin struct { + Database Database + Admin struct { Name string `default:"admin"` Password string `default:"admin"` MatrixID string `required:"true"` diff --git a/internal/database/database.go b/internal/database/database.go index c7f52ff..92ce45b 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -139,3 +139,8 @@ func (d *Database) RepairChannels(dp Dispatcher) error { return nil } + +// GetSqldb returns the databases sql.DB object +func (d *Database) GetSqldb() *sql.DB { + return d.sqldb +} diff --git a/internal/router/router.go b/internal/router/router.go index 5872ef8..06c0832 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -18,7 +18,7 @@ import ( ) // Create a Gin engine and setup all routes. -func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, authConfig configuration.Authentication) *gin.Engine { +func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, config *configuration.Configuration) *gin.Engine { log.Println("Setting up HTTP routes.") if !debug { @@ -32,13 +32,13 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp // Set up authentication and handler auth := authentication.Authenticator{ DB: db, - Config: authConfig, + Config: config.Authentication, } - switch authConfig.Method { + switch config.Authentication.Method { case "oauth": authHandler := oauth.AuthHandler{} - authHandler.Initialize(db, authConfig) + authHandler.Initialize(db, config.Authentication, config.Database) auth.RegisterHandler(authHandler) // Register oauth endpoints From a5e6472e8199fe12fc963433ec3e6c15b6cd76c7 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Wed, 21 Jul 2021 18:47:32 +0200 Subject: [PATCH 25/29] clean up merge conflicts --- go.sum | 5 +++++ internal/configuration/configuration.go | 2 ++ 2 files changed, 7 insertions(+) diff --git a/go.sum b/go.sum index c1bd977..d9d60d2 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -133,6 +135,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= github.com/tidwall/buntdb v1.0.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= @@ -208,6 +212,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 7319d6b..0753fdf 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -44,6 +44,8 @@ type Oauth struct { type Database struct { Dialect string `default:"sqlite3"` Connection string `default:"pushbits.db"` +} + // Matrix holds credentials for a matrix account type Matrix struct { Homeserver string `default:"https://matrix.org"` From b226a5eecdffd9bd9cd5163ec5e615fd05819ef6 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Wed, 21 Jul 2021 18:50:54 +0200 Subject: [PATCH 26/29] add missing package --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index bf30111..35c0562 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/ugorji/go v1.2.4 // indirect golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/oauth2.v3 v3.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 gorm.io/driver/mysql v1.0.4 gorm.io/driver/sqlite v1.1.4 From d98aa1ccb8a87457fd6cc000578340df614af3f0 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Wed, 21 Jul 2021 19:10:58 +0200 Subject: [PATCH 27/29] lowercase errors --- internal/authentication/authentication.go | 8 ++++---- internal/authentication/oauth/authhandler.go | 2 +- internal/authentication/oauth/handler.go | 12 ++++++------ internal/authentication/oauth/middleware.go | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index 036d20f..c609396 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -47,12 +47,12 @@ type hasUserProperty func(user *model.User) bool func (a *Authenticator) requireUserProperty(has hasUserProperty, errorMessage string) gin.HandlerFunc { return func(ctx *gin.Context) { - err := errors.New("User not found") + err := errors.New("user not found") u, exists := ctx.Get("user") if !exists { - log.Println("No user object in context") + log.Println("no user object in context") ctx.AbortWithError(http.StatusForbidden, err) return } @@ -60,7 +60,7 @@ func (a *Authenticator) requireUserProperty(has hasUserProperty, errorMessage st user, ok := u.(*model.User) if !ok { - log.Println("User object from context has wrong format") + log.Println("user object from context has wrong format") ctx.AbortWithError(http.StatusForbidden, err) return } @@ -87,7 +87,7 @@ func (a *Authenticator) RequireAdmin() []gin.HandlerFunc { funcs = append(funcs, a.UserSetter()) funcs = append(funcs, a.requireUserProperty(func(user *model.User) bool { return user.IsAdmin - }, "User does not have permission: admin")) + }, "user does not have permission: admin")) return funcs } diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 083128e..940c5b5 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -145,7 +145,7 @@ func (a *AuthHandler) UserAuthHandler() server.UserAuthorizationHandler { } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { return fmt.Sprint(user.ID), nil } - return "", errors.New("No credentials provided") + return "", errors.New("no credentials provided") } } diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go index 220276a..3b53a5c 100644 --- a/internal/authentication/oauth/handler.go +++ b/internal/authentication/oauth/handler.go @@ -42,14 +42,14 @@ func (a *AuthHandler) RevokeAccess(c *gin.Context) { err := c.BindJSON(&request) if err != nil || request.Access == "" { log.Println("Error when reading request.") - c.AbortWithError(http.StatusUnprocessableEntity, errors.New("Missing access_token")) + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("missing access_token")) return } err = a.manager.RemoveAccessToken(request.Access) if err != nil { log.Println("Error when revoking: ", err) - c.AbortWithError(http.StatusNotFound, errors.New("Unknown access token")) + c.AbortWithError(http.StatusNotFound, errors.New("unknown access token")) return } @@ -65,7 +65,7 @@ func (a *AuthHandler) LongtermToken(c *gin.Context) { err := c.BindJSON(&request) if err != nil { log.Println(err) - c.AbortWithError(http.StatusUnprocessableEntity, errors.New("Missing or malformated request")) + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("missing or malformated request")) return } @@ -95,17 +95,17 @@ func (a *AuthHandler) LongtermToken(c *gin.Context) { } func (a *AuthHandler) tokenFromContext(c *gin.Context) (oauth2.TokenInfo, error) { - err := errors.New("Token not found") + err := errors.New("token not found") data, exists := c.Get(ginserver.DefaultConfig.TokenKey) if !exists { - log.Println("Token does not exist in context.") + log.Println("token does not exist in context.") return nil, err } ti, ok := data.(oauth2.TokenInfo) if !ok { - log.Println("Token from context has wrong format.") + log.Println("token from context has wrong format.") return nil, err } return ti, nil diff --git a/internal/authentication/oauth/middleware.go b/internal/authentication/oauth/middleware.go index 76b5f6b..5f8500b 100644 --- a/internal/authentication/oauth/middleware.go +++ b/internal/authentication/oauth/middleware.go @@ -21,21 +21,21 @@ func (a AuthHandler) UserSetter() gin.HandlerFunc { var err error ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) if !exists { - err = errors.New("No token available") + err = errors.New("no token available") ctx.AbortWithError(http.StatusForbidden, err) return } token, ok := ti.(oauth2.TokenInfo) if !ok { - err = errors.New("Wrong token format") + err = errors.New("wrong token format") ctx.AbortWithError(http.StatusForbidden, err) return } userID, err := strconv.ParseUint(token.GetUserID(), 10, 64) if err != nil { - err = errors.New("User information of wrong format") + err = errors.New("user information of wrong format") ctx.AbortWithError(http.StatusForbidden, err) return } From bed622ee2766a2f4a068ce762998768df66adafa Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 12 Sep 2021 12:58:49 +0200 Subject: [PATCH 28/29] allow multiple oauth clients --- config.example.yml | 13 +++--- internal/authentication/oauth/authhandler.go | 44 ++++++++++++++------ internal/configuration/configuration.go | 7 +++- internal/database/database.go | 4 ++ 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/config.example.yml b/config.example.yml index 427e447..7e39bbd 100644 --- a/config.example.yml +++ b/config.example.yml @@ -66,11 +66,12 @@ authentication: method: basic # Only needed if you choose oauth method. oauth: - # Oauth client identifier (random string). - clientid: "000000" - # Oauth client secret (random string). - clientsecret: "" - # Oauth redirect url after successful auth. Can be overwritten in the authentication request. - clientredirect: "http://localhost" + clients: + # Oauth client identifier (random string). + - clientid: "000000" + # Oauth client secret (random string). + clientsecret: "" + # Oauth redirect url after successful auth. Redirects will only be accepted for this url. + clientredirect: "http://localhost" # Key used for signing (JWT with HS512) the access tokens (random string). tokenkey: "" diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go index 940c5b5..127d716 100644 --- a/internal/authentication/oauth/authhandler.go +++ b/internal/authentication/oauth/authhandler.go @@ -44,9 +44,7 @@ func (a *AuthHandler) Initialize(db *database.Database, configAuth configuration a.db = db a.config = configAuth - if len(a.config.Oauth.ClientSecret) < 5 { - panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") - } else if len(a.config.Oauth.TokenKey) < 5 { + if len(a.config.Oauth.TokenKey) < 5 { panic("Your Oauth 2.0 token key is empty or not long enough to be secure. Please change it in the configuration file.") } @@ -72,26 +70,46 @@ func (a *AuthHandler) Initialize(db *database.Database, configAuth configuration switch configDatabase.Dialect { case "mysql": dbOauth := sqlx.NewDb(db.GetSqldb(), "mysql") + // Workaround to get rid of clients that still exist in the database but no longer in the config file + _, err := dbOauth.Exec("DROP TABLE IF EXISTS oauth_clients") + if err != nil { + panic(err) + } a.manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) a.manager.MapClientStorage(clientStore) - clientStore.Create(&models.Client{ - ID: a.config.Oauth.ClientID, - Secret: a.config.Oauth.ClientSecret, - Domain: a.config.Oauth.ClientRedirect, - }) + for _, client := range a.config.Oauth.Clients { + if len(client.ClientSecret) < 5 { + panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") + } + + clientStore.Create(&models.Client{ + ID: client.ClientID, + Secret: client.ClientSecret, + Domain: client.ClientRedirect, + }) + } + case "sqlite3": a.manager.MustTokenStorage(store.NewFileTokenStore("pushbits_tokens.db")) clientStore := store.NewClientStore() // memory store a.manager.MapClientStorage(clientStore) - clientStore.Set(a.config.Oauth.ClientID, &models.Client{ - ID: a.config.Oauth.ClientID, - Secret: a.config.Oauth.ClientSecret, - Domain: a.config.Oauth.ClientRedirect, - }) + + for _, client := range a.config.Oauth.Clients { + if len(client.ClientSecret) < 5 { + panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") + } + + clientStore.Set(client.ClientID, &models.Client{ + ID: client.ClientID, + Secret: client.ClientSecret, + Domain: client.ClientRedirect, + }) + } + default: log.Panicln("Unknown (oauth) storage dialect") } diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 0753fdf..5f37240 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -34,10 +34,15 @@ type Authentication struct { // Oauth holds information about the oauth server type Oauth struct { + Clients []OauthClient + TokenKey string `default:""` +} + +// OauthClient holds information about an oauth client +type OauthClient struct { ClientID string `default:"000000"` ClientSecret string `default:""` ClientRedirect string `default:"http://localhost"` - TokenKey string `default:""` } // Database holds information about the used database type diff --git a/internal/database/database.go b/internal/database/database.go index 92ce45b..9f50035 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "strings" "time" "github.com/pushbits/server/internal/authentication/credentials" @@ -46,6 +47,9 @@ func Create(cm *credentials.Manager, dialect, connection string) (*Database, err maxOpenConns = 1 db, err = gorm.Open(sqlite.Open(connection), &gorm.Config{}) case "mysql": + if !strings.Contains(connection, "parseTime=true") { + connection += "?parseTime=true" + } db, err = gorm.Open(mysql.Open(connection), &gorm.Config{}) default: message := "Database dialect is not supported" From 2c62a49da2f28858a62e74a391cefd66b60e3e92 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 21 Nov 2021 17:04:07 +0100 Subject: [PATCH 29/29] add ginserver again --- go.mod | 3 +-- go.sum | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 28b995d..22fbbd2 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,8 @@ require ( github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gin-contrib/location v0.0.2 - github.com/go-oauth2/gin-server v1.0.0 - github.com/go-playground/validator/v10 v10.3.0 // indirect github.com/gin-gonic/gin v1.7.0 + github.com/go-oauth2/gin-server v1.0.0 github.com/golang/protobuf v1.4.3 // indirect github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd github.com/google/go-cmp v0.5.0 // indirect diff --git a/go.sum b/go.sum index f7460cc..fc1e857 100644 --- a/go.sum +++ b/go.sum @@ -24,16 +24,19 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-oauth2/gin-server v1.0.0 h1:KR4YMbaK6Od/a0ZIeu1UhoIiYCKT7+2xfrHet0g6E9U= +github.com/go-oauth2/gin-server v1.0.0/go.mod h1:f08F3l5/Pbayb4pjnv5PpUdQLFejgGfHrTjA6IZb0eM= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -176,6 +179,8 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= 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/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -194,7 +199,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190222072716-a9d3bda3a223/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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=