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  }