Browse Source

auth be authing.

master
Gisle Aune 2 years ago
parent
commit
8725918bdf
  1. 23
      cmd/stufflog3-local/main.go
  2. 12
      entities/user.go
  3. 18
      go.mod
  4. 51
      go.sum
  5. 21
      internal/genutils/find.go
  6. 13
      models/errors.go
  7. 209
      ports/cognitoauth/client.go
  8. 72
      ports/httpapi/auth.go
  9. 55
      ports/httpapi/scopes.go
  10. 13
      usecases/auth/provider.go
  11. 29
      usecases/auth/service.go
  12. 12
      usecases/scopes/result.go
  13. 40
      usecases/scopes/service.go

23
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)

12
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"`
}

18
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
)

51
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=

21
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
}

13
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
}

209
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
}

72
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),
)
}
}
}
}

55
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)
}))
}

13
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
}

29
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)
}

12
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
}

40
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
}
Loading…
Cancel
Save