├── go.mod ├── README.md ├── LICENSE └── oauth.go /go.mod: -------------------------------------------------------------------------------- 1 | module rsc.io/oauthprompt 2 | 3 | go 1.22 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go get rsc.io/oauthprompt 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /oauth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package oauthprompt implements prompting a local user for 6 | // an OAuth token and caching the result in the user's home directory. 7 | package oauthprompt 8 | 9 | import ( 10 | "context" 11 | "crypto/rand" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "net" 16 | "net/http" 17 | "os" 18 | "os/exec" 19 | "path/filepath" 20 | 21 | "golang.org/x/oauth2" 22 | oauth "golang.org/x/oauth2" 23 | ) 24 | 25 | // Token obtains an OAuth token, keeping a cached copy in file. 26 | // If the file name is not an absolute path, it is interpreted relative to the 27 | // user's home directory. 28 | func Token(file string, cfg *oauth.Config) (*http.Client, error) { 29 | if !filepath.IsAbs(file) { 30 | file = filepath.Join(os.Getenv("HOME"), file) 31 | } 32 | data, err := os.ReadFile(file) 33 | if err == nil { 34 | var tok oauth.Token 35 | if err := json.Unmarshal(data, &tok); err != nil { 36 | return nil, fmt.Errorf("oauthprompt.Token: unmarshal %s: %v", file, err) 37 | } 38 | return cfg.Client(context.Background(), &tok), nil 39 | } 40 | 41 | // Start HTTP server on localhost. 42 | l, err := net.Listen("tcp", "127.0.0.1:0") 43 | if err != nil { 44 | var err1 error 45 | if l, err1 = net.Listen("tcp6", "[::1]:0"); err1 != nil { 46 | return nil, fmt.Errorf("oauthprompt.Token: starting HTTP server: %v", err) 47 | } 48 | } 49 | 50 | type done struct { 51 | err error 52 | code string 53 | } 54 | ch := make(chan done, 100) 55 | 56 | randState, err := randomID() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | cfg1 := *cfg 62 | cfg = &cfg1 63 | cfg.RedirectURL = "http://" + l.Addr().String() + "/done" 64 | authURL := cfg1.AuthCodeURL(randState) 65 | 66 | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 67 | if req.URL.Path == "/auth" { 68 | http.Redirect(w, req, authURL, 301) 69 | return 70 | } 71 | if req.URL.Path != "/done" { 72 | http.Error(w, "", 404) 73 | return 74 | } 75 | if req.FormValue("state") != randState { 76 | ch <- done{err: fmt.Errorf("oauthprompt.Token: incorrect response")} 77 | http.Error(w, "", 500) 78 | return 79 | } 80 | if code := req.FormValue("code"); code != "" { 81 | ch <- done{code: code} 82 | w.Write([]byte(success)) 83 | return 84 | } 85 | http.Error(w, "", 500) 86 | }) 87 | 88 | srv := &http.Server{Handler: handler} 89 | go srv.Serve(l) 90 | if err := openURL("http://" + l.Addr().String() + "/auth"); err != nil { 91 | l.Close() 92 | return nil, err 93 | } 94 | d := <-ch 95 | l.Close() 96 | 97 | if d.err != nil { 98 | return nil, err 99 | } 100 | 101 | tok, err := cfg.Exchange(context.Background(), d.code) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | data, err = json.Marshal(tok) 107 | if err != nil { 108 | return nil, err 109 | } 110 | if err := os.WriteFile(file, data, 0666); err != nil { 111 | return nil, err 112 | } 113 | 114 | return cfg.Client(context.Background(), tok), nil 115 | } 116 | 117 | var browsers = []string{ 118 | "xdg-open", 119 | "google-chrome", 120 | "open", // for OS X 121 | } 122 | 123 | func openURL(url string) error { 124 | fmt.Fprintf(os.Stderr, "oauthprompt: %s\n", url) 125 | for _, browser := range browsers { 126 | err := exec.Command(browser, url).Run() 127 | if err == nil { 128 | return nil 129 | } 130 | } 131 | 132 | tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0) 133 | if err != nil { 134 | // Hope for the best with standard error. 135 | tty = os.Stderr 136 | } else { 137 | defer tty.Close() 138 | } 139 | 140 | _, err = fmt.Fprintf(tty, "To log in, please visit %s\n", url) 141 | if err != nil { 142 | return fmt.Errorf("failed to notify user about URL") 143 | } 144 | return nil 145 | } 146 | 147 | // GoogleToken is like Token but assumes the Google AuthURL and TokenURL, 148 | // so that only the client ID and secret and desired scope must be specified. 149 | func GoogleToken(file, clientID, clientSecret string, scopes ...string) (*http.Client, error) { 150 | cfg := &oauth.Config{ 151 | ClientID: clientID, 152 | ClientSecret: clientSecret, 153 | Scopes: scopes, 154 | Endpoint: oauth2.Endpoint{ 155 | AuthURL: "https://accounts.google.com/o/oauth2/auth", 156 | TokenURL: "https://accounts.google.com/o/oauth2/token", 157 | }, 158 | } 159 | return Token(file, cfg) 160 | } 161 | 162 | func randomID() (string, error) { 163 | buf := make([]byte, 16) 164 | _, err := io.ReadFull(rand.Reader, buf) 165 | if err != nil { 166 | return "", fmt.Errorf("RandomID: reading rand.Reader: %v", err) 167 | } 168 | return fmt.Sprintf("%x", buf), nil 169 | } 170 | 171 | var success = ` 172 | 173 | Authenticated 174 | 179 | 180 | 181 | Thanks for authenticating. 182 | 183 | 184 | ` 185 | --------------------------------------------------------------------------------