loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit 1f46a6546c6b81a03ecac4af660271faea54dee6
parent cc0cc25f2e1ffaf1aa9c6b89b2067a958d812530
Author: Sebastian Mancke <sebastian.mancke@snabble.io>
Date:   Mon, 13 May 2019 16:22:10 +0200

Merge pull request #127 from g-w/user-claims

Introduce user claims provider
Diffstat:
M.travis.yml | 4+++-
MREADME.md | 51+++++++++++++++++++++++++++++++++++++++++++++++----
Ago.mod | 17+++++++++++++++++
Ago.sum | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlogin/config.go | 9+++++++++
Mlogin/config_test.go | 20++++++++++++++++----
Mlogin/handler.go | 2--
Mlogin/redirect.go | 4+++-
Mlogin/user_claims.go | 86+++++++++----------------------------------------------------------------------
Alogin/user_claims_file.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/user_claims_file_test.go | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/user_claims_provider.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/user_claims_provider_test.go | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlogin/user_claims_test.go | 147++++++++++++++-----------------------------------------------------------------
14 files changed, 730 insertions(+), 210 deletions(-)

diff --git a/.travis.yml b/.travis.yml @@ -3,6 +3,9 @@ language: go go: - tip +env: + - GO111MODULE=on + services: - docker @@ -25,4 +28,3 @@ deploy: after_success: - ./.generate_coverage.sh - goveralls -coverprofile=full_cov.out -service=travis-ci - diff --git a/README.md b/README.md @@ -90,6 +90,9 @@ _Note for Caddy users_: Not all parameters are available in Caddy. See the table | -jwt-refreshes | int | 0 | X | The maximum number of JWT refreshes | | -grace-period | go duration | 5s | - | Duration to wait after SIGINT/SIGTERM for existing requests. No new requests are accepted. | | -user-file | string | | X | A YAML file with user specific data for the tokens. (see below for an example) | +| -user-endpoint | string | | X | URL of an endpoint providing user specific data for the tokens. (see below for an example) | +| -user-endpoint-token | string | | X | Authentication token used when communicating with the user endpoint | +| -user-endpoint-timeout | go duration | 5s | X | Timeout used when communicating with the user endpoint | ### Environment Variables All of the above Config Options can also be applied as environment variables by using variables named this way: `LOGINSRV_OPTION_NAME`. @@ -369,11 +372,18 @@ When you specify a custom template, only the layout of the original template is </html> ``` -## User File +## Custom claims -To customize the content of the JWT token, a YAML file with user data can be provided. -After successful authentication against a backend system, the user is searched within the file -and the content of the claims parameter is used to enhance the user JWT claim parameters. +To customize the content of the JWT token either a file wich contains +user data or an endpoint providing claims can be provided. + +### User file + +A user file is a YAML file which contains additional information which +is encoded in the token. After successful authentication against a +backend system, the user is searched within the file and the content +of the claims parameter is used to enhance the user JWT claim +parameters. To match an entry, the user file is searched in linear order and all attributes has to match the data of the authentication backend. The first matching entry will be used and all parameters @@ -421,3 +431,36 @@ Example: - claims: role: unknown ``` + +### User endpoint + +A user endpoint is a http endpoint which provides additional +information on an authenticated user. After successful authentication +against a backend system, the endpoint gets called and the provided +information is used to enhance the user JWT claim parameters. + +loginsrv passes these paramters to the endpoint: +* `sub` - the username (all backends) +* `origin` - the provider or backend name (all backends) +* `email` - the mail address (the OAuth provider) +* `domain` - the domain (Google only) +* `groups` - the full path string of user groups enclosed in an array (Gitlab only) + +An interaction looks like this + +``` +GET /claims?origin=google&sub=test@example.com&email=test@example.com HTTP/1.1 +Host: localhost:8080 +Accept: */* +Authorization: Bearer token + +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "sub":"test@example.com", + "uid":"113", + "origin":"google", + "permissions": ["read", "write"] +} +``` diff --git a/go.mod b/go.mod @@ -0,0 +1,17 @@ +module github.com/tarent/loginsrv + +go 1.12 + +require ( + github.com/BTBurke/caddy-jwt v3.7.0+incompatible + github.com/abbot/go-http-auth v0.4.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gorilla/mux v1.7.1 + github.com/mholt/caddy v1.0.0 + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.3.0 + github.com/tarent/lib-compose v0.0.0-20170829113806-69430f91d1d6 + github.com/tarent/logrus v0.11.5 + golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum @@ -0,0 +1,106 @@ +github.com/BTBurke/caddy-jwt v3.7.0+incompatible h1:s+KyRkFojVls447rBDpSgbVk1c+Ocvs/342rTV71dlY= +github.com/BTBurke/caddy-jwt v3.7.0+incompatible/go.mod h1:kHIkQzCNxzUICXYHPXO+vKxX5iz929FAA4zmSQLzU4Y= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= +github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= +github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115 h1:fUjoj2bT6dG8LoEe+uNsKk8J+sLkDbQkJnB6Z1F02Bc= +github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk= +github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-acme/lego v2.5.0+incompatible h1:5fNN9yRQfv8ymH3DSsxla+4aYeQt2IgfZqHKVnK8f0s= +github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= +github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw= +github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a h1:BcF8coBl0QFVhe8vAMMlD+CV8EISiu9MGKLoj6ZEyJA= +github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A= +github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= +github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= +github.com/lucas-clemente/quic-go v0.10.2 h1:iQtTSZVbd44k94Lu0U16lLBIG3lrnjDvQongjPd4B/s= +github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= +github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug= +github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/mholt/caddy v1.0.0 h1:KI6RPGih2GFzWRPG8s9clKK28Ns4ZlVMKR/v7mxq6+c= +github.com/mholt/caddy v1.0.0/go.mod h1:PzUpQ3yGCTuEuy0KSxEeB4TZOi3zBZ8BR/zY0RBP414= +github.com/mholt/certmagic v0.5.0 h1:lYXxsLUFya/I3BgDCrfuwcMQOB+4auzI8CCzpK41tjc= +github.com/mholt/certmagic v0.5.0/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM= +github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= +github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4 h1:S9YlS71UNJIyS61OqGAmLXv3w5zclSidN+qwr80XxKs= +github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tarent/lib-compose v0.0.0-20170829113806-69430f91d1d6 h1:EdRyqD9aAKam90IDujb6wtPsOV4JG79ZxkZE01DUA3M= +github.com/tarent/lib-compose v0.0.0-20170829113806-69430f91d1d6/go.mod h1:mqxWNjaOgpyafkwOVyRkP/PIL+RN8phVEf4sjP8yW6c= +github.com/tarent/logrus v0.11.5 h1:6Ecuym2kpXpZURcyYKm7K5IQ1AZGK2hye7auueNyEtE= +github.com/tarent/logrus v0.11.5/go.mod h1:ql8ihK/sxurTyP1LVhkGrMHhl0aXd/+hu4MBiAJDFN4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/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-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/login/config.go b/login/config.go @@ -44,6 +44,9 @@ func DefaultConfig() *Config { Oauth: Options{}, GracePeriod: 5 * time.Second, UserFile: "", + UserEndpoint: "", + UserEndpointToken: "", + UserEndpointTimeout: 5 * time.Second, } } @@ -76,6 +79,9 @@ type Config struct { Oauth Options GracePeriod time.Duration UserFile string + UserEndpoint string + UserEndpointToken string + UserEndpointTimeout time.Duration } // Options is the configuration structure for oauth and backend provider @@ -130,6 +136,9 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) { f.StringVar(&c.LoginPath, "login-path", c.LoginPath, "The path of the login resource") f.DurationVar(&c.GracePeriod, "grace-period", c.GracePeriod, "Graceful shutdown grace period") f.StringVar(&c.UserFile, "user-file", c.UserFile, "A YAML file with user specific data for the tokens") + f.StringVar(&c.UserEndpoint, "user-endpoint", c.UserEndpoint, "URL of an endpoint providing user specific data for the tokens") + f.StringVar(&c.UserEndpointToken, "user-endpoint-token", c.UserEndpointToken, "Authentication token used when communicating with the user endpoint") + f.DurationVar(&c.UserEndpointTimeout, "user-endpoint-timeout", c.UserEndpointTimeout, "Timeout used when communicating with the user endpoint") // the -backends is deprecated, but we support it for backwards compatibility deprecatedBackends := setFunc(func(optsKvList string) error { diff --git a/login/config_test.go b/login/config_test.go @@ -47,6 +47,9 @@ func TestConfig_ReadConfig(t *testing.T) { "--github=client_id=foo,client_secret=bar", "--grace-period=4s", "--user-file=users.yml", + "--user-endpoint=http://test.io/claims", + "--user-endpoint-token=token", + "--user-endpoint-timeout=1s", } expected := &Config{ @@ -80,8 +83,11 @@ func TestConfig_ReadConfig(t *testing.T) { "client_secret": "bar", }, }, - GracePeriod: 4 * time.Second, - UserFile: "users.yml", + GracePeriod: 4 * time.Second, + UserFile: "users.yml", + UserEndpoint: "http://test.io/claims", + UserEndpointToken: "token", + UserEndpointTimeout: time.Second, } cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), input) @@ -114,6 +120,9 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) { NoError(t, os.Setenv("LOGINSRV_GITHUB", "client_id=foo,client_secret=bar")) NoError(t, os.Setenv("LOGINSRV_GRACE_PERIOD", "4s")) NoError(t, os.Setenv("LOGINSRV_USER_FILE", "users.yml")) + NoError(t, os.Setenv("LOGINSRV_USER_ENDPOINT", "http://test.io/claims")) + NoError(t, os.Setenv("LOGINSRV_USER_ENDPOINT_TOKEN", "token")) + NoError(t, os.Setenv("LOGINSRV_USER_ENDPOINT_TIMEOUT", "1s")) expected := &Config{ Host: "host", @@ -147,8 +156,11 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) { "client_secret": "bar", }, }, - GracePeriod: 4 * time.Second, - UserFile: "users.yml", + GracePeriod: 4 * time.Second, + UserFile: "users.yml", + UserEndpoint: "http://test.io/claims", + UserEndpointToken: "token", + UserEndpointTimeout: time.Second, } cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), []string{}) diff --git a/login/handler.go b/login/handler.go @@ -89,7 +89,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } h.handleLogin(w, r) - return } func (h *Handler) handleOauth(w http.ResponseWriter, r *http.Request) { @@ -116,7 +115,6 @@ func (h *Handler) handleOauth(w http.ResponseWriter, r *http.Request) { WithField("username", userInfo.Sub).Info("failed authentication") h.respondAuthFailure(w, r) - return } func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { diff --git a/login/redirect.go b/login/redirect.go @@ -6,9 +6,10 @@ import ( "net/url" "os" - "github.com/tarent/loginsrv/logging" "strings" "time" + + "github.com/tarent/loginsrv/logging" ) func (h *Handler) setRedirectCookie(w http.ResponseWriter, r *http.Request) { @@ -38,6 +39,7 @@ func (h *Handler) allowRedirect(r *http.Request) bool { if !h.config.Redirect { return false } + if !h.config.RedirectCheckReferer { return true } diff --git a/login/user_claims.go b/login/user_claims.go @@ -1,13 +1,11 @@ package login import ( - "io/ioutil" "time" "github.com/dgrijalva/jwt-go" "github.com/pkg/errors" "github.com/tarent/loginsrv/model" - "gopkg.in/yaml.v2" ) type customClaims map[string]interface{} @@ -23,85 +21,19 @@ func (custom customClaims) Valid() error { return nil } -type userFileEntry struct { - Sub string `yaml:"sub"` - Origin string `yaml:"origin"` - Email string `yaml:"email"` - Domain string `yaml:"domain"` - Groups []string `yaml:"groups"` - Claims map[string]interface{} `yaml:"claims"` -} - -type UserClaims struct { - userFile string - userFileEntries []userFileEntry -} - -func NewUserClaims(config *Config) (*UserClaims, error) { - c := &UserClaims{ - userFile: config.UserFile, - userFileEntries: []userFileEntry{}, +func (custom customClaims) merge(values map[string]interface{}) { + for k, v := range values { + custom[k] = v } - err := c.parseUserFile() - return c, err } -func (c *UserClaims) parseUserFile() error { - if c.userFile == "" { - return nil - } - b, err := ioutil.ReadFile(c.userFile) - if err != nil { - return errors.Wrapf(err, "can't read user file %v", c.userFile) - } - - err = yaml.Unmarshal(b, &c.userFileEntries) - if err != nil { - return errors.Wrapf(err, "can't parse user file %v", c.userFile) - } - return nil +type UserClaims interface { + Claims(userInfo model.UserInfo) (jwt.Claims, error) } -// Claims returns a map of the token claims for a user. -func (c *UserClaims) Claims(userInfo model.UserInfo) (jwt.Claims, error) { - for _, entry := range c.userFileEntries { - if match(userInfo, entry) { - claims := customClaims(userInfo.AsMap()) - for k, v := range entry.Claims { - claims[k] = v - } - return claims, nil - } - } - return userInfo, nil -} - -func match(userInfo model.UserInfo, entry userFileEntry) bool { - if entry.Sub != "" && entry.Sub != userInfo.Sub { - return false - } - if entry.Domain != "" && entry.Domain != userInfo.Domain { - return false - } - if entry.Email != "" && entry.Email != userInfo.Email { - return false - } - if entry.Origin != "" && entry.Origin != userInfo.Origin { - return false - } - if len(entry.Groups) > 0 { - eligible := false - for _, entryGroup := range entry.Groups { - for _, userGroup := range userInfo.Groups { - if entryGroup == userGroup { - eligible = true - break - } - } - } - if !eligible { - return false - } +func NewUserClaims(config *Config) (UserClaims, error) { + if config.UserEndpoint != "" { + return newUserClaimsProvider(config.UserEndpoint, config.UserEndpointToken, config.UserEndpointTimeout) } - return true + return newUserClaimsFile(config.UserFile) } diff --git a/login/user_claims_file.go b/login/user_claims_file.go @@ -0,0 +1,91 @@ +package login + +import ( + "io/ioutil" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" + "github.com/tarent/loginsrv/model" + yaml "gopkg.in/yaml.v2" +) + +type userFileEntry struct { + Sub string `yaml:"sub"` + Origin string `yaml:"origin"` + Email string `yaml:"email"` + Domain string `yaml:"domain"` + Groups []string `yaml:"groups"` + Claims map[string]interface{} `yaml:"claims"` +} + +type userClaimsFile struct { + userFile string + userFileEntries []userFileEntry +} + +func newUserClaimsFile(file string) (*userClaimsFile, error) { + c := &userClaimsFile{ + userFile: file, + userFileEntries: []userFileEntry{}, + } + err := c.parseUserFile() + return c, err +} + +func (c *userClaimsFile) parseUserFile() error { + if c.userFile == "" { + return nil + } + b, err := ioutil.ReadFile(c.userFile) + if err != nil { + return errors.Wrapf(err, "can't read user file %v", c.userFile) + } + + err = yaml.Unmarshal(b, &c.userFileEntries) + if err != nil { + return errors.Wrapf(err, "can't parse user file %v", c.userFile) + } + return nil +} + +// Claims returns a map of the token claims for a user. +func (c *userClaimsFile) Claims(userInfo model.UserInfo) (jwt.Claims, error) { + for _, entry := range c.userFileEntries { + if match(userInfo, entry) { + claims := customClaims(userInfo.AsMap()) + claims.merge(entry.Claims) + return claims, nil + } + } + return userInfo, nil +} + +func match(userInfo model.UserInfo, entry userFileEntry) bool { + if entry.Sub != "" && entry.Sub != userInfo.Sub { + return false + } + if entry.Domain != "" && entry.Domain != userInfo.Domain { + return false + } + if entry.Email != "" && entry.Email != userInfo.Email { + return false + } + if entry.Origin != "" && entry.Origin != userInfo.Origin { + return false + } + if len(entry.Groups) > 0 { + eligible := false + for _, entryGroup := range entry.Groups { + for _, userGroup := range userInfo.Groups { + if entryGroup == userGroup { + eligible = true + break + } + } + } + if !eligible { + return false + } + } + return true +} diff --git a/login/user_claims_file_test.go b/login/user_claims_file_test.go @@ -0,0 +1,145 @@ +package login + +import ( + "io/ioutil" + "os" + "testing" + + . "github.com/stretchr/testify/assert" + "github.com/tarent/loginsrv/model" +) + +var claimsExample = ` +- sub: bob + origin: htpasswd + claims: + role: superAdmin + +- email: admin@example.org + origin: google + claims: + role: admin + projects: + - example + sub: overwrittenSubject + +- domain: example.org + origin: google + claims: + role: user + projects: + - example + +- origin: gitlab + groups: + - "example/subgroup" + - othergroup + claims: + role: admin + +- claims: + role: unknown +` + +var invalidClaimsExample = ` +- sub: bob + origin: google +` + +func Test_newUserClaimsFile_InvalidFile(t *testing.T) { + c, err := newUserClaimsFile("notfound") + + Error(t, err) + Equal(t, &userClaimsFile{ + userFile: "notfound", + userFileEntries: []userFileEntry{}, + }, c) +} + +func Test_newUserClaimsFile_InvalidYAML(t *testing.T) { + f, _ := ioutil.TempFile("", "") + f.WriteString(invalidClaimsExample) + f.Close() + defer os.Remove(f.Name()) + + c, err := newUserClaimsFile(f.Name()) + + Error(t, err) + Equal(t, &userClaimsFile{ + userFile: f.Name(), + userFileEntries: []userFileEntry{}, + }, c) +} + +func Test_newUserClaimsFile_ParseFile(t *testing.T) { + fileName, cleanup := createClaimsFile(claimsExample) + defer cleanup() + + c, err := newUserClaimsFile(fileName) + + NoError(t, err) + Equal(t, 5, len(c.userFileEntries)) + Equal(t, "admin@example.org", c.userFileEntries[1].Email) + Equal(t, "google", c.userFileEntries[1].Origin) + Equal(t, "admin", c.userFileEntries[1].Claims["role"]) + Equal(t, []interface{}{"example"}, c.userFileEntries[1].Claims["projects"]) + Equal(t, []string{"example/subgroup", "othergroup"}, c.userFileEntries[3].Groups) +} + +func Test_userClaimsFile_Claims(t *testing.T) { + f, _ := ioutil.TempFile("", "") + f.WriteString(claimsExample) + f.Close() + fileName := f.Name() + defer os.Remove(f.Name()) + + c, err := NewUserClaims(&Config{UserFile: fileName}) + NoError(t, err) + + // Match first entry + claims, _ := c.Claims(model.UserInfo{Sub: "bob", Origin: "htpasswd"}) + Equal(t, customClaims{"sub": "bob", "origin": "htpasswd", "role": "superAdmin"}, claims) + + // Match second entry + claims, _ = c.Claims(model.UserInfo{Sub: "any", Email: "admin@example.org", Origin: "google"}) + Equal(t, customClaims{"sub": "overwrittenSubject", "email": "admin@example.org", "origin": "google", "role": "admin", "projects": []interface{}{"example"}}, claims) + + // Match fourth entry + claims, _ = c.Claims(model.UserInfo{Sub: "any", Groups: []string{"example/subgroup", "othergroup"}, Origin: "gitlab"}) + Equal(t, customClaims{"sub": "any", "groups": []string{"example/subgroup", "othergroup"}, "origin": "gitlab", "role": "admin"}, claims) + + // default case with no rules + claims, _ = c.Claims(model.UserInfo{Sub: "bob"}) + Equal(t, customClaims{"sub": "bob", "role": "unknown"}, claims) +} + +func Test_userClaimsFile_NoMatch(t *testing.T) { + userFile, cleanup := createClaimsFile(` +- sub: bob + groups: + - othergroup + claims: + role: superAdmin +`) + defer cleanup() + + c, err := NewUserClaims(&Config{UserFile: userFile}) + NoError(t, err) + + // No Match -> not Modified + claims, err := c.Claims(model.UserInfo{Sub: "foo"}) + NoError(t, err) + Equal(t, model.UserInfo{Sub: "foo"}, claims) + + claims, err = c.Claims(model.UserInfo{Sub: "bob", Groups: []string{"group"}}) + NoError(t, err) + Equal(t, model.UserInfo{Sub: "bob", Groups: []string{"group"}}, claims) +} + +func createClaimsFile(claims string) (string, func()) { + f, _ := ioutil.TempFile("", "") + f.WriteString(claims) + f.Close() + + return f.Name(), func() { os.Remove(f.Name()) } +} diff --git a/login/user_claims_provider.go b/login/user_claims_provider.go @@ -0,0 +1,97 @@ +package login + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" + "github.com/tarent/loginsrv/model" +) + +type userClaimsProvider struct { + url string + auth string + httpClient http.Client +} + +func newUserClaimsProvider(url, auth string, timeout time.Duration) (*userClaimsProvider, error) { + if err := validateURL(url); err != nil { + return nil, err + } + + return &userClaimsProvider{ + url: url, + auth: auth, + httpClient: http.Client{Timeout: timeout}, + }, nil +} + +func (provider *userClaimsProvider) Claims(userInfo model.UserInfo) (jwt.Claims, error) { + claimsURL := provider.buildURL(userInfo) + req, _ := http.NewRequest(http.MethodGet, claimsURL, nil) + if provider.auth != "" { + req.Header.Add("Authorization", "Bearer "+provider.auth) + } + + resp, err := provider.httpClient.Do(req) + if err != nil { + return nil, err + } + defer func() { + ioutil.ReadAll(resp.Body) + resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("bad http response code %d", resp.StatusCode) + } + + decoder := json.NewDecoder(resp.Body) + + remoteClaims := map[string]interface{}{} + err = decoder.Decode(&remoteClaims) + if err != nil { + return nil, err + } + + claims := customClaims(userInfo.AsMap()) + claims.merge(remoteClaims) + + return claims, nil +} + +func (provider *userClaimsProvider) buildURL(userInfo model.UserInfo) string { + // error can be ignored, it was already checked in validateURL + u, _ := url.Parse(provider.url) + + query := u.Query() + + query.Add("sub", url.QueryEscape(userInfo.Sub)) + if userInfo.Origin != "" { + query.Add("origin", url.QueryEscape(userInfo.Origin)) + } + if userInfo.Domain != "" { + query.Add("domain", url.QueryEscape(userInfo.Domain)) + } + if userInfo.Email != "" { + query.Add("email", url.QueryEscape(userInfo.Email)) + } + if len(userInfo.Groups) > 0 { + for _, group := range userInfo.Groups { + query.Add("group", url.QueryEscape(group)) + } + } + + u.RawQuery = query.Encode() + + return u.String() +} + +func validateURL(s string) error { + _, err := url.Parse(s) + return errors.Wrap(err, "invalid claims provider url") +} diff --git a/login/user_claims_provider_test.go b/login/user_claims_provider_test.go @@ -0,0 +1,161 @@ +package login + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarent/loginsrv/model" +) + +const ( + endpointPath = "/claims" + token = "token" + timeout = time.Second +) + +var aUserInfo = model.UserInfo{ + Sub: "test@example.com", + Origin: "origin", + Domain: "example.com", +} + +func Test_newUserClaimsProvider_ValidatesURL(t *testing.T) { + _, err := newUserClaimsProvider("@#$%^&*(", "auth", time.Minute) + + assert.Error(t, err) +} + +func Test_userClaimsProvider_Claims(t *testing.T) { + mock := createMockServer( + mockResponse{ + url: endpointPath, + status: http.StatusOK, + body: `{ + "claims": [ + { "role": "admin" } + ] +}`, + }, + ) + defer mock.Close() + provider, err := newUserClaimsProvider(mock.URL+endpointPath, token, time.Minute) + require.NoError(t, err) + + claims, err := provider.Claims(model.UserInfo{ + Sub: "test@example.com", + Origin: "origin", + Domain: "example.com", + }) + + require.NoError(t, err) + + assert.Equal(t, 1, len(mock.requests)) + + request := mock.requests[0] + assertQueryValue(t, "sub", "test@example.com", request.URL) + assertQueryValue(t, "origin", "origin", request.URL) + assertQueryValue(t, "domain", "example.com", request.URL) + + assert.Equal(t, "Bearer "+token, request.Header.Get("Authorization")) + + assert.Equal(t, + customClaims{ + "claims": []interface{}{ + map[string]interface{}{ + "role": "admin", + }, + }, + "domain": "example.com", + "origin": "origin", + "sub": "test@example.com", + }, + claims, + ) +} + +func Test_userClaimsProvider_Claims_EndpointNotReachable(t *testing.T) { + provider, err := newUserClaimsProvider("http://not-exists.example.com", token, time.Millisecond) + require.NoError(t, err) + + _, err = provider.Claims(aUserInfo) + + assert.Error(t, err) +} + +func Test_userClaimsProvider_Claims_Errors(t *testing.T) { + for _, test := range []struct { + name string + status int + body string + }{ + { + name: "invalid json body", + status: http.StatusOK, + body: `}{`, + }, + { + name: "not 200 response code", + status: http.StatusForbidden, + }, + } { + t.Run(test.name, func(t *testing.T) { + mock := createMockServer( + mockResponse{ + url: endpointPath, + status: test.status, + body: test.body, + }, + ) + defer mock.Close() + provider, err := newUserClaimsProvider(mock.URL+endpointPath, token, time.Minute) + require.NoError(t, err) + + _, err = provider.Claims(aUserInfo) + + assert.Error(t, err) + }) + } +} + +type mockServer struct { + *httptest.Server + requests []*http.Request +} + +type mockResponse struct { + url, body string + status int +} + +func createMockServer(responses ...mockResponse) *mockServer { + mux := http.NewServeMux() + server := &mockServer{ + httptest.NewServer(mux), + []*http.Request{}, + } + + for _, response := range responses { + body := response.body + mux.HandleFunc( + response.url, + func(w http.ResponseWriter, r *http.Request) { + server.requests = append(server.requests, r) + + w.WriteHeader(response.status) + w.Write([]byte(body)) + }) + } + + return server +} + +func assertQueryValue(t *testing.T, name, expectedValue string, u *url.URL) { + value, err := url.QueryUnescape(u.Query().Get(name)) + require.NoError(t, err) + assert.Equal(t, expectedValue, value) +} diff --git a/login/user_claims_test.go b/login/user_claims_test.go @@ -1,156 +1,61 @@ package login import ( - "io/ioutil" - "os" "testing" "time" . "github.com/stretchr/testify/assert" - "github.com/tarent/loginsrv/model" + "github.com/stretchr/testify/require" ) -var claimsExample = ` +func Test_NewUserClaims_File(t *testing.T) { + userFile, cleanup := createClaimsFile(` - sub: bob - origin: htpasswd claims: role: superAdmin +`) + defer cleanup() + config := &Config{UserFile: userFile} -- email: admin@example.org - origin: google - claims: - role: admin - projects: - - example - sub: overwrittenSubject - -- domain: example.org - origin: google - claims: - role: user - projects: - - example - -- origin: gitlab - groups: - - "example/subgroup" - - othergroup - claims: - role: admin - -- claims: - role: unknown -` - -var invalidClaimsExample = ` -- sub: bob - origin: google -` - -func Test_ParseUserClaims_InvalidFile(t *testing.T) { - c, err := NewUserClaims(&Config{UserFile: "notfound"}) - Error(t, err) - Equal(t, &UserClaims{ - userFile: "notfound", - userFileEntries: []userFileEntry{}, - }, c) -} - -func Test_ParseUserClaims_InvalidYAML(t *testing.T) { - f, _ := ioutil.TempFile("", "") - f.WriteString(invalidClaimsExample) - f.Close() - defer os.Remove(f.Name()) - - c, err := NewUserClaims(&Config{UserFile: f.Name()}) - Error(t, err) - Equal(t, &UserClaims{ - userFile: f.Name(), - userFileEntries: []userFileEntry{}, - }, c) -} - -func Test_UserClaims_ParseUserClaims(t *testing.T) { - f, _ := ioutil.TempFile("", "") - f.WriteString(claimsExample) - f.Close() - defer os.Remove(f.Name()) - - c, err := NewUserClaims(&Config{UserFile: f.Name()}) - NoError(t, err) - Equal(t, 5, len(c.userFileEntries)) - Equal(t, "admin@example.org", c.userFileEntries[1].Email) - Equal(t, "google", c.userFileEntries[1].Origin) - Equal(t, "admin", c.userFileEntries[1].Claims["role"]) - Equal(t, []interface{}{"example"}, c.userFileEntries[1].Claims["projects"]) - Equal(t, []string{"example/subgroup", "othergroup"}, c.userFileEntries[3].Groups) -} - -func Test_UserClaims_Claims(t *testing.T) { - f, _ := ioutil.TempFile("", "") - f.WriteString(claimsExample) - f.Close() - defer os.Remove(f.Name()) - - c, err := NewUserClaims(&Config{UserFile: f.Name()}) - NoError(t, err) - - // Match first entry - claims, _ := c.Claims(model.UserInfo{Sub: "bob", Origin: "htpasswd"}) - Equal(t, customClaims{"sub": "bob", "origin": "htpasswd", "role": "superAdmin"}, claims) - - // Match second entry - claims, _ = c.Claims(model.UserInfo{Sub: "any", Email: "admin@example.org", Origin: "google"}) - Equal(t, customClaims{"sub": "overwrittenSubject", "email": "admin@example.org", "origin": "google", "role": "admin", "projects": []interface{}{"example"}}, claims) - - // Match fourth entry - claims, _ = c.Claims(model.UserInfo{Sub: "any", Groups: []string{"example/subgroup", "othergroup"}, Origin: "gitlab"}) - Equal(t, customClaims{"sub": "any", "groups": []string{"example/subgroup", "othergroup"}, "origin": "gitlab", "role": "admin"}, claims) + claims, err := NewUserClaims(config) - // default case with no rules - claims, _ = c.Claims(model.UserInfo{Sub: "bob"}) - Equal(t, customClaims{"sub": "bob", "role": "unknown"}, claims) + require.NoError(t, err) + NotNil(t, claims) + _, ok := claims.(*userClaimsFile) + True(t, ok) } -func Test_UserClaims_NoMatch(t *testing.T) { - f, _ := ioutil.TempFile("", "") - f.WriteString(` -- sub: bob - groups: - - othergroup - claims: - role: superAdmin -`) - f.Close() - defer os.Remove(f.Name()) - - c, err := NewUserClaims(&Config{UserFile: f.Name()}) - NoError(t, err) +func Test_NewUserClaims_Provider(t *testing.T) { + config := &Config{ + UserEndpoint: "https://test.io/something", + UserEndpointToken: "token", + UserEndpointTimeout: time.Minute, + } - // No Match -> not Modified - claims, err := c.Claims(model.UserInfo{Sub: "foo"}) - NoError(t, err) - Equal(t, model.UserInfo{Sub: "foo"}, claims) + claims, err := NewUserClaims(config) - claims, err = c.Claims(model.UserInfo{Sub: "bob", Groups: []string{"group"}}) - NoError(t, err) - Equal(t, model.UserInfo{Sub: "bob", Groups: []string{"group"}}, claims) + require.NoError(t, err) + NotNil(t, claims) + _, ok := claims.(*userClaimsProvider) + True(t, ok) } -func Test_UserClaims_Valid(t *testing.T) { +func Test_customClaims_Valid(t *testing.T) { cc := customClaims{ "exp": time.Now().Unix() + 3600, } err := cc.Valid() + NoError(t, err) } -func Test_UserClaims_Invalid(t *testing.T) { +func Test_customClaims_Invalid(t *testing.T) { cc := customClaims{ "exp": time.Now().Unix() - 3600, } err := cc.Valid() + Error(t, err) }