diff --git a/cmd/service_add.go b/cmd/service_add.go index 634a511..0bf671d 100644 --- a/cmd/service_add.go +++ b/cmd/service_add.go @@ -27,9 +27,11 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" + "go.uploadedlobster.com/scotty/internal/models" ) var serviceAddCmd = &cobra.Command{ @@ -71,6 +73,10 @@ var serviceAddCmd = &cobra.Command{ err = service.Save() cobra.CheckErr(err) fmt.Println(i18n.Tr("Saved service %v using backend %v", service.Name, service.Backend)) + + // Check whether authentication is required + err = promptForAuth(service) + cobra.CheckErr(err) }, } @@ -87,3 +93,25 @@ func init() { // is called directly, e.g.: // serviceAddCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } + +func promptForAuth(service config.ServiceConfig) error { + backend, err := backends.ResolveBackend[models.OAuth2Authenticator](service) + if err != nil { + // No authentication required, return + return nil + } + + doAuth, err := cli.PromptYesNo( + i18n.Tr("The backend %v requires authentication. Authenticate now?", service.Backend), + true, + ) + if err != nil { + return err + } + if !doAuth { + return nil + } + + cli.AuthenticationFlow(service, backend) + return nil +} diff --git a/cmd/service_auth.go b/cmd/service_auth.go index 91125ad..ddab35d 100644 --- a/cmd/service_auth.go +++ b/cmd/service_auth.go @@ -17,20 +17,10 @@ Scotty. If not, see . package cmd import ( - "fmt" - "os" - - "github.com/cli/browser" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uploadedlobster.com/scotty/internal/auth" "go.uploadedlobster.com/scotty/internal/backends" "go.uploadedlobster.com/scotty/internal/cli" - "go.uploadedlobster.com/scotty/internal/config" - "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" - "go.uploadedlobster.com/scotty/internal/storage" - "golang.org/x/oauth2" ) var serviceAuthCmd = &cobra.Command{ @@ -45,49 +35,7 @@ multiple services using the same backend but different authentication.`, cobra.CheckErr(err) backend, err := backends.ResolveBackend[models.OAuth2Authenticator](serviceConfig) cobra.CheckErr(err) - - redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) - cobra.CheckErr(err) - - // The backend must provide an authentication strategy - strategy := backend.OAuth2Strategy(redirectURL) - - // use PKCE to protect against CSRF attacks - // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 - verifier := oauth2.GenerateVerifier() - - state := auth.RandomState() - // Redirect user to consent page to ask for permission specified scopes. - authUrl := strategy.AuthCodeURL(verifier, state) - - // Start an HTTP server to listen for the response - responseChan := make(chan auth.CodeResponse) - auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) - - // Open the URL - fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) - err = browser.OpenURL(authUrl.Url) - cobra.CheckErr(err) - - // Retrieve the code from the authentication callback - code := <-responseChan - if code.State != authUrl.State { - cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) - os.Exit(1) - } - - // Exchange the code for the authentication token - tok, err := strategy.ExchangeToken(code, verifier) - cobra.CheckErr(err) - - // Store the retrieved token in the database - db, err := storage.New(config.DatabasePath()) - cobra.CheckErr(err) - - err = db.SetOAuth2Token(serviceConfig.Name, tok) - cobra.CheckErr(err) - - fmt.Println(i18n.Tr("Access token received, you can use %v now.\n", serviceConfig.Name)) + cli.AuthenticationFlow(serviceConfig, backend) }, } diff --git a/internal/cli/auth.go b/internal/cli/auth.go new file mode 100644 index 0000000..fc5c889 --- /dev/null +++ b/internal/cli/auth.go @@ -0,0 +1,77 @@ +/* +Copyright © 2023 Philipp Wolfer + +Scotty is free software: you can redistribute it and/or modify it under the +terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later version. + +Scotty is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +Scotty. If not, see . +*/ + +package cli + +import ( + "fmt" + "os" + + "github.com/cli/browser" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/internal/auth" + "go.uploadedlobster.com/scotty/internal/backends" + "go.uploadedlobster.com/scotty/internal/config" + "go.uploadedlobster.com/scotty/internal/i18n" + "go.uploadedlobster.com/scotty/internal/models" + "go.uploadedlobster.com/scotty/internal/storage" + "golang.org/x/oauth2" +) + +func AuthenticationFlow(service config.ServiceConfig, backend models.OAuth2Authenticator) { + redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend.Name()) + cobra.CheckErr(err) + + // The backend must provide an authentication strategy + strategy := backend.OAuth2Strategy(redirectURL) + + // use PKCE to protect against CSRF attacks + // https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-22.html#name-countermeasures-6 + verifier := oauth2.GenerateVerifier() + + state := auth.RandomState() + // Redirect user to consent page to ask for permission specified scopes. + authUrl := strategy.AuthCodeURL(verifier, state) + + // Start an HTTP server to listen for the response + responseChan := make(chan auth.CodeResponse) + auth.RunOauth2CallbackServer(*redirectURL, authUrl.Param, responseChan) + + // Open the URL + fmt.Println(i18n.Tr("Visit the URL for authorization: %v", authUrl.Url)) + err = browser.OpenURL(authUrl.Url) + cobra.CheckErr(err) + + // Retrieve the code from the authentication callback + code := <-responseChan + if code.State != authUrl.State { + cobra.CompErrorln(i18n.Tr("Error: OAuth state mismatch")) + os.Exit(1) + } + + // Exchange the code for the authentication token + tok, err := strategy.ExchangeToken(code, verifier) + cobra.CheckErr(err) + + // Store the retrieved token in the database + db, err := storage.New(config.DatabasePath()) + cobra.CheckErr(err) + + err = db.SetOAuth2Token(service.Name, tok) + cobra.CheckErr(err) + + fmt.Println(i18n.Tr("Access token received, you can use %v now.\n", service.Name)) +}