diff --git a/polyculeconnect/cmd/serve/serve.go b/polyculeconnect/cmd/serve/serve.go index 83a879b..93a68e3 100644 --- a/polyculeconnect/cmd/serve/serve.go +++ b/polyculeconnect/cmd/serve/serve.go @@ -2,6 +2,9 @@ package serve import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" "log/slog" "os" "os/signal" @@ -12,10 +15,12 @@ import ( "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/client" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/middlewares" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/storage" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/server" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services" + "github.com/go-jose/go-jose/v4" "github.com/google/uuid" "github.com/spf13/cobra" "github.com/zitadel/oidc/v3/pkg/op" @@ -53,9 +58,23 @@ func serve() { } backends := map[uuid.UUID]*client.OIDCClient{} + key := sha256.Sum256([]byte("test")) - st := storage.Storage{LocalStorage: userDB, InitializedBackends: backends} - opConf := op.Config{} + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + utils.Failf("Failed to generate private key: %s", err) + } + signingKey := model.Key{ + PrivateKey: privateKey, + KeyID: uuid.New(), + SigningAlg: jose.RS256, + } + + st := storage.Storage{LocalStorage: userDB, InitializedBackends: backends, Key: &signingKey} + opConf := op.Config{ + CryptoKey: key, + CodeMethodS256: false, + } slogger := slog.New(zapslog.NewHandler(logger.L.Desugar().Core(), nil)) // slogger := options := []op.Option{ diff --git a/polyculeconnect/go.mod b/polyculeconnect/go.mod index 2730293..223a046 100644 --- a/polyculeconnect/go.mod +++ b/polyculeconnect/go.mod @@ -14,7 +14,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.9.0 - github.com/zitadel/oidc/v3 v3.27.0 + github.com/zitadel/oidc/v3 v3.30.1 go.uber.org/zap v1.24.0 go.uber.org/zap/exp v0.2.0 ) @@ -68,25 +68,25 @@ require ( github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect - github.com/rs/cors v1.11.0 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/zitadel/logging v0.6.0 // indirect + github.com/zitadel/logging v0.6.1 // indirect github.com/zitadel/schema v1.3.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/text v0.18.0 // indirect google.golang.org/api v0.150.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect google.golang.org/grpc v1.59.0 // indirect diff --git a/polyculeconnect/go.sum b/polyculeconnect/go.sum index 99d4fb1..a322aa1 100644 --- a/polyculeconnect/go.sum +++ b/polyculeconnect/go.sum @@ -177,8 +177,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -210,20 +210,20 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= -github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= -github.com/zitadel/oidc/v3 v3.27.0 h1:zeYpyRH0UcgdCjVHUYzSsqf1jbSwVMPVxYKOnRXstgU= -github.com/zitadel/oidc/v3 v3.27.0/go.mod h1:ZwBEqSviCpJVZiYashzo53bEGRGXi7amE5Q8PpQg9IM= +github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= +github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= +github.com/zitadel/oidc/v3 v3.30.1 h1:CCi9qDjleuRYbECfUoVKrrN97KdheNCHAs33X8XnIRg= +github.com/zitadel/oidc/v3 v3.30.1/go.mod h1:N5p02vx+mLUwf+WFNpDsNp+8DS8+jlgFBwpz7NIQjrg= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= @@ -265,16 +265,16 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -303,8 +303,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/polyculeconnect/internal/db/authcode/authcode.go b/polyculeconnect/internal/db/authcode/authcode.go new file mode 100644 index 0000000..f3a9084 --- /dev/null +++ b/polyculeconnect/internal/db/authcode/authcode.go @@ -0,0 +1,62 @@ +package authcode + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" +) + +var ErrNotFound = errors.New("auth code not found") + +type AuthCodeDB interface { + GetAuthCodeByCode(ctx context.Context, code string) (*model.AuthCode, error) + CreateAuthCode(ctx context.Context, code model.AuthCode) error +} + +type sqlAuthCodeDB struct { + db *sql.DB +} + +func (db *sqlAuthCodeDB) CreateAuthCode(ctx context.Context, code model.AuthCode) error { + logger.L.Debugf("Creating auth code for request %s", code.RequestID) + tx, err := db.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + defer func() { _ = tx.Rollback() }() + + query := `INSERT INTO "auth_code_2" ("id", "auth_request_id", "code") VALUES ($1, $2, $3)` + _, err = tx.ExecContext(ctx, query, code.CodeID, code.RequestID, code.Code) + if err != nil { + return fmt.Errorf("failed to insert in DB: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil +} + +func (db *sqlAuthCodeDB) GetAuthCodeByCode(ctx context.Context, code string) (*model.AuthCode, error) { + logger.L.Debugf("Getting auth code %s from DB", code) + query := `SELECT "id", "auth_request_id", "code" FROM "auth_code_2" WHERE "code" = ?` + row := db.db.QueryRowContext(ctx, query, code) + + var res model.AuthCode + if err := row.Scan(&res.CodeID, &res.RequestID, &res.Code); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("failed to read row from DB: %w", err) + } + + return &res, nil +} + +func New(db *sql.DB) AuthCodeDB { + return &sqlAuthCodeDB{db: db} +} diff --git a/polyculeconnect/internal/db/authrequest/authrequest.go b/polyculeconnect/internal/db/authrequest/authrequest.go index fd906ea..0b878a0 100644 --- a/polyculeconnect/internal/db/authrequest/authrequest.go +++ b/polyculeconnect/internal/db/authrequest/authrequest.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "time" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" @@ -14,12 +15,13 @@ import ( var ErrNotFound = errors.New("backend not found") -const authRequestRows = `"id", "client_id", "backend_id", "scopes", "redirect_uri", "state", "nonce", "response_type", "creation_time", "done"` +const authRequestRows = `"id", "client_id", "backend_id", "scopes", "redirect_uri", "state", "nonce", "response_type", "creation_time", "done", "code_challenge", "code_challenge_method", "auth_time"` type AuthRequestDB interface { GetAuthRequestByID(ctx context.Context, id uuid.UUID) (*model.AuthRequest, error) CreateAuthRequest(ctx context.Context, req model.AuthRequest) error ValidateAuthRequest(ctx context.Context, reqID uuid.UUID) error + DeleteAuthRequest(ctx context.Context, reqID uuid.UUID) error } type sqlAuthRequestDB struct { @@ -34,10 +36,14 @@ func (db *sqlAuthRequestDB) GetAuthRequestByID(ctx context.Context, id uuid.UUID var res model.AuthRequest var scopesStr []byte - fmt.Println(query) - if err := row.Scan(&res.ID, &res.ClientID, &res.BackendID, &scopesStr, &res.RedirectURI, &res.State, &res.Nonce, &res.ResponseType, &res.CreationDate, &res.DoneVal); err != nil { + var timestamp *time.Time + + if err := row.Scan(&res.ID, &res.ClientID, &res.BackendID, &scopesStr, &res.RedirectURI, &res.State, &res.Nonce, &res.ResponseType, &res.CreationDate, &res.DoneVal, &res.CodeChallenge, &res.CodeChallengeMethod, ×tamp); err != nil { return nil, fmt.Errorf("failed to get auth request from DB: %w", err) } + if timestamp != nil { + res.AuthTime = *timestamp + } if err := json.Unmarshal(scopesStr, &res.Scopes); err != nil { return nil, fmt.Errorf("invalid format for scopes: %w", err) } @@ -59,11 +65,12 @@ func (db *sqlAuthRequestDB) CreateAuthRequest(ctx context.Context, req model.Aut } // TODO: when the old table is done, rename into auth_request - query := fmt.Sprintf(`INSERT INTO "auth_request_2" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, authRequestRows) + query := fmt.Sprintf(`INSERT INTO "auth_request_2" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NULL)`, authRequestRows) _, err = tx.ExecContext(ctx, query, req.ID, req.ClientID, req.BackendID, scopesStr, req.RedirectURI, req.State, req.Nonce, req.ResponseType, req.CreationDate, false, + req.CodeChallenge, req.CodeChallengeMethod, ) if err != nil { return fmt.Errorf("failed to insert in DB: %w", err) @@ -84,7 +91,7 @@ func (db *sqlAuthRequestDB) ValidateAuthRequest(ctx context.Context, reqID uuid. } defer func() { _ = tx.Rollback() }() - res, err := tx.ExecContext(ctx, `UPDATE "auth_request_2" SET done = true WHERE id = $1`, reqID.String()) + res, err := tx.ExecContext(ctx, `UPDATE "auth_request_2" SET done = true, auth_time = $1 WHERE id = $2`, time.Now().UTC(), reqID.String()) if err != nil { return fmt.Errorf("failed to update in DB: %w", err) } @@ -103,6 +110,24 @@ func (db *sqlAuthRequestDB) ValidateAuthRequest(ctx context.Context, reqID uuid. return nil } +func (db *sqlAuthRequestDB) DeleteAuthRequest(ctx context.Context, reqID uuid.UUID) error { + logger.L.Debugf("Deleting auth request: %s", reqID) + tx, err := db.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + defer func() { _ = tx.Rollback() }() + + _, err = tx.ExecContext(ctx, `DELETE FROM "auth_request_2" WHERE id = $1`, reqID.String()) + if err != nil { + return fmt.Errorf("failed to delete auth request: %w", err) + } + if err := tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil +} + func New(db *sql.DB) *sqlAuthRequestDB { return &sqlAuthRequestDB{db: db} } diff --git a/polyculeconnect/internal/db/base.go b/polyculeconnect/internal/db/base.go index 207a520..6c0e7f8 100644 --- a/polyculeconnect/internal/db/base.go +++ b/polyculeconnect/internal/db/base.go @@ -5,6 +5,7 @@ import ( "fmt" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/authcode" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/authrequest" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/backend" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/client" @@ -15,6 +16,7 @@ type Storage interface { ClientStorage() client.ClientDB BackendStorage() backend.BackendDB AuthRequestStorage() authrequest.AuthRequestDB + AuthCodeStorage() authcode.AuthCodeDB } type sqlStorage struct { @@ -37,6 +39,10 @@ func (s *sqlStorage) AuthRequestStorage() authrequest.AuthRequestDB { return authrequest.New(s.db) } +func (s *sqlStorage) AuthCodeStorage() authcode.AuthCodeDB { + return authcode.New(s.db) +} + func New(conf config.AppConfig) (Storage, error) { db, err := sql.Open("sqlite3", conf.StorageConfig.File) if err != nil { diff --git a/polyculeconnect/internal/model/authcode.go b/polyculeconnect/internal/model/authcode.go new file mode 100644 index 0000000..4d896c6 --- /dev/null +++ b/polyculeconnect/internal/model/authcode.go @@ -0,0 +1,9 @@ +package model + +import "github.com/google/uuid" + +type AuthCode struct { + CodeID uuid.UUID + RequestID uuid.UUID + Code string +} diff --git a/polyculeconnect/internal/model/authrequest.go b/polyculeconnect/internal/model/authrequest.go index 65dd700..343a72e 100644 --- a/polyculeconnect/internal/model/authrequest.go +++ b/polyculeconnect/internal/model/authrequest.go @@ -56,7 +56,7 @@ func (a AuthRequest) GetAuthTime() time.Time { } func (a AuthRequest) GetClientID() string { - return a.ID.String() // small hack since we actually need the AuthRequestID here + return a.ClientID } func (a AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { diff --git a/polyculeconnect/internal/model/client.go b/polyculeconnect/internal/model/client.go index d9b4c46..a7ae0d5 100644 --- a/polyculeconnect/internal/model/client.go +++ b/polyculeconnect/internal/model/client.go @@ -39,7 +39,7 @@ func (c Client) ApplicationType() op.ApplicationType { } func (c Client) AuthMethod() oidc.AuthMethod { - return oidc.AuthMethodNone + return oidc.AuthMethodBasic } func (c Client) ResponseTypes() []oidc.ResponseType { @@ -47,13 +47,13 @@ func (c Client) ResponseTypes() []oidc.ResponseType { } func (c Client) GrantTypes() []oidc.GrantType { - return []oidc.GrantType{oidc.GrantTypeCode} + return []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken, oidc.GrantTypeTokenExchange} } // LoginURL returns the login URL for a given client app and auth request. // This login url should be the authorization URL for the selected OIDC backend func (c Client) LoginURL(authRequestID string) string { - if c.AuthRequest == nil { + if authRequestID == "" { return "" // we don't have a request, let's return nothing } diff --git a/polyculeconnect/internal/model/signingkey.go b/polyculeconnect/internal/model/signingkey.go new file mode 100644 index 0000000..16a8fab --- /dev/null +++ b/polyculeconnect/internal/model/signingkey.go @@ -0,0 +1,57 @@ +package model + +import ( + "crypto/rsa" + "strings" + + "github.com/go-jose/go-jose/v4" + "github.com/google/uuid" +) + +type SigningKey struct { + PrivateKey *rsa.PrivateKey + KeyID uuid.UUID + Algorithm jose.SignatureAlgorithm +} + +func (k *SigningKey) ID() string { + return strings.ReplaceAll(k.KeyID.String(), "-", "") +} + +func (k *SigningKey) SignatureAlgorithm() jose.SignatureAlgorithm { + return k.Algorithm +} + +func (k *SigningKey) Key() any { + return k.PrivateKey +} + +type Key struct { + PrivateKey *rsa.PrivateKey + KeyID uuid.UUID + SigningAlg jose.SignatureAlgorithm +} + +func (k *Key) SigningKey() *SigningKey { + return &SigningKey{ + PrivateKey: k.PrivateKey, + KeyID: k.KeyID, + Algorithm: k.SigningAlg, + } +} + +func (k *Key) ID() string { + return strings.ReplaceAll(k.KeyID.String(), "-", "") +} + +func (k *Key) Algorithm() jose.SignatureAlgorithm { + return k.SigningAlg +} + +func (k *Key) Key() any { + return &k.PrivateKey.PublicKey +} + +func (k *Key) Use() string { + return "sig" +} diff --git a/polyculeconnect/internal/model/token.go b/polyculeconnect/internal/model/token.go new file mode 100644 index 0000000..afde439 --- /dev/null +++ b/polyculeconnect/internal/model/token.go @@ -0,0 +1,21 @@ +package model + +import ( + "time" + + "github.com/google/uuid" +) + +type Token struct { + ID uuid.UUID + RefreshTokenID uuid.UUID + Expiration time.Time + Subjet string + Audiences []string + Scopes []string +} + +type RefreshToken struct { + ID uuid.UUID + AuthTime time.Time +} diff --git a/polyculeconnect/internal/storage/storage.go b/polyculeconnect/internal/storage/storage.go index 7029074..44520f5 100644 --- a/polyculeconnect/internal/storage/storage.go +++ b/polyculeconnect/internal/storage/storage.go @@ -24,6 +24,7 @@ func ErrNotImplemented(name string) error { type Storage struct { LocalStorage db.Storage InitializedBackends map[uuid.UUID]*client.OIDCClient + Key *model.Key } /* @@ -33,7 +34,7 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, // userID should normally be an empty string (to verify), we don't get it in our workflow from what I saw // TODO: check this is indeed not needed / never present - logger.L.Debug("Creating a new auth request") + logger.L.Debugf("Creating a new auth request") // validate that the connector is correct backendName, ok := stringFromCtx(ctx, "backendName") @@ -69,24 +70,90 @@ func (s *Storage) AuthRequestByID(ctx context.Context, requestID string) (op.Aut } func (s *Storage) AuthRequestByCode(ctx context.Context, requestCode string) (op.AuthRequest, error) { - return nil, ErrNotImplemented("AuthRequestByCode") + logger.L.Debugf("Getting auth request from code %s", requestCode) + + authCode, err := s.LocalStorage.AuthCodeStorage().GetAuthCodeByCode(ctx, requestCode) + if err != nil { + return nil, fmt.Errorf("failed to get auth code from DB: %w", err) + } + + return s.LocalStorage.AuthRequestStorage().GetAuthRequestByID(ctx, authCode.RequestID) } func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error { logger.L.Debugf("Saving auth code %s for request %s", code, id) - return ErrNotImplemented("SaveAuthCode") + + requestID, err := uuid.Parse(id) + if err != nil { + return fmt.Errorf("invalid requestID %s: %w", requestID, err) + } + + codeID := uuid.New() + + savedCode := model.AuthCode{ + CodeID: codeID, + RequestID: requestID, + Code: code, + } + return s.LocalStorage.AuthCodeStorage().CreateAuthCode(ctx, savedCode) } func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { - return ErrNotImplemented("DeleteAuthRequest") + return nil // don't delete it for now, it seems we might need it????? (cc dex) + // reqID, err := uuid.Parse(id) + // if err != nil { + // return fmt.Errorf("invalid id format: %w", err) + // } + // return s.LocalStorage.AuthRequestStorage().DeleteAuthRequest(ctx, reqID) } func (s *Storage) CreateAccessToken(ctx context.Context, req op.TokenRequest) (accessTokenID string, expiration time.Time, err error) { - return "", time.Time{}, ErrNotImplemented("CreateAccessToken") + accessTokenUUID := uuid.New() + + // we are expecting our own request model + authRequest, ok := req.(*model.AuthRequest) + if !ok { + return "", time.Time{}, errors.New("failed to parse auth request") + } + + authTime := authRequest.AuthTime.UTC() + expiration = authTime.Add(5 * time.Minute) + + // token := model.Token{ + // ID: accessTokenUUID, + // RefreshTokenID: refreshTokenUUID, + // Expiration: authTime.Add(5 * time.Minute), + // Subjet: request.GetSubject(), + // Audiences: request.GetAudience(), + // Scopes: request.GetScopes(), + // } + + return accessTokenUUID.String(), expiration, nil } func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error) { - return "", "", time.Time{}, ErrNotImplemented("CreateAccessAndRefreshTokens") + accessTokenUUID := uuid.New() + refreshTokenUUID := uuid.New() + + // we are expecting our own request model + authRequest, ok := request.(*model.AuthRequest) + if !ok { + return "", "", time.Time{}, errors.New("failed to parse auth request") + } + + authTime := authRequest.AuthTime.UTC() + expiration = authTime.Add(5 * time.Minute) + + // token := model.Token{ + // ID: accessTokenUUID, + // RefreshTokenID: refreshTokenUUID, + // Expiration: authTime.Add(5 * time.Minute), + // Subjet: request.GetSubject(), + // Audiences: request.GetAudience(), + // Scopes: request.GetScopes(), + // } + + return accessTokenUUID.String(), refreshTokenUUID.String(), expiration, nil } func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (op.RefreshTokenRequest, error) { @@ -106,7 +173,7 @@ func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, stok } func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) { - return nil, ErrNotImplemented("SigningKey") + return s.Key.SigningKey(), nil } func (s *Storage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) { @@ -114,7 +181,7 @@ func (s *Storage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgo } func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) { - return nil, ErrNotImplemented("KeySet") + return []op.Key{s.Key}, nil } /* @@ -169,11 +236,23 @@ func (s *Storage) GetClientByClientID(ctx context.Context, id string) (op.Client } func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { - return ErrNotImplemented("AuthorizeClientIDSecret") + logger.L.Debugf("Validating client secret %s for client %s", clientSecret, clientID) + + client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, clientID) + if err != nil { + return err + } + + if client.Secret != clientSecret { + return errors.New("invalid secret") + } + + return nil } func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { - return ErrNotImplemented("SetUserinfoFromScopes") + // we'll use FromRequest instead + return nil } func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { @@ -185,7 +264,8 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, userinfo *oidc. } func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) { - return nil, ErrNotImplemented("GetPrivateClaimsFromScopes") + // For now, let's just return nothing, we don't want to add any private scope + return nil, nil } func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { diff --git a/polyculeconnect/migrations/1_create_auth_request.up.sql b/polyculeconnect/migrations/1_create_auth_request.up.sql index 534cadc..803dc3b 100644 --- a/polyculeconnect/migrations/1_create_auth_request.up.sql +++ b/polyculeconnect/migrations/1_create_auth_request.up.sql @@ -7,5 +7,5 @@ CREATE TABLE "auth_request_2" ( state TEXT NOT NULL, nonce TEXT NOT NULL, response_type TEXT NOT NULL, - CREATION_TIME timestamp NOT NULL + creation_time timestamp NOT NULL ); \ No newline at end of file diff --git a/polyculeconnect/migrations/4_add_code_challenge.down.sql b/polyculeconnect/migrations/4_add_code_challenge.down.sql new file mode 100644 index 0000000..f3aa033 --- /dev/null +++ b/polyculeconnect/migrations/4_add_code_challenge.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "auth_request_2" DROP COLUMN code_challenge; +ALTER TABLE "auth_request_2" DROP COLUMN code_challenge_method; diff --git a/polyculeconnect/migrations/4_add_code_challenge.up.sql b/polyculeconnect/migrations/4_add_code_challenge.up.sql new file mode 100644 index 0000000..bc8f9cd --- /dev/null +++ b/polyculeconnect/migrations/4_add_code_challenge.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "auth_request_2" ADD COLUMN code_challenge STRING NOT NULL DEFAULT ''; +ALTER TABLE "auth_request_2" ADD COLUMN code_challenge_method STRING NOT NULL DEFAULT ''; diff --git a/polyculeconnect/migrations/5_add_auth_request_auth_time.down.sql b/polyculeconnect/migrations/5_add_auth_request_auth_time.down.sql new file mode 100644 index 0000000..fc5738d --- /dev/null +++ b/polyculeconnect/migrations/5_add_auth_request_auth_time.down.sql @@ -0,0 +1 @@ +ALTER TABLE "auth_request_2" DROP COLUMN auth_time; diff --git a/polyculeconnect/migrations/5_add_auth_request_auth_time.up.sql b/polyculeconnect/migrations/5_add_auth_request_auth_time.up.sql new file mode 100644 index 0000000..e8d84b0 --- /dev/null +++ b/polyculeconnect/migrations/5_add_auth_request_auth_time.up.sql @@ -0,0 +1 @@ +ALTER TABLE "auth_request_2" ADD COLUMN auth_time timestamp; diff --git a/polyculeconnect/polyculeconnect.db b/polyculeconnect/polyculeconnect.db index e4f8d2d..46f8e19 100644 Binary files a/polyculeconnect/polyculeconnect.db and b/polyculeconnect/polyculeconnect.db differ