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 }