github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/authentication/interactor.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package authentication 5 6 import ( 7 "context" 8 "encoding/json" 9 "net/http" 10 "net/url" 11 12 "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 13 "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" 14 "github.com/juju/errors" 15 "gopkg.in/httprequest.v1" 16 ) 17 18 const authMethod = "juju_userpass" 19 20 // Interactor is a httpbakery.Interactor that will login directly 21 // to the Juju controller using password authentication. This 22 // only applies when logging in as a local user. 23 type Interactor struct { 24 username string 25 getPassword func(string) (string, error) 26 } 27 28 // NewInteractor returns a new Interactor. 29 func NewInteractor(username string, getPassword func(string) (string, error)) httpbakery.Interactor { 30 return &Interactor{ 31 username: username, 32 getPassword: getPassword, 33 } 34 } 35 36 // Kind implements httpbakery.Interactor.Kind. 37 func (i Interactor) Kind() string { 38 return authMethod 39 } 40 41 // Interact implements httpbakery.Interactor for the Interactor. 42 func (i Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { 43 var p form.InteractionInfo 44 if err := interactionRequiredErr.InteractionMethod(authMethod, &p); err != nil { 45 return nil, errors.Trace(err) 46 } 47 if p.URL == "" { 48 return nil, errors.New("no URL found in form information") 49 } 50 schemaURL, err := relativeURL(location, p.URL) 51 if err != nil { 52 return nil, errors.Annotatef(err, "invalid url %q", p.URL) 53 } 54 httpReqClient := &httprequest.Client{ 55 Doer: client, 56 } 57 password, err := i.getPassword(i.username) 58 if err != nil { 59 return nil, errors.Trace(err) 60 } 61 lr := form.LoginRequest{ 62 Body: form.LoginBody{ 63 Form: map[string]interface{}{ 64 "user": i.username, 65 "password": password, 66 }, 67 }, 68 } 69 var lresp form.LoginResponse 70 if err := httpReqClient.CallURL(ctx, schemaURL.String(), &lr, &lresp); err != nil { 71 return nil, errors.Annotate(err, "cannot submit form") 72 } 73 if lresp.Token == nil { 74 return nil, errors.New("no token found in form response") 75 } 76 return lresp.Token, nil 77 } 78 79 // relativeURL returns newPath relative to an original URL. 80 func relativeURL(base, new string) (*url.URL, error) { 81 if new == "" { 82 return nil, errors.New("empty URL") 83 } 84 baseURL, err := url.Parse(base) 85 if err != nil { 86 return nil, errors.Annotate(err, "cannot parse URL") 87 } 88 newURL, err := url.Parse(new) 89 if err != nil { 90 return nil, errors.Annotate(err, "cannot parse URL") 91 } 92 return baseURL.ResolveReference(newURL), nil 93 } 94 95 // LegacyInteract implements httpbakery.LegacyInteractor for the Interactor. 96 func (i *Interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, methodURL *url.URL) error { 97 password, err := i.getPassword(i.username) 98 if err != nil { 99 return err 100 } 101 102 // POST to the URL with username and password. 103 resp, err := client.PostForm(methodURL.String(), url.Values{ 104 "user": {i.username}, 105 "password": {password}, 106 }) 107 if err != nil { 108 return err 109 } 110 defer resp.Body.Close() 111 112 if resp.StatusCode == http.StatusOK { 113 return nil 114 } 115 var jsonError httpbakery.Error 116 if err := json.NewDecoder(resp.Body).Decode(&jsonError); err != nil { 117 return errors.Annotate(err, "unmarshalling error") 118 } 119 return &jsonError 120 } 121 122 // NewNotSupportedInteractor returns an interactor that does 123 // not support any discharge workflow. 124 func NewNotSupportedInteractor() httpbakery.Interactor { 125 return ¬SupportedInteractor{} 126 } 127 128 type notSupportedInteractor struct { 129 } 130 131 // Kind implements httpbakery.Interactor for the Interactor. 132 func (i notSupportedInteractor) Kind() string { 133 return authMethod 134 } 135 136 // Interact implements httpbakery.Interactor for the Interactor. 137 func (i notSupportedInteractor) Interact(_ context.Context, _ *httpbakery.Client, location string, _ *httpbakery.Error) (*httpbakery.DischargeToken, error) { 138 return nil, errors.NotSupportedf("interaction for %s", location) 139 }