Add start of auth request storage
Some checks failed
/ docker-build-only (push) Failing after 45s
/ go-test (push) Failing after 1m28s

This commit is contained in:
Melora Hugues 2024-09-22 10:26:27 +02:00
parent 741e638c78
commit 9206c8e41e
13 changed files with 174 additions and 23 deletions

BIN
polyculeconnect.db Normal file

Binary file not shown.

View file

@ -72,6 +72,7 @@ func serve() {
// - do a try-loop? // - do a try-loop?
// - only init when using them in a request? // - only init when using them in a request?
for _, c := range backendConfs { for _, c := range backendConfs {
logger.L.Debugf("Initializing backend %s", c.Name)
b, err := client.New(context.Background(), c) b, err := client.New(context.Background(), c)
if err != nil { if err != nil {
utils.Failf("failed to init backend client: %s", err.Error()) utils.Failf("failed to init backend client: %s", err.Error())

View file

@ -125,6 +125,8 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=

View file

@ -3,21 +3,37 @@ package client
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
) )
type OIDCClient struct { type OIDCClient struct {
Conf *model.Backend conf *model.Backend
provider rp.RelyingParty provider rp.RelyingParty
ctx context.Context
st db.Storage
l *zap.SugaredLogger
} }
func New(ctx context.Context, conf *model.Backend) (*OIDCClient, error) { func New(ctx context.Context, conf *model.Backend, l *zap.SugaredLogger) (*OIDCClient, error) {
pr, err := rp.NewRelyingPartyOIDC(ctx, conf.OIDCConfig.Issuer, conf.OIDCConfig.ClientID, conf.OIDCConfig.ClientSecret, conf.OIDCConfig.RedirectURI, []string{}) options := []rp.Option{
rp.WithLogger(slog.New(zapslog.NewHandler(logger.L.Desugar().Core(), nil))),
}
pr, err := rp.NewRelyingPartyOIDC(ctx, conf.OIDCConfig.Issuer, conf.OIDCConfig.ClientID, conf.OIDCConfig.ClientSecret, conf.OIDCConfig.RedirectURI, []string{}, options...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to init relying party provider: %w", err) return nil, fmt.Errorf("failed to init relying party provider: %w", err)
} }
return &OIDCClient{Conf: conf, provider: pr}, nil return &OIDCClient{ctx: ctx, conf: conf, provider: pr, l: l}, nil
}
func (c *OIDCClient) toto() {
c.provider.GetDeviceAuthorizationEndpoint()
} }

View file

@ -0,0 +1,77 @@
package authrequest
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
"github.com/google/uuid"
)
var ErrNotFound = errors.New("backend not found")
const authRequestRows = `"id", "client_id", "backend_id", "scopes", "redirect_uri", "state", "nonce", "response_type", "creation_time"`
type AuthRequestDB interface {
GetAuthRequestByID(ctx context.Context, id uuid.UUID) (*model.AuthRequest, error)
CreateAuthRequest(ctx context.Context, req model.AuthRequest) error
}
type sqlAuthRequestDB struct {
db *sql.DB
}
func (db *sqlAuthRequestDB) GetAuthRequestByID(ctx context.Context, id uuid.UUID) (*model.AuthRequest, error) {
query := fmt.Sprintf(`SELECT %s FROM "auth_request_2" WHERE "id" = ?`, authRequestRows)
row := db.db.QueryRowContext(ctx, query, id)
var res model.AuthRequest
var scopesStr []byte
if err := row.Scan(&res.ID, &res.ClientID, &res.BackendID, &scopesStr, &res.RedirectURI, &res.State, &res.Nonce, &res.ResponseType, &res.CreationDate); err != nil {
return nil, fmt.Errorf("failed to get auth request from DB: %w", err)
}
if err := json.Unmarshal(scopesStr, &res.Scopes); err != nil {
return nil, fmt.Errorf("invalid format for scopes: %w", err)
}
fmt.Println(res)
return &res, nil
}
func (db *sqlAuthRequestDB) CreateAuthRequest(ctx context.Context, req model.AuthRequest) error {
tx, err := db.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
scopesStr, err := json.Marshal(req.Scopes)
if err != nil {
return fmt.Errorf("failed to serialize scopes: %w", err)
}
query := fmt.Sprintf(`INSERT INTO "auth_request_2" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, authRequestRows)
_, err = tx.ExecContext(ctx, query,
req.ID, req.ClientID, req.BackendID,
scopesStr, req.RedirectURI, req.State,
req.Nonce, req.ResponseType, req.CreationDate, req.AuthTime,
)
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 New(db *sql.DB) *sqlAuthRequestDB {
return &sqlAuthRequestDB{db: db}
}

View file

@ -21,6 +21,7 @@ type scannable interface {
type BackendDB interface { type BackendDB interface {
GetAllBackends(ctx context.Context) ([]*model.Backend, error) GetAllBackends(ctx context.Context) ([]*model.Backend, error)
GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error)
GetBackendByName(ctx context.Context, name string) (*model.Backend, error) GetBackendByName(ctx context.Context, name string) (*model.Backend, error)
AddBackend(ctx context.Context, newBackend *model.Backend) error AddBackend(ctx context.Context, newBackend *model.Backend) error
@ -50,6 +51,12 @@ func (db *sqlBackendDB) GetBackendByName(ctx context.Context, name string) (*mod
return backendFromRow(row) return backendFromRow(row)
} }
func (db *sqlBackendDB) GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error) {
query := fmt.Sprintf(`SELECT %s FROM "backend" WHERE "id" = ?`, backendRows)
row := db.db.QueryRowContext(ctx, query, id)
return backendFromRow(row)
}
func (db *sqlBackendDB) GetAllBackends(ctx context.Context) ([]*model.Backend, error) { func (db *sqlBackendDB) GetAllBackends(ctx context.Context) ([]*model.Backend, error) {
rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "backend"`, backendRows)) rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "backend"`, backendRows))
if err != nil { if err != nil {

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
"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/backend"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/client" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db/client"
) )
@ -13,6 +14,7 @@ type Storage interface {
DB() *sql.DB DB() *sql.DB
ClientStorage() client.ClientDB ClientStorage() client.ClientDB
BackendStorage() backend.BackendDB BackendStorage() backend.BackendDB
AuthRequestStorage() authrequest.AuthRequestDB
} }
type sqlStorage struct { type sqlStorage struct {
@ -31,6 +33,10 @@ func (s *sqlStorage) BackendStorage() backend.BackendDB {
return backend.New(s.db) return backend.New(s.db)
} }
func (s *sqlStorage) AuthRequestStorage() authrequest.AuthRequestDB {
return authrequest.New(s.db)
}
func New(conf config.AppConfig) (Storage, error) { func New(conf config.AppConfig) (Storage, error) {
db, err := sql.Open("sqlite3", conf.StorageConfig.File) db, err := sql.Open("sqlite3", conf.StorageConfig.File)
if err != nil { if err != nil {

View file

@ -1,7 +1,6 @@
package model package model
import ( import (
"fmt"
"strings" "strings"
"time" "time"
@ -29,6 +28,8 @@ type AuthRequest struct {
CodeChallengeMethod string CodeChallengeMethod string
BackendID uuid.UUID BackendID uuid.UUID
Backend *Backend
UserID uuid.UUID UserID uuid.UUID
done bool done bool
} }
@ -54,7 +55,7 @@ func (a AuthRequest) GetAuthTime() time.Time {
} }
func (a AuthRequest) GetClientID() string { func (a AuthRequest) GetClientID() string {
return a.ClientID return a.ID.String() // small hack since we actually need the AuthRequestID here
} }
func (a AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { func (a AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
@ -97,8 +98,6 @@ func (a AuthRequest) Done() bool {
} }
func (a *AuthRequest) FromOIDCAuthRequest(req *oidc.AuthRequest, backendID uuid.UUID) { func (a *AuthRequest) FromOIDCAuthRequest(req *oidc.AuthRequest, backendID uuid.UUID) {
fmt.Println(req)
a.ID = uuid.New() a.ID = uuid.New()
a.ClientID = req.ClientID a.ClientID = req.ClientID
a.Scopes = strings.Split(req.Scopes.String(), " ") a.Scopes = strings.Split(req.Scopes.String(), " ")
@ -110,7 +109,4 @@ func (a *AuthRequest) FromOIDCAuthRequest(req *oidc.AuthRequest, backendID uuid.
a.CodeChallenge = req.CodeChallenge a.CodeChallenge = req.CodeChallenge
a.CodeChallengeMethod = string(req.CodeChallengeMethod) a.CodeChallengeMethod = string(req.CodeChallengeMethod)
a.BackendID = backendID a.BackendID = backendID
fmt.Println(a)
} }

View file

@ -49,12 +49,11 @@ func (c Client) GrantTypes() []oidc.GrantType {
} }
func (c Client) LoginURL(authRequestID string) string { func (c Client) LoginURL(authRequestID string) string {
// here we have the requestID, meaning we should: if c.AuthRequest == nil {
// - get the request from its ID return "" // we don't have a request, let's return nothing
// - get the associated backend }
// - build the correct URI to use as a redirection, which is from the backend
// - afterwards would should basically handle it as a OIDC client return c.AuthRequest.Backend.OIDCConfig.Issuer
return authRequestID
} }
func (c Client) AccessTokenType() op.AccessTokenType { func (c Client) AccessTokenType() op.AccessTokenType {

View file

@ -9,6 +9,7 @@ import (
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/db"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model"
"github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4"
"github.com/google/uuid"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/oidc/v3/pkg/op"
) )
@ -39,6 +40,10 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest,
var opReq model.AuthRequest var opReq model.AuthRequest
opReq.FromOIDCAuthRequest(req, selectedBackend.ID) opReq.FromOIDCAuthRequest(req, selectedBackend.ID)
if err := s.LocalStorage.AuthRequestStorage().CreateAuthRequest(ctx, opReq); err != nil {
return nil, fmt.Errorf("failed to save auth request: %w", err)
}
return opReq, nil return opReq, nil
} }
@ -98,13 +103,43 @@ func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
OP storage OP storage
*/ */
func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { func (s *Storage) getClientWithDetails(ctx context.Context, authRequestID uuid.UUID) (op.Client, error) {
clt, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, clientID) authRequest, err := s.LocalStorage.AuthRequestStorage().GetAuthRequestByID(ctx, authRequestID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get client from local storage: %w", err) return nil, fmt.Errorf("failed to get authRequest from local storage: %w", err)
}
backend, err := s.LocalStorage.BackendStorage().GetBackendByID(ctx, authRequest.BackendID)
if err != nil {
return nil, fmt.Errorf("failed to get associated backend from local storage: %w", err)
}
client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, authRequest.ClientID)
if err != nil {
return nil, fmt.Errorf("failed to get associated client from local storage: %w", err)
} }
return clt, nil authRequest.Backend = backend
client.AuthRequest = authRequest
return client, nil
}
// We're cheating a bit here since we're using the authrequest to get its associated client
// but a request is always associated to a backend, and we really need both, so we have no
// choice here. I'll maybe need to have a more elegant solution later, but not choice for now
func (s *Storage) GetClientByClientID(ctx context.Context, id string) (op.Client, error) {
authRequestID, err := uuid.Parse(id)
if err != nil {
// it's not a UUID, it means this was called using client_id, we just return the client without details
client, err := s.LocalStorage.ClientStorage().GetClientByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get client %s from local storage: %w", id, err)
}
return client, nil
}
// we have a UUID, it means we got a requestID, so we can get all details here
return s.getClientWithDetails(ctx, authRequestID)
} }
func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {

View file

@ -0,0 +1 @@
DROP TABLE "auth_request_2";

View file

@ -0,0 +1,11 @@
CREATE TABLE "auth_request_2" (
id TEXT NOT NULL PRIMARY KEY,
client_id TEXT NOT NULL,
backend_id TEXT NOT NULL,
scopes blob NOT NULL, -- list of strings, json-encoded
redirect_uri TEXT NOT NULL,
state TEXT NOT NULL,
nonce TEXT NOT NULL,
response_type TEXT NOT NULL,
CREATION_TIME timestamp NOT NULL
);

Binary file not shown.