From e4ed02482a8709b7040ac2498f738d308d1942f3 Mon Sep 17 00:00:00 2001 From: rektdeckard Date: Fri, 9 Aug 2024 19:08:12 -0600 Subject: [PATCH] feat(auth): add revocation of keys --- cmd/lk/cloud.go | 96 ++++++++++++++++++++++++++++++++------------ cmd/lk/project.go | 22 +--------- pkg/config/config.go | 35 ++++++++++------ 3 files changed, 94 insertions(+), 59 deletions(-) diff --git a/cmd/lk/cloud.go b/cmd/lk/cloud.go index 6ff21766..be822917 100644 --- a/cmd/lk/cloud.go +++ b/cmd/lk/cloud.go @@ -28,19 +28,30 @@ import ( "github.com/urfave/cli/v3" "github.com/livekit/livekit-cli/pkg/config" + "github.com/livekit/protocol/auth" "github.com/pkg/browser" ) +type ClaimAccessKeyResponse struct { + Key string + Secret string + ProjectId string + OwnerId string + Description string + URL string +} + const ( - cloudAPIServerURL = "https://cloud-api.livekit.io" - cloudDashboardURL = "https://cloud.livekit.io" - createTokenEndpoint = "/cli/auth" - confirmAuthEndpoint = "/cli/confirm-auth" - claimSessionEndpoint = "/cli/claim" + cloudAPIServerURL = "https://cloud-api.livekit.io" + cloudDashboardURL = "https://cloud.livekit.io" + createTokenEndpoint = "/cli/auth" + claimKeyEndpoint = "/cli/claim" + confirmAuthEndpoint = "/cli/confirm-auth" + revokeKeyEndpoint = "/cli/revoke" ) var ( - disconnect bool + revoke bool timeout int64 interval int64 serverURL string @@ -61,7 +72,7 @@ var ( &cli.BoolFlag{ Name: "R", Aliases: []string{"revoke"}, - Destination: &disconnect, + Destination: &revoke, }, &cli.IntFlag{ Name: "t", @@ -139,12 +150,12 @@ func (a *AuthClient) GetVerificationToken(deviceName string) (*VerificationToken return &a.verificationToken, nil } -func (a *AuthClient) ClaimCliKey(ctx context.Context) (*config.AccessKey, error) { +func (a *AuthClient) ClaimCliKey(ctx context.Context) (*ClaimAccessKeyResponse, error) { if a.verificationToken.Token == "" || time.Now().Unix() > a.verificationToken.Expires { return nil, errors.New("session expired") } - reqURL, err := url.Parse(a.baseURL + claimSessionEndpoint) + reqURL, err := url.Parse(a.baseURL + claimKeyEndpoint) if err != nil { return nil, err } @@ -170,7 +181,7 @@ func (a *AuthClient) ClaimCliKey(ctx context.Context) (*config.AccessKey, error) return nil, nil } - ak := &config.AccessKey{} + ak := &ClaimAccessKeyResponse{} err = json.NewDecoder(resp.Body).Decode(&ak) if err != nil { return nil, err @@ -179,9 +190,26 @@ func (a *AuthClient) ClaimCliKey(ctx context.Context) (*config.AccessKey, error) return ak, nil } -func (a *AuthClient) Deauthenticate() error { - // TODO: revoke any session token - return nil +func (a *AuthClient) Deauthenticate(ctx context.Context, projectName, token string) error { + reqURL, err := url.Parse(a.baseURL + revokeKeyEndpoint) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", reqURL.String(), nil) + req.Header = newHeaderWithToken(token) + if err != nil { + return err + } + + resp, err := a.client.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New("access denied") + } + return cliConfig.RemoveProject(projectName) } func NewAuthClient(client *http.Client, baseURL string) *AuthClient { @@ -193,23 +221,41 @@ func NewAuthClient(client *http.Client, baseURL string) *AuthClient { } func initAuth(ctx context.Context, cmd *cli.Command) error { - if err := loadProjectConfig(ctx, cmd); err != nil { - return err - } authClient = *NewAuthClient(&http.Client{}, serverURL) return nil } func handleAuth(ctx context.Context, cmd *cli.Command) error { - if disconnect { - return authClient.Deauthenticate() + if revoke { + if err := loadProjectConfig(ctx, cmd); err != nil { + return err + } + cfg, token, err := requireToken(ctx, cmd) + if err != nil { + return err + } + return authClient.Deauthenticate(ctx, cfg.Name, token) } return tryAuthIfNeeded(ctx, cmd) } -func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error { - _, err := loadProjectDetails(cmd) +func requireToken(_ context.Context, cmd *cli.Command) (*config.ProjectConfig, string, error) { + cfg, err := loadProjectDetails(cmd) + if err != nil { + return nil, "", err + } + + at := auth.NewAccessToken(cfg.APIKey, cfg.APISecret) + token, err := at.ToJWT() if err != nil { + return nil, "", err + } + + return cfg, token, nil +} + +func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error { + if err := loadProjectConfig(ctx, cmd); err != nil { return err } @@ -242,14 +288,13 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error { return err } - var key *config.AccessKey + var key *ClaimAccessKeyResponse var pollErr error if err := spinner.New(). Title("Awaiting confirmation..."). Action(func() { key, pollErr = pollClaim(ctx, cmd) }). - Context(ctx). Run(); err != nil { return err } @@ -274,8 +319,9 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error { Name: key.Description, APIKey: key.Key, APISecret: key.Secret, - URL: "ws://" + key.Project.Subdomain + ".livekit:7800", + URL: key.URL, }) + if isDefault { cliConfig.DefaultProject = key.Description } @@ -298,8 +344,8 @@ func generateConfirmURL(token string) (*url.URL, error) { return base, nil } -func pollClaim(ctx context.Context, _ *cli.Command) (*config.AccessKey, error) { - claim := make(chan *config.AccessKey) +func pollClaim(ctx context.Context, _ *cli.Command) (*ClaimAccessKeyResponse, error) { + claim := make(chan *ClaimAccessKeyResponse) cancel := make(chan error) // every seconds, poll diff --git a/cmd/lk/project.go b/cmd/lk/project.go index fcffff4a..19c9cdfc 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -273,27 +273,7 @@ func removeProject(ctx context.Context, cmd *cli.Command) error { return errors.New("project name is required") } name := cmd.Args().First() - - var newProjects []config.ProjectConfig - for _, p := range cliConfig.Projects { - if p.Name == name { - continue - } - newProjects = append(newProjects, p) - } - cliConfig.Projects = newProjects - - if cliConfig.DefaultProject == name { - cliConfig.DefaultProject = "" - } - - if err := cliConfig.PersistIfNeeded(); err != nil { - return err - } - - fmt.Println("Removed project", name) - - return nil + return cliConfig.RemoveProject(name) } func setDefaultProject(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/config/config.go b/pkg/config/config.go index d1f37aa3..519a9d6d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -37,19 +37,6 @@ type ProjectConfig struct { APISecret string `yaml:"api_secret"` } -type Project struct { - Subdomain string -} - -type AccessKey struct { - Key string - Secret string - ProjectId string - Project Project - OwnerId string - Description string -} - func LoadDefaultProject() (*ProjectConfig, error) { conf, err := LoadOrCreate() if err != nil { @@ -114,6 +101,28 @@ func LoadOrCreate() (*CLIConfig, error) { return c, nil } +func (c *CLIConfig) RemoveProject(name string) error { + var newProjects []ProjectConfig + for _, p := range c.Projects { + if p.Name == name { + continue + } + newProjects = append(newProjects, p) + } + c.Projects = newProjects + + if c.DefaultProject == name { + c.DefaultProject = "" + } + + if err := c.PersistIfNeeded(); err != nil { + return err + } + + fmt.Println("Removed project", name) + return nil +} + func (c *CLIConfig) PersistIfNeeded() error { if len(c.Projects) == 0 && !c.hasPersisted { // doesn't need to be persisted