/* Copyright © 2023 Philipp Wolfer This file is part of Scotty. 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 cmd import ( "context" "fmt" "net/http" "github.com/cli/browser" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/backends" "go.uploadedlobster.com/scotty/storage" "golang.org/x/oauth2" "golang.org/x/oauth2/spotify" ) // authCmd represents the auth command var authCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with a backend", Long: `For backends requiring authentication this command can be used to authenticate.`, Run: func(cmd *cobra.Command, args []string) { serviceName, serviceConfig := getConfigFromFlag(cmd, "service") backend := serviceConfig.GetString("backend") redirectURL, err := backends.BuildRedirectURL(viper.GetViper(), backend) cobra.CheckErr(err) ctx := context.Background() conf := &oauth2.Config{ ClientID: serviceConfig.GetString("client-id"), ClientSecret: serviceConfig.GetString("client-secret"), Scopes: []string{"user-read-recently-played"}, RedirectURL: redirectURL.String(), Endpoint: spotify.Endpoint, } responseChan := make(chan string) // Start an HTTP server to listen for the response http.HandleFunc(redirectURL.Path, func(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") fmt.Fprint(w, "Token received, you can close this window now.") responseChan <- code }) go http.ListenAndServe(redirectURL.Host, nil) // 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() // Redirect user to consent page to ask for permission // for the scopes specified above. url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier)) fmt.Printf("Visit the URL for the auth dialog: %v\n", url) err = browser.OpenURL(url) cobra.CheckErr(err) code := <-responseChan // Use the authorization code that is pushed to the redirect // URL. Exchange will do the handshake to retrieve the // initial access token. The HTTP Client returned by // conf.Client will refresh the token as necessary. // var code string // _, err = fmt.Scan(&code) // cobra.CheckErr(err) tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier)) cobra.CheckErr(err) db, err := storage.New(viper.GetString("database")) cobra.CheckErr(err) err = db.SetOAuth2Token(serviceName, tok) cobra.CheckErr(err) fmt.Printf("Access token received, you can use %v now.\n\n", serviceName) }, } func init() { rootCmd.AddCommand(authCmd) authCmd.Flags().StringP("service", "s", "", "Service configuration (required)") authCmd.MarkFlagRequired("service") }