github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/session_token_login_provider.go (about) 1 // Copyright 2024 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api 5 6 import ( 7 "context" 8 9 "github.com/juju/errors" 10 "github.com/juju/names/v5" 11 "github.com/juju/version/v2" 12 13 "github.com/juju/juju/api/base" 14 "github.com/juju/juju/rpc/params" 15 ) 16 17 var ( 18 loginDeviceAPICall = func(caller base.APICaller, request interface{}, response interface{}) error { 19 return caller.APICall("Admin", 4, "", "LoginDevice", request, response) 20 } 21 getDeviceSessionTokenAPICall = func(caller base.APICaller, request interface{}, response interface{}) error { 22 return caller.APICall("Admin", 4, "", "GetDeviceSessionToken", request, response) 23 } 24 loginWithSessionTokenAPICall = func(caller base.APICaller, request interface{}, response interface{}) error { 25 return caller.APICall("Admin", 4, "", "LoginWithSessionToken", request, response) 26 } 27 ) 28 29 // NewSessionTokenLoginProvider returns a LoginProvider implementation that 30 // authenticates the entity with the session token. 31 func NewSessionTokenLoginProvider( 32 token string, 33 printOutputFunc func(string, ...any) error, 34 updateAccountDetailsFunc func(string) error, 35 ) *sessionTokenLoginProvider { 36 return &sessionTokenLoginProvider{ 37 sessionToken: token, 38 printOutputFunc: printOutputFunc, 39 updateAccountDetailsFunc: updateAccountDetailsFunc, 40 } 41 } 42 43 type sessionTokenLoginProvider struct { 44 sessionToken string 45 // printOutpuFunc is used by the login provider to print the user code 46 // and verification URL. 47 printOutputFunc func(string, ...any) error 48 // updateAccountDetailsFunc function is used to update the session 49 // token for the account details. 50 updateAccountDetailsFunc func(string) error 51 } 52 53 // Login implements the LoginProvider.Login method. 54 // 55 // It authenticates as the entity using the specified session token. 56 // Subsequent requests on the state will act as that entity. 57 func (p *sessionTokenLoginProvider) Login(ctx context.Context, caller base.APICaller) (*LoginResultParams, error) { 58 // First we try to log in using the session token we have. 59 result, err := p.login(ctx, caller) 60 if err == nil { 61 return result, nil 62 } 63 64 if params.ErrCode(err) == params.CodeUnauthorized { 65 // if we fail with an "unauthorized" error, we initiate a 66 // new device login. 67 if err := p.initiateDeviceLogin(ctx, caller); err != nil { 68 return nil, errors.Trace(err) 69 } 70 // and retry the login using the obtained session token. 71 return p.login(ctx, caller) 72 } 73 return nil, errors.Trace(err) 74 } 75 76 func (p *sessionTokenLoginProvider) initiateDeviceLogin(ctx context.Context, caller base.APICaller) error { 77 if p.printOutputFunc == nil { 78 return errors.New("cannot present login details") 79 } 80 81 type loginRequest struct{} 82 83 var deviceResult struct { 84 UserCode string `json:"user-code"` 85 VerificationURI string `json:"verification-uri"` 86 } 87 88 // The first call we make is to initiate the device login oauth2 flow. This will 89 // return a user code and the verification URL - verification URL will point to the 90 // configured IdP. These two will be presented to the user. User will have to 91 // open a browser, visit the verification URL, enter the user code and log in. 92 err := loginDeviceAPICall(caller, &loginRequest{}, &deviceResult) 93 if err != nil { 94 return errors.Trace(err) 95 } 96 97 // We print the verification URL and the user code. 98 err = p.printOutputFunc("Please visit %s and enter code %s to log in.", deviceResult.VerificationURI, deviceResult.UserCode) 99 if err != nil { 100 return errors.Trace(err) 101 } 102 103 type loginResponse struct { 104 SessionToken string `json:"session-token"` 105 } 106 var sessionTokenResult loginResponse 107 // Then we make a blocking call to get the session token. 108 err = getDeviceSessionTokenAPICall(caller, &loginRequest{}, &sessionTokenResult) 109 if err != nil { 110 return errors.Trace(err) 111 } 112 113 p.sessionToken = sessionTokenResult.SessionToken 114 115 return p.updateAccountDetailsFunc(sessionTokenResult.SessionToken) 116 } 117 118 func (p *sessionTokenLoginProvider) login(ctx context.Context, caller base.APICaller) (*LoginResultParams, error) { 119 var result params.LoginResult 120 request := struct { 121 SessionToken string `json:"session-token"` 122 }{ 123 SessionToken: p.sessionToken, 124 } 125 126 err := loginWithSessionTokenAPICall(caller, request, &result) 127 if err != nil { 128 return nil, errors.Trace(err) 129 } 130 131 var controllerAccess string 132 var modelAccess string 133 var tag names.Tag 134 if result.UserInfo != nil { 135 tag, err = names.ParseTag(result.UserInfo.Identity) 136 if err != nil { 137 return nil, errors.Trace(err) 138 } 139 controllerAccess = result.UserInfo.ControllerAccess 140 modelAccess = result.UserInfo.ModelAccess 141 } 142 servers := params.ToMachineHostsPorts(result.Servers) 143 serverVersion, err := version.Parse(result.ServerVersion) 144 if err != nil { 145 return nil, errors.Trace(err) 146 } 147 return &LoginResultParams{ 148 tag: tag, 149 modelTag: result.ModelTag, 150 controllerTag: result.ControllerTag, 151 servers: servers, 152 publicDNSName: result.PublicDNSName, 153 facades: result.Facades, 154 modelAccess: modelAccess, 155 controllerAccess: controllerAccess, 156 serverVersion: serverVersion, 157 }, nil 158 }