github.com/Venafi/vcert/v5@v5.10.2/pkg/venafi/firefly/deviceFlow.go (about)

     1  /*
     2   * Copyright 2023 Venafi, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *  http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package firefly
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"time"
    25  
    26  	"github.com/Venafi/vcert/v5/pkg/endpoint"
    27  	"github.com/Venafi/vcert/v5/pkg/verror"
    28  	"golang.org/x/oauth2"
    29  )
    30  
    31  // DeviceCred It's the representation of the info returned when a Device Code is requested
    32  // to the OAuth 2.0 Identity Provider to request an access code
    33  type DeviceCred struct {
    34  	DeviceCode      string `json:"device_code"`
    35  	UserCode        string `json:"user_code"`
    36  	VerificationURL string `json:"verification_url"` //Google use this to return the URL to share to the user
    37  	VerificationURI string `json:"verification_uri"` // others like Okta, Auth0 and WSO2 use this one to return the URI to share to the user
    38  	Interval        int64  `json:"interval"`
    39  	ExpiresIn       int64  `json:"expires_in"`
    40  }
    41  
    42  func (c *Connector) getDeviceAccessToken(auth *endpoint.Authentication) (token *oauth2.Token, err error) {
    43  
    44  	//requesting the device code
    45  	devCred, err := c.requestDeviceCode(auth)
    46  
    47  	if err != nil {
    48  		return
    49  	}
    50  
    51  	//setting as default verificationURL the value returned in VerificationURI given that is the used for the most of the
    52  	// OAuth IdP like Okta, Auth0 and WSO2 return the verification_uri to show to the user
    53  	verificationURL := devCred.VerificationURI
    54  	//if that is empty then trying to use the VerificationURL given google uses verification_url to
    55  	//return the verification_uri to show to the user
    56  	if verificationURL == "" {
    57  		verificationURL = devCred.VerificationURL
    58  	}
    59  
    60  	fmt.Printf("Please open de following URL in your web browser:\n\n%v\n\n and then enter the code:\n\n%v\n\nIt will expire in %dm and %ds\n\n", verificationURL, devCred.UserCode, devCred.ExpiresIn/60, devCred.ExpiresIn%60)
    61  
    62  	//waiting for the user authorization
    63  	token, err = c.waitForDeviceAuthorization(devCred, auth)
    64  	if err == nil {
    65  		fmt.Println("Successfully authorized device.")
    66  	}
    67  	return
    68  }
    69  
    70  func (c *Connector) requestDeviceCode(auth *endpoint.Authentication) (*DeviceCred, error) {
    71  	data := url.Values{
    72  		"client_id": {auth.ClientId},
    73  	}
    74  
    75  	//There are IdPs like Okta and Auth0 which provides the support for default scopes, so it's possible for
    76  	//these that the scope is empty
    77  	if auth.Scope != "" {
    78  		data.Add("scope", auth.Scope)
    79  	}
    80  
    81  	//audience is only supported by Okta and Auth0
    82  	if auth.IdentityProvider.Audience != "" {
    83  		data.Add("audience", auth.IdentityProvider.Audience)
    84  	}
    85  
    86  	statusCode, status, body, err := c.request("POST", urlResource(auth.IdentityProvider.DeviceURL), data)
    87  
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	//parsing the response
    92  	return parseDeviceCodeRequestResult(statusCode, status, body)
    93  }
    94  
    95  func parseDeviceCodeRequestResult(httpStatusCode int, httpStatus string, body []byte) (*DeviceCred, error) {
    96  	switch httpStatusCode {
    97  	case http.StatusOK:
    98  		return parseDeviceCodeRequestData(body)
    99  	default:
   100  		respError, err := NewResponseError(body)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  
   105  		return nil, fmt.Errorf("unexpected status code requesting Device Code. Status: %s error: %w", httpStatus, respError)
   106  	}
   107  }
   108  
   109  func parseDeviceCodeRequestData(b []byte) (*DeviceCred, error) {
   110  	var data DeviceCred
   111  	err := json.Unmarshal(b, &data)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("%w: %v", verror.ServerError, err)
   114  	}
   115  
   116  	return &data, nil
   117  }
   118  
   119  func (c *Connector) waitForDeviceAuthorization(devCred *DeviceCred, auth *endpoint.Authentication) (*oauth2.Token, error) {
   120  
   121  	data := url.Values{"client_id": {auth.ClientId},
   122  		"device_code": {devCred.DeviceCode},
   123  		"grant_type":  {"urn:ietf:params:oauth:grant-type:device_code"},
   124  	}
   125  
   126  	// Google requires the client-secret also to request the accessToken
   127  	if auth.ClientSecret != "" {
   128  		data.Add("client_secret", auth.ClientSecret)
   129  	}
   130  
   131  	//polling the authorization
   132  	for {
   133  		//requesting the authorization
   134  		statusCode, _, body, err := c.request("POST", urlResource(auth.IdentityProvider.TokenURL), data)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  
   139  		//parsing the response
   140  		token, err := parseWaitingDeviceAuthorizationRequestResult(statusCode, body)
   141  
   142  		//if there is not any error, then the token was gotten
   143  		if err == nil {
   144  			return token, err
   145  		}
   146  
   147  		//verifying the error gotten
   148  		switch GetDevAuthStatusFromError(err) {
   149  		case AuthorizationPending:
   150  			time.Sleep(time.Duration(devCred.Interval) * time.Second)
   151  		case SlowDown:
   152  			devCred.Interval += 5
   153  			time.Sleep(time.Duration(devCred.Interval) * time.Second)
   154  		case AccessDenied:
   155  			return nil, fmt.Errorf("the access from device was denied by the user")
   156  		case ExpiredToken:
   157  			return nil, fmt.Errorf("the device code expired")
   158  		default:
   159  			return nil, fmt.Errorf("the authorization failed. %w", err)
   160  		}
   161  	}
   162  }
   163  
   164  func parseWaitingDeviceAuthorizationRequestResult(httpStatusCode int, body []byte) (*oauth2.Token, error) {
   165  	switch httpStatusCode {
   166  	case http.StatusOK:
   167  		//Based on the oauth2.internal.tokenJSON which complains the
   168  		// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
   169  		type jsonToken struct {
   170  			AccessToken  string `json:"access_token"`
   171  			TokenType    string `json:"token_type,omitempty"`
   172  			RefreshToken string `json:"refresh_token,omitempty"`
   173  			ExpiresIn    int32  `json:"expires_in,omitempty"`
   174  			Scope        string `json:"scope,omitempty"`
   175  		}
   176  
   177  		var data jsonToken
   178  		err := json.Unmarshal(body, &data)
   179  		if err != nil {
   180  			return nil, fmt.Errorf("%w: %v", verror.ServerError, err)
   181  		}
   182  
   183  		//creating an oauth2.Token and matching it with the deviceAccessToken gotten
   184  		token := oauth2.Token{
   185  			AccessToken:  data.AccessToken,
   186  			TokenType:    data.TokenType,
   187  			RefreshToken: data.RefreshToken,
   188  		}
   189  
   190  		if data.ExpiresIn > 0 {
   191  			token.Expiry = time.Now().Add(time.Duration(data.ExpiresIn) * time.Second)
   192  		}
   193  
   194  		return &token, nil
   195  	default:
   196  		respError, err := NewResponseError(body)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  
   201  		return nil, respError
   202  	}
   203  }