From 8725918bdf3fffad4c3a162a45b5a241ec5b2593 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Mon, 6 Jun 2022 10:49:06 +0200 Subject: [PATCH] auth be authing. --- cmd/stufflog3-local/main.go | 23 +++- entities/user.go | 12 +++ go.mod | 18 +++- go.sum | 51 ++++++++- internal/genutils/find.go | 21 ++++ models/errors.go | 13 +++ ports/cognitoauth/client.go | 209 ++++++++++++++++++++++++++++++++++++ ports/httpapi/auth.go | 72 +++++++++++-- ports/httpapi/scopes.go | 55 ++-------- usecases/auth/provider.go | 13 +++ usecases/auth/service.go | 29 ++++- usecases/scopes/result.go | 12 +-- usecases/scopes/service.go | 40 ++++--- 13 files changed, 477 insertions(+), 91 deletions(-) create mode 100644 entities/user.go create mode 100644 internal/genutils/find.go create mode 100644 ports/cognitoauth/client.go create mode 100644 usecases/auth/provider.go diff --git a/cmd/stufflog3-local/main.go b/cmd/stufflog3-local/main.go index 34f4f71..3bb752b 100644 --- a/cmd/stufflog3-local/main.go +++ b/cmd/stufflog3-local/main.go @@ -1,6 +1,7 @@ package main import ( + "git.aiterp.net/stufflog3/stufflog3/ports/cognitoauth" "git.aiterp.net/stufflog3/stufflog3/ports/httpapi" "git.aiterp.net/stufflog3/stufflog3/ports/mysql" "git.aiterp.net/stufflog3/stufflog3/usecases/auth" @@ -30,7 +31,22 @@ func main() { os.Exit(1) } - authService := &auth.Service{} + cognitoAuth, err := cognitoauth.New( + os.Getenv("STUFFLOG3_AWS_REGION"), + os.Getenv("STUFFLOG3_AWS_CLIENT_ID"), + os.Getenv("STUFFLOG3_AWS_CLIENT_SECRET"), + os.Getenv("STUFFLOG3_AWS_POOL_ID"), + os.Getenv("STUFFLOG3_AWS_POOL_CLIENT_ID"), + os.Getenv("STUFFLOG3_AWS_POOL_CLIENT_SECRET"), + ) + if err != nil { + log.Println("Failed to setup cognito:", err) + os.Exit(1) + } + + authService := &auth.Service{ + Provider: cognitoAuth, + } scopesService := &scopes.Service{ Auth: authService, Repository: db.Scopes(), @@ -59,12 +75,13 @@ func main() { } server := gin.New() + httpapi.Auth(server.Group("/api/v1/auth"), authService) apiV1 := server.Group("/api/v1") - if os.Getenv("STUFFLOG3_USE_DUMMY_USER") != "" { + if os.Getenv("STUFFLOG3_USE_DUMMY_USER") == "true" { log.Println("Using dummy UUID") apiV1.Use(httpapi.DummyMiddleware(authService, "c11230be-4912-4313-83b0-410a248b5bd1")) } else { - apiV1.Use(httpapi.TrustingJwtParserMiddleware(authService)) + apiV1.Use(httpapi.JwtValidatorMiddleware(authService)) } apiV1Scopes := apiV1.Group("/scopes") httpapi.Scopes(apiV1Scopes, scopesService) diff --git a/entities/user.go b/entities/user.go new file mode 100644 index 0000000..dc9e29e --- /dev/null +++ b/entities/user.go @@ -0,0 +1,12 @@ +package entities + +type User struct { + ID string `json:"id"` +} + +type AuthResult struct { + User *User `json:"user"` + Token string `json:"token,omitempty"` + Session string `json:"session,omitempty"` + PasswordChangeRequired bool `json:"passwordChangeRequired"` +} diff --git a/go.mod b/go.mod index 4629f37..bd90de3 100644 --- a/go.mod +++ b/go.mod @@ -4,26 +4,38 @@ go 1.18 require ( github.com/Masterminds/squirrel v1.5.2 + github.com/aws/aws-sdk-go v1.44.27 github.com/gin-gonic/gin v1.7.7 github.com/go-sql-driver/mysql v1.6.0 + github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 + github.com/lestrrat-go/jwx v1.2.25 + golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 ) require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/goccy/go-json v0.9.7 // indirect github.com/golang/protobuf v1.3.3 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.9 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.2.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect - golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index e32d664..8ab45d8 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,15 @@ github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE= github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/aws/aws-sdk-go v1.44.27 h1:8CMspeZSrewnbvAwgl8qo5R7orDLwQnTGBf/OKPiHxI= +github.com/aws/aws-sdk-go v1.44.27/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= 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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= 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.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= @@ -17,9 +24,15 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -28,37 +41,69 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 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/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/genutils/find.go b/internal/genutils/find.go new file mode 100644 index 0000000..2f09a5e --- /dev/null +++ b/internal/genutils/find.go @@ -0,0 +1,21 @@ +package genutils + +func Find[T any](arr []T, cb func(t T) bool) *T { + for i, item := range arr { + if cb(item) { + return &arr[i] + } + } + + return nil +} + +func Contains[T comparable](arr []T, v T) bool { + for _, item := range arr { + if item == v { + return true + } + } + + return false +} diff --git a/models/errors.go b/models/errors.go index d6965f6..c0004e0 100644 --- a/models/errors.go +++ b/models/errors.go @@ -55,3 +55,16 @@ func (e BadInputError) Error() string { func (e BadInputError) HttpStatus() (int, string, interface{}) { return 400, e.Error(), &e } + +type NotImplementedError struct { + Function string `json:"function"` + Message string `json:"message"` +} + +func (e NotImplementedError) Error() string { + return fmt.Sprintf("Not implemented in %s: %s", e.Function, e.Message) +} + +func (e NotImplementedError) HttpStatus() (int, string, interface{}) { + return 501, e.Error(), nil +} diff --git a/ports/cognitoauth/client.go b/ports/cognitoauth/client.go new file mode 100644 index 0000000..e0a878b --- /dev/null +++ b/ports/cognitoauth/client.go @@ -0,0 +1,209 @@ +package cognitoauth + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "git.aiterp.net/stufflog3/stufflog3/entities" + "git.aiterp.net/stufflog3/stufflog3/models" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + "github.com/dgrijalva/jwt-go/v4" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "strings" +) + +type Client struct { + poolID string + poolClientID string + poolClientSecret string + session *session.Session + keySet jwk.Set +} + +func (c *Client) ListUsers(ctx context.Context) ([]entities.User, error) { + cognitoClient := cognitoidentityprovider.New(c.session) + res, err := cognitoClient.ListUsersWithContext(ctx, &cognitoidentityprovider.ListUsersInput{ + UserPoolId: aws.String(c.poolID), + }) + if err != nil { + return nil, err + } + + users := make([]entities.User, 0, 16) + for _, u := range res.Users { + user := entities.User{} + for _, attr := range u.Attributes { + switch *attr.Name { + case "sub": + user.ID = *attr.Value + } + } + + users = append(users, user) + } + + return users, nil +} + +func (c *Client) LoginUser(ctx context.Context, username, password string) (*entities.AuthResult, error) { + mac := hmac.New(sha256.New, []byte(c.poolClientSecret)) + mac.Write([]byte(username + c.poolClientID)) + secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + + cognitoClient := cognitoidentityprovider.New(c.session) + + res, err := cognitoClient.InitiateAuthWithContext(ctx, &cognitoidentityprovider.InitiateAuthInput{ + AuthFlow: aws.String("USER_PASSWORD_AUTH"), + AuthParameters: map[string]*string{ + "USERNAME": &username, + "PASSWORD": &password, + "SECRET_HASH": aws.String(secretHash), + }, + ClientId: aws.String(c.poolClientID), + }) + if err != nil { + return nil, err + } + + if res.ChallengeName != nil && *res.ChallengeName == "NEW_PASSWORD_REQUIRED" { + return &entities.AuthResult{ + Session: *res.Session, + PasswordChangeRequired: true, + }, nil + } else if res.ChallengeName != nil { + return nil, models.NotImplementedError{ + Function: "cognitoauth.Client", + Message: "Missing handler for challenge: " + *res.ChallengeName, + } + } + + if res.AuthenticationResult == nil || res.AuthenticationResult.IdToken == nil { + return nil, models.PermissionDeniedError{} + } + + idToken := *res.AuthenticationResult.IdToken + + return &entities.AuthResult{ + User: c.ValidateToken(nil, idToken), + Token: idToken, + }, nil +} + +func (c *Client) SetupUser(ctx context.Context, session, username, newPassword string) (*entities.User, error) { + mac := hmac.New(sha256.New, []byte(c.poolClientSecret)) + mac.Write([]byte(username + c.poolClientID)) + secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + + cognitoClient := cognitoidentityprovider.New(c.session) + + res, err := cognitoClient.RespondToAuthChallengeWithContext(ctx, &cognitoidentityprovider.RespondToAuthChallengeInput{ + ChallengeName: aws.String("NEW_PASSWORD_REQUIRED"), + ChallengeResponses: map[string]*string{ + "NEW_PASSWORD": aws.String(newPassword), + "USERNAME": aws.String(username), + "SECRET_HASH": aws.String(secretHash), + }, + Session: aws.String(session), + ClientId: aws.String(c.poolClientID), + }) + if err != nil { + return nil, err + } + + if res.AuthenticationResult == nil || res.AuthenticationResult.IdToken == nil { + return nil, models.PermissionDeniedError{} + } + + idToken := *res.AuthenticationResult.IdToken + + return c.ValidateToken(ctx, idToken), nil +} + +func (c *Client) ValidateToken(_ context.Context, token string) *entities.User { + _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + if token.Method.Alg() != jwa.RS256.String() { // jwa.RS256.String() works as well + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, errors.New("kid header not found") + } + key, ok := c.keySet.LookupKeyID(kid) + if !ok { + return nil, fmt.Errorf("key %v not found", kid) + } + var raw interface{} + err := key.Raw(&raw) + return raw, err + }, jwt.WithoutAudienceValidation()) + if err != nil { + return nil + } + + split := strings.SplitN(token, ".", 3) + if len(split) != 3 { + return nil + } + + data, err := base64.RawStdEncoding.DecodeString(split[1]) + if err != nil { + return nil + } + + m := make(map[string]interface{}, 16) + err = json.Unmarshal(data, &m) + if err != nil { + return nil + } + + userID, _ := m["sub"].(string) + if sub, ok := m["custom:actual_userid"].(string); ok { + userID = sub + } + if actualUserID, ok := m["custom:override_sub"].(string); ok { + userID = actualUserID + } + + return &entities.User{ + ID: userID, + } +} + +func New(regionId, clientID, clientSecret, poolId, poolClientId, poolClientSecret string) (*Client, error) { + s, err := session.NewSession(&aws.Config{ + Region: aws.String(regionId), + Credentials: credentials.NewStaticCredentials( + clientID, + clientSecret, + "", + ), + }) + if err != nil { + return nil, err + } + + keySet, err := jwk.Fetch(context.Background(), fmt.Sprintf( + "https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", + regionId, + poolId, + )) + if err != nil { + return nil, err + } + + return &Client{ + poolID: poolId, + poolClientID: poolClientId, + poolClientSecret: poolClientSecret, + session: s, + keySet: keySet, + }, nil +} diff --git a/ports/httpapi/auth.go b/ports/httpapi/auth.go index 8c97508..78c7ee6 100644 --- a/ports/httpapi/auth.go +++ b/ports/httpapi/auth.go @@ -1,29 +1,68 @@ package httpapi import ( - "context" "encoding/base64" "encoding/json" + "git.aiterp.net/stufflog3/stufflog3/entities" + "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/usecases/auth" "github.com/gin-gonic/gin" "net/http" "strings" ) -var contextKey = struct{}{} +type loginInput struct { + Username string `json:"username"` + Password string `json:"password"` +} -func UserID(ctx context.Context) string { - if c, ok := ctx.(*gin.Context); ok { - return UserID(c.Request.Context()) - } +type setupInput struct { + Username string `json:"username"` + Password string `json:"password"` + Session string `json:"session"` +} + +func Auth(g *gin.RouterGroup, service *auth.Service) { + g.GET("", handler("user", func(c *gin.Context) (interface{}, error) { + user := service.GetUser(c.Request.Context()) + if user == nil { + return nil, models.PermissionDeniedError{} + } + + return user, nil + })) - return ctx.Value(&contextKey).(string) + g.POST("/login", handler("auth", func(c *gin.Context) (interface{}, error) { + input := &loginInput{} + err := c.BindJSON(input) + if err != nil { + return nil, models.BadInputError{ + Object: "LoginInput", + Problem: "Invalid JSON: " + err.Error(), + } + } + + return service.Provider.LoginUser(c.Request.Context(), input.Username, input.Password) + })) + + g.POST("/setup", handler("auth", func(c *gin.Context) (interface{}, error) { + input := &setupInput{} + err := c.BindJSON(input) + if err != nil { + return nil, models.BadInputError{ + Object: "LoginInput", + Problem: "Invalid JSON: " + err.Error(), + } + } + + return service.Provider.SetupUser(c.Request.Context(), input.Session, input.Username, input.Password) + })) } func DummyMiddleware(auth *auth.Service, uuid string) gin.HandlerFunc { return func(c *gin.Context) { c.Request = c.Request.WithContext( - auth.ContextWithUser(c.Request.Context(), uuid), + auth.ContextWithUser(c.Request.Context(), entities.User{ID: uuid}), ) } } @@ -58,7 +97,7 @@ func TrustingJwtParserMiddleware(auth *auth.Service) gin.HandlerFunc { if sub, ok := fields["sub"].(string); ok { c.Request = c.Request.WithContext( - auth.ContextWithUser(c.Request.Context(), sub), + auth.ContextWithUser(c.Request.Context(), entities.User{ID: sub}), ) } else { abortRequest(c) @@ -69,3 +108,18 @@ func TrustingJwtParserMiddleware(auth *auth.Service) gin.HandlerFunc { } } } + +// JwtValidatorMiddleware does check the JWT against the provider. +func JwtValidatorMiddleware(s *auth.Service) gin.HandlerFunc { + return func(c *gin.Context) { + header := c.GetHeader("Authorization") + if header != "" { + user := s.ValidateUser(c.Request.Context(), header[7:]) + if user != nil { + c.Request = c.Request.WithContext( + s.ContextWithUser(c.Request.Context(), *user), + ) + } + } + } +} diff --git a/ports/httpapi/scopes.go b/ports/httpapi/scopes.go index 31a594a..3a69d70 100644 --- a/ports/httpapi/scopes.go +++ b/ports/httpapi/scopes.go @@ -36,7 +36,7 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc }) } - scope, members, err := scopes.Find(c.Request.Context(), id) + scope, err := scopes.Find(c.Request.Context(), id) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, Error{ Code: http.StatusNotFound, @@ -46,9 +46,9 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc } found := false - userID := auth.GetUser(c.Request.Context()) - for _, member := range members { - if member.UserID == userID { + user := auth.GetUser(c.Request.Context()) + for id, _ := range scope.Members { + if id == user.ID { found = true break } @@ -61,37 +61,13 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc return } - c.Request = c.Request.WithContext(scopes.CreateContext(c.Request.Context(), userID, *scope, members)) + c.Request = c.Request.WithContext(scopes.CreateContext(c.Request.Context(), user.ID, *scope)) } } func Scopes(g *gin.RouterGroup, scopes *scopes.Service) { g.GET("", handler("scopes", func(c *gin.Context) (interface{}, error) { - scopes, members, err := scopes.List(c.Request.Context()) - if err != nil { - return nil, err - } - - results := make([]scopeResult, 0, len(scopes)) - for _, scope := range scopes { - resMembers := make([]scopeResultMember, 0, len(members)/2) - for _, member := range members { - if member.ScopeID == scope.ID { - resMembers = append(resMembers, scopeResultMember{ - ID: member.UserID, - Name: member.Name, - Owner: member.Owner, - }) - } - } - - results = append(results, scopeResult{ - Scope: scope, - Members: resMembers, - }) - } - - return results, nil + return scopes.List(c.Request.Context()) })) g.GET("/:scope_id", handler("scope", func(c *gin.Context) (interface{}, error) { @@ -100,23 +76,6 @@ func Scopes(g *gin.RouterGroup, scopes *scopes.Service) { return nil, err } - scope, members, err := scopes.Find(c.Request.Context(), id) - if err != nil { - return nil, err - } - - resMembers := make([]scopeResultMember, 0, len(members)/2) - for _, member := range members { - resMembers = append(resMembers, scopeResultMember{ - ID: member.UserID, - Name: member.Name, - Owner: member.Owner, - }) - } - - return scopeResult{ - Scope: *scope, - Members: resMembers, - }, nil + return scopes.Find(c.Request.Context(), id) })) } diff --git a/usecases/auth/provider.go b/usecases/auth/provider.go new file mode 100644 index 0000000..d28eea6 --- /dev/null +++ b/usecases/auth/provider.go @@ -0,0 +1,13 @@ +package auth + +import ( + "context" + "git.aiterp.net/stufflog3/stufflog3/entities" +) + +type Provider interface { + ListUsers(ctx context.Context) ([]entities.User, error) + LoginUser(ctx context.Context, username, password string) (*entities.AuthResult, error) + SetupUser(ctx context.Context, session, username, newPassword string) (*entities.User, error) + ValidateToken(ctx context.Context, token string) *entities.User +} diff --git a/usecases/auth/service.go b/usecases/auth/service.go index 28f9460..18e983e 100644 --- a/usecases/auth/service.go +++ b/usecases/auth/service.go @@ -2,21 +2,40 @@ package auth import ( "context" + "git.aiterp.net/stufflog3/stufflog3/entities" ) type Service struct { + Provider Provider + key struct{ Stuff uint64 } } -func (s *Service) ContextWithUser(ctx context.Context, id string) context.Context { - return context.WithValue(ctx, &s.key, id) +func (s *Service) Login(ctx context.Context, username, password string) (*entities.AuthResult, error) { + return s.Provider.LoginUser(ctx, username, password) +} + +func (s *Service) Setup(ctx context.Context, session, username, newPassword string) (*entities.User, error) { + return s.Provider.SetupUser(ctx, session, username, newPassword) +} + +func (s *Service) ValidateUser(ctx context.Context, token string) *entities.User { + return s.Provider.ValidateToken(ctx, token) +} + +func (s *Service) ContextWithUser(ctx context.Context, user entities.User) context.Context { + return context.WithValue(ctx, &s.key, &user) +} + +func (s *Service) Users(ctx context.Context) ([]entities.User, error) { + return s.Provider.ListUsers(ctx) } -func (s *Service) GetUser(ctx context.Context) string { +func (s *Service) GetUser(ctx context.Context) *entities.User { v := ctx.Value(&s.key) if v == nil { - return "" + return nil } - return v.(string) + return v.(*entities.User) } diff --git a/usecases/scopes/result.go b/usecases/scopes/result.go index 32b3065..d4a24c0 100644 --- a/usecases/scopes/result.go +++ b/usecases/scopes/result.go @@ -5,7 +5,7 @@ import "git.aiterp.net/stufflog3/stufflog3/entities" type Result struct { entities.Scope - Members []ResultMember `json:"members"` + Members map[string]ResultMember `json:"members"` } type ResultMember struct { @@ -14,19 +14,19 @@ type ResultMember struct { Owner bool `json:"owner"` } -func generateResult(scope entities.Scope, members []entities.ScopeMember) Result { - res := Result{Scope: scope, Members: make([]ResultMember, 0, 2)} +func generateResult(scope entities.Scope, members []entities.ScopeMember) *Result { + res := Result{Scope: scope, Members: make(map[string]ResultMember, len(members))} for _, member := range members { if member.ScopeID != scope.ID { continue } - res.Members = append(res.Members, ResultMember{ + res.Members[member.UserID] = ResultMember{ ID: member.UserID, Name: member.Name, Owner: member.Owner, - }) + } } - return res + return &res } diff --git a/usecases/scopes/service.go b/usecases/scopes/service.go index 98ef6bd..c6fda0c 100644 --- a/usecases/scopes/service.go +++ b/usecases/scopes/service.go @@ -3,6 +3,7 @@ package scopes import ( "context" "git.aiterp.net/stufflog3/stufflog3/entities" + "git.aiterp.net/stufflog3/stufflog3/internal/genutils" "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/usecases/auth" ) @@ -15,14 +16,14 @@ type Service struct { contextKey struct{} } -func (s *Service) CreateContext(ctx context.Context, userID string, scope entities.Scope, members []entities.ScopeMember) context.Context { +func (s *Service) CreateContext(ctx context.Context, userID string, scope Result) context.Context { return context.WithValue(ctx, &s.contextKey, &Context{ scopesRepo: s.Repository, statsRepo: s.StatsLister, userID: userID, ID: scope.ID, - Scope: generateResult(scope, members), + Scope: scope, }) } @@ -36,36 +37,45 @@ func (s *Service) Context(ctx context.Context) *Context { } // Find finds a scope and its members, and returns it if the logged-in user is part of this list. -func (s *Service) Find(ctx context.Context, id int) (*entities.Scope, []entities.ScopeMember, error) { +func (s *Service) Find(ctx context.Context, id int) (*Result, error) { + user := s.Auth.GetUser(ctx) + if user == nil { + return nil, models.PermissionDeniedError{} + } + scope, err := s.Repository.Find(ctx, id) if err != nil { - return nil, nil, err + return nil, err } members, err := s.Repository.ListMembers(ctx, scope.ID) if err != nil { - return nil, nil, err + return nil, err } - userID := s.Auth.GetUser(ctx) found := false for _, member := range members { - if member.UserID == userID { + if member.UserID == user.ID { found = true break } } if !found { - return nil, nil, models.NotFoundError("Scope") + return nil, models.NotFoundError("Scope") } - return scope, members, nil + return generateResult(*scope, members), nil } // List lists a scope and their members, and returns it if the logged-in user is part of this list. -func (s *Service) List(ctx context.Context) ([]entities.Scope, []entities.ScopeMember, error) { - scopes, err := s.Repository.ListUser(ctx, s.Auth.GetUser(ctx)) +func (s *Service) List(ctx context.Context) ([]Result, error) { + user := s.Auth.GetUser(ctx) + if user == nil { + return nil, models.PermissionDeniedError{} + } + + scopes, err := s.Repository.ListUser(ctx, user.ID) if err != nil { - return nil, nil, err + return nil, err } ids := make([]int, 0, len(scopes)) @@ -74,8 +84,10 @@ func (s *Service) List(ctx context.Context) ([]entities.Scope, []entities.ScopeM } members, err := s.Repository.ListMembers(ctx, ids...) if err != nil { - return nil, nil, err + return nil, err } - return scopes, members, nil + return genutils.Map(scopes, func(scope entities.Scope) Result { + return *generateResult(scope, members) + }), nil }