@@ -3,13 +3,19 @@ package rtl
33import (
44 "bytes"
55 "context"
6+ "crypto/rand"
7+ "crypto/sha256"
68 "crypto/tls"
9+ "encoding/base64"
710 "fmt"
811 "io/ioutil"
12+ "log"
913 "net/http"
1014 "net/url"
1115 "os"
16+ "os/exec"
1217 "reflect"
18+ "runtime"
1319 "time"
1420
1521 "golang.org/x/oauth2"
@@ -87,6 +93,76 @@ func NewAuthSessionWithTransport(config ApiSettings, transport http.RoundTripper
8793 }
8894}
8995
96+ func NewPkceAuthSession (config ApiSettings ) * AuthSession {
97+ transport := & http.Transport {
98+ TLSClientConfig : & tls.Config {
99+ InsecureSkipVerify : ! config .VerifySsl ,
100+ },
101+ }
102+
103+ return NewPkceAuthSessionWithTransport (config , transport )
104+ }
105+
106+ // The transport parameter may override your VerifySSL setting
107+ func NewPkceAuthSessionWithTransport (config ApiSettings , transport http.RoundTripper ) * AuthSession {
108+ // This transport (Roundtripper) sets
109+ // the "x-looker-appid" Header on requests
110+ appIdHeaderTransport := & transportWithHeaders {
111+ Base : transport ,
112+ }
113+
114+ oauthConfig := & oauth2.Config {
115+ ClientID : config .ClientId ,
116+ ClientSecret : "" , // Public client, no secret
117+ Scopes : []string {"cors_api" },
118+ Endpoint : oauth2.Endpoint {
119+ AuthURL : config .AuthUrl ,
120+ TokenURL : config .BaseUrl + "/api/token" ,
121+ },
122+ RedirectURL : fmt .Sprintf ("http://localhost:%d%s" , config .RedirectPort , config .RedirectPath ),
123+ }
124+
125+ verifier , challenge , err := generatePKCEPair ()
126+ if err != nil {
127+ log .Fatalf ("Failed to generate PKCE pair: %v" , err )
128+ }
129+
130+ state , err := generateSecureRandomString (32 )
131+ authURL := oauthConfig .AuthCodeURL (state , oauth2 .AccessTypeOffline ,
132+ oauth2 .SetAuthURLParam ("code_challenge" , challenge ),
133+ oauth2 .SetAuthURLParam ("code_challenge_method" , "S256" ))
134+
135+ authCode , err := startLocalServerAndWaitForCode (authURL , config .RedirectPort , config .RedirectPath )
136+ if err != nil {
137+ log .Fatalf ("Authorization failed: %v" , err )
138+ }
139+
140+ ctx := context .WithValue (
141+ context .Background (),
142+ oauth2 .HTTPClient ,
143+ // Will set "x-looker-appid" Header on TokenURL requests
144+ & http.Client {Transport : appIdHeaderTransport },
145+ )
146+
147+ token , err := oauthConfig .Exchange (ctx , authCode ,
148+ oauth2 .SetAuthURLParam ("code_verifier" , verifier ))
149+ if err != nil {
150+ log .Fatalf ("Failed to exchange token: %v" , err )
151+ }
152+
153+ // Make use of oauth2 transport to handle token management
154+ oauthTransport := & oauth2.Transport {
155+ Source : oauthConfig .TokenSource (ctx , token ),
156+ // Will set "x-looker-appid" Header on all other requests
157+ Base : appIdHeaderTransport ,
158+ }
159+
160+ return & AuthSession {
161+ Config : config ,
162+ Client : http.Client {Transport : oauthTransport },
163+ }
164+ }
165+
90166func (s * AuthSession ) Do (result interface {}, method , ver , path string , reqPars map [string ]interface {}, body interface {}, options * ApiSettings ) error {
91167
92168 // prepare URL
@@ -237,3 +313,88 @@ func setQuery(u *url.URL, pars map[string]interface{}) {
237313 }
238314 u .RawQuery = q .Encode ()
239315}
316+
317+ func generatePKCEPair () (string , string , error ) {
318+ verifierBytes := make ([]byte , 96 )
319+ _ , err := rand .Read (verifierBytes )
320+ if err != nil {
321+ return "" , "" , err
322+ }
323+ verifier := base64 .RawURLEncoding .EncodeToString (verifierBytes )
324+ hasher := sha256 .New ()
325+ hasher .Write ([]byte (verifier ))
326+ challenge := base64 .RawURLEncoding .EncodeToString (hasher .Sum (nil ))
327+ return verifier , challenge , nil
328+ }
329+
330+ // --- Local HTTP Server for Redirect ---
331+ func startLocalServerAndWaitForCode (authURL string , redirectPort int64 , redirectPath string ) (string , error ) {
332+ codeChan := make (chan string )
333+ errChan := make (chan error )
334+
335+ mux := http .NewServeMux ()
336+ server := & http.Server {Addr : fmt .Sprintf (":%d" , redirectPort ), Handler : mux }
337+
338+ mux .HandleFunc (redirectPath , func (w http.ResponseWriter , r * http.Request ) {
339+ code := r .URL .Query ().Get ("code" )
340+ if code == "" {
341+ errMsg := "authorization failed: no code received"
342+ http .Error (w , errMsg , http .StatusBadRequest )
343+ errChan <- fmt .Errorf (errMsg )
344+ return
345+ }
346+ fmt .Fprintf (w , "Authorization successful! You can close this tab." )
347+ codeChan <- code
348+ go func () {
349+ time .Sleep (1 * time .Second )
350+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
351+ defer cancel ()
352+ if err := server .Shutdown (ctx ); err != nil {
353+ log .Printf ("HTTP server Shutdown error: %v" , err )
354+ }
355+ }()
356+ })
357+
358+ go func () {
359+ if err := server .ListenAndServe (); err != http .ErrServerClosed {
360+ errChan <- err
361+ }
362+ }()
363+
364+ openBrowser (authURL )
365+
366+ select {
367+ case code := <- codeChan :
368+ return code , nil
369+ case err := <- errChan :
370+ return "" , err
371+ case <- time .After (5 * time .Minute ):
372+ return "" , fmt .Errorf ("timed out waiting for authorization code" )
373+ }
374+ }
375+
376+ func generateSecureRandomString (length int ) (string , error ) {
377+ b := make ([]byte , length )
378+ _ , err := rand .Read (b )
379+ if err != nil {
380+ return "" , err
381+ }
382+ return base64 .URLEncoding .EncodeToString (b ), nil
383+ }
384+
385+ func openBrowser (url string ) {
386+ var err error
387+ switch runtime .GOOS {
388+ case "linux" :
389+ err = exec .Command ("xdg-open" , url ).Start ()
390+ case "windows" :
391+ err = exec .Command ("rundll32" , "url.dll,FileProtocolHandler" , url ).Start ()
392+ case "darwin" :
393+ err = exec .Command ("open" , url ).Start ()
394+ default :
395+ err = fmt .Errorf ("unsupported platform" )
396+ }
397+ if err != nil {
398+ log .Printf ("Failed to open browser: %v" , err )
399+ }
400+ }
0 commit comments