Blame vendor/github.com/Azure/go-autorest/autorest/adal/devicetoken.go

Packit Service 4d2de5
package adal
Packit Service 4d2de5
Packit Service 4d2de5
// Copyright 2017 Microsoft Corporation
Packit Service 4d2de5
//
Packit Service 4d2de5
//  Licensed under the Apache License, Version 2.0 (the "License");
Packit Service 4d2de5
//  you may not use this file except in compliance with the License.
Packit Service 4d2de5
//  You may obtain a copy of the License at
Packit Service 4d2de5
//
Packit Service 4d2de5
//      http://www.apache.org/licenses/LICENSE-2.0
Packit Service 4d2de5
//
Packit Service 4d2de5
//  Unless required by applicable law or agreed to in writing, software
Packit Service 4d2de5
//  distributed under the License is distributed on an "AS IS" BASIS,
Packit Service 4d2de5
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Packit Service 4d2de5
//  See the License for the specific language governing permissions and
Packit Service 4d2de5
//  limitations under the License.
Packit Service 4d2de5
Packit Service 4d2de5
/*
Packit Service 4d2de5
  This file is largely based on rjw57/oauth2device's code, with the follow differences:
Packit Service 4d2de5
   * scope -> resource, and only allow a single one
Packit Service 4d2de5
   * receive "Message" in the DeviceCode struct and show it to users as the prompt
Packit Service 4d2de5
   * azure-xplat-cli has the following behavior that this emulates:
Packit Service 4d2de5
     - does not send client_secret during the token exchange
Packit Service 4d2de5
     - sends resource again in the token exchange request
Packit Service 4d2de5
*/
Packit Service 4d2de5
Packit Service 4d2de5
import (
Packit Service 4d2de5
	"context"
Packit Service 4d2de5
	"encoding/json"
Packit Service 4d2de5
	"fmt"
Packit Service 4d2de5
	"io/ioutil"
Packit Service 4d2de5
	"net/http"
Packit Service 4d2de5
	"net/url"
Packit Service 4d2de5
	"strings"
Packit Service 4d2de5
	"time"
Packit Service 4d2de5
)
Packit Service 4d2de5
Packit Service 4d2de5
const (
Packit Service 4d2de5
	logPrefix = "autorest/adal/devicetoken:"
Packit Service 4d2de5
)
Packit Service 4d2de5
Packit Service 4d2de5
var (
Packit Service 4d2de5
	// ErrDeviceGeneric represents an unknown error from the token endpoint when using device flow
Packit Service 4d2de5
	ErrDeviceGeneric = fmt.Errorf("%s Error while retrieving OAuth token: Unknown Error", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrDeviceAccessDenied represents an access denied error from the token endpoint when using device flow
Packit Service 4d2de5
	ErrDeviceAccessDenied = fmt.Errorf("%s Error while retrieving OAuth token: Access Denied", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrDeviceAuthorizationPending represents the server waiting on the user to complete the device flow
Packit Service 4d2de5
	ErrDeviceAuthorizationPending = fmt.Errorf("%s Error while retrieving OAuth token: Authorization Pending", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrDeviceCodeExpired represents the server timing out and expiring the code during device flow
Packit Service 4d2de5
	ErrDeviceCodeExpired = fmt.Errorf("%s Error while retrieving OAuth token: Code Expired", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrDeviceSlowDown represents the service telling us we're polling too often during device flow
Packit Service 4d2de5
	ErrDeviceSlowDown = fmt.Errorf("%s Error while retrieving OAuth token: Slow Down", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrDeviceCodeEmpty represents an empty device code from the device endpoint while using device flow
Packit Service 4d2de5
	ErrDeviceCodeEmpty = fmt.Errorf("%s Error while retrieving device code: Device Code Empty", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	// ErrOAuthTokenEmpty represents an empty OAuth token from the token endpoint when using device flow
Packit Service 4d2de5
	ErrOAuthTokenEmpty = fmt.Errorf("%s Error while retrieving OAuth token: Token Empty", logPrefix)
Packit Service 4d2de5
Packit Service 4d2de5
	errCodeSendingFails   = "Error occurred while sending request for Device Authorization Code"
Packit Service 4d2de5
	errCodeHandlingFails  = "Error occurred while handling response from the Device Endpoint"
Packit Service 4d2de5
	errTokenSendingFails  = "Error occurred while sending request with device code for a token"
Packit Service 4d2de5
	errTokenHandlingFails = "Error occurred while handling response from the Token Endpoint (during device flow)"
Packit Service 4d2de5
	errStatusNotOK        = "Error HTTP status != 200"
Packit Service 4d2de5
)
Packit Service 4d2de5
Packit Service 4d2de5
// DeviceCode is the object returned by the device auth endpoint
Packit Service 4d2de5
// It contains information to instruct the user to complete the auth flow
Packit Service 4d2de5
type DeviceCode struct {
Packit Service 4d2de5
	DeviceCode      *string `json:"device_code,omitempty"`
Packit Service 4d2de5
	UserCode        *string `json:"user_code,omitempty"`
Packit Service 4d2de5
	VerificationURL *string `json:"verification_url,omitempty"`
Packit Service 4d2de5
	ExpiresIn       *int64  `json:"expires_in,string,omitempty"`
Packit Service 4d2de5
	Interval        *int64  `json:"interval,string,omitempty"`
Packit Service 4d2de5
Packit Service 4d2de5
	Message     *string `json:"message"` // Azure specific
Packit Service 4d2de5
	Resource    string  // store the following, stored when initiating, used when exchanging
Packit Service 4d2de5
	OAuthConfig OAuthConfig
Packit Service 4d2de5
	ClientID    string
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// TokenError is the object returned by the token exchange endpoint
Packit Service 4d2de5
// when something is amiss
Packit Service 4d2de5
type TokenError struct {
Packit Service 4d2de5
	Error            *string `json:"error,omitempty"`
Packit Service 4d2de5
	ErrorCodes       []int   `json:"error_codes,omitempty"`
Packit Service 4d2de5
	ErrorDescription *string `json:"error_description,omitempty"`
Packit Service 4d2de5
	Timestamp        *string `json:"timestamp,omitempty"`
Packit Service 4d2de5
	TraceID          *string `json:"trace_id,omitempty"`
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// DeviceToken is the object return by the token exchange endpoint
Packit Service 4d2de5
// It can either look like a Token or an ErrorToken, so put both here
Packit Service 4d2de5
// and check for presence of "Error" to know if we are in error state
Packit Service 4d2de5
type deviceToken struct {
Packit Service 4d2de5
	Token
Packit Service 4d2de5
	TokenError
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// InitiateDeviceAuth initiates a device auth flow. It returns a DeviceCode
Packit Service 4d2de5
// that can be used with CheckForUserCompletion or WaitForUserCompletion.
Packit Service 4d2de5
// Deprecated: use InitiateDeviceAuthWithContext() instead.
Packit Service 4d2de5
func InitiateDeviceAuth(sender Sender, oauthConfig OAuthConfig, clientID, resource string) (*DeviceCode, error) {
Packit Service 4d2de5
	return InitiateDeviceAuthWithContext(context.Background(), sender, oauthConfig, clientID, resource)
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// InitiateDeviceAuthWithContext initiates a device auth flow. It returns a DeviceCode
Packit Service 4d2de5
// that can be used with CheckForUserCompletion or WaitForUserCompletion.
Packit Service 4d2de5
func InitiateDeviceAuthWithContext(ctx context.Context, sender Sender, oauthConfig OAuthConfig, clientID, resource string) (*DeviceCode, error) {
Packit Service 4d2de5
	v := url.Values{
Packit Service 4d2de5
		"client_id": []string{clientID},
Packit Service 4d2de5
		"resource":  []string{resource},
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	s := v.Encode()
Packit Service 4d2de5
	body := ioutil.NopCloser(strings.NewReader(s))
Packit Service 4d2de5
Packit Service 4d2de5
	req, err := http.NewRequest(http.MethodPost, oauthConfig.DeviceCodeEndpoint.String(), body)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	req.ContentLength = int64(len(s))
Packit Service 4d2de5
	req.Header.Set(contentType, mimeTypeFormPost)
Packit Service 4d2de5
	resp, err := sender.Do(req.WithContext(ctx))
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
	defer resp.Body.Close()
Packit Service 4d2de5
Packit Service 4d2de5
	rb, err := ioutil.ReadAll(resp.Body)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	if resp.StatusCode != http.StatusOK {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, errStatusNotOK)
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	if len(strings.Trim(string(rb), " ")) == 0 {
Packit Service 4d2de5
		return nil, ErrDeviceCodeEmpty
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	var code DeviceCode
Packit Service 4d2de5
	err = json.Unmarshal(rb, &code)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	code.ClientID = clientID
Packit Service 4d2de5
	code.Resource = resource
Packit Service 4d2de5
	code.OAuthConfig = oauthConfig
Packit Service 4d2de5
Packit Service 4d2de5
	return &code, nil
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// CheckForUserCompletion takes a DeviceCode and checks with the Azure AD OAuth endpoint
Packit Service 4d2de5
// to see if the device flow has: been completed, timed out, or otherwise failed
Packit Service 4d2de5
// Deprecated: use CheckForUserCompletionWithContext() instead.
Packit Service 4d2de5
func CheckForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
Packit Service 4d2de5
	return CheckForUserCompletionWithContext(context.Background(), sender, code)
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// CheckForUserCompletionWithContext takes a DeviceCode and checks with the Azure AD OAuth endpoint
Packit Service 4d2de5
// to see if the device flow has: been completed, timed out, or otherwise failed
Packit Service 4d2de5
func CheckForUserCompletionWithContext(ctx context.Context, sender Sender, code *DeviceCode) (*Token, error) {
Packit Service 4d2de5
	v := url.Values{
Packit Service 4d2de5
		"client_id":  []string{code.ClientID},
Packit Service 4d2de5
		"code":       []string{*code.DeviceCode},
Packit Service 4d2de5
		"grant_type": []string{OAuthGrantTypeDeviceCode},
Packit Service 4d2de5
		"resource":   []string{code.Resource},
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	s := v.Encode()
Packit Service 4d2de5
	body := ioutil.NopCloser(strings.NewReader(s))
Packit Service 4d2de5
Packit Service 4d2de5
	req, err := http.NewRequest(http.MethodPost, code.OAuthConfig.TokenEndpoint.String(), body)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	req.ContentLength = int64(len(s))
Packit Service 4d2de5
	req.Header.Set(contentType, mimeTypeFormPost)
Packit Service 4d2de5
	resp, err := sender.Do(req.WithContext(ctx))
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
	defer resp.Body.Close()
Packit Service 4d2de5
Packit Service 4d2de5
	rb, err := ioutil.ReadAll(resp.Body)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	if resp.StatusCode != http.StatusOK && len(strings.Trim(string(rb), " ")) == 0 {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, errStatusNotOK)
Packit Service 4d2de5
	}
Packit Service 4d2de5
	if len(strings.Trim(string(rb), " ")) == 0 {
Packit Service 4d2de5
		return nil, ErrOAuthTokenEmpty
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	var token deviceToken
Packit Service 4d2de5
	err = json.Unmarshal(rb, &token)
Packit Service 4d2de5
	if err != nil {
Packit Service 4d2de5
		return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	if token.Error == nil {
Packit Service 4d2de5
		return &token.Token, nil
Packit Service 4d2de5
	}
Packit Service 4d2de5
Packit Service 4d2de5
	switch *token.Error {
Packit Service 4d2de5
	case "authorization_pending":
Packit Service 4d2de5
		return nil, ErrDeviceAuthorizationPending
Packit Service 4d2de5
	case "slow_down":
Packit Service 4d2de5
		return nil, ErrDeviceSlowDown
Packit Service 4d2de5
	case "access_denied":
Packit Service 4d2de5
		return nil, ErrDeviceAccessDenied
Packit Service 4d2de5
	case "code_expired":
Packit Service 4d2de5
		return nil, ErrDeviceCodeExpired
Packit Service 4d2de5
	default:
Packit Service 4d2de5
		return nil, ErrDeviceGeneric
Packit Service 4d2de5
	}
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// WaitForUserCompletion calls CheckForUserCompletion repeatedly until a token is granted or an error state occurs.
Packit Service 4d2de5
// This prevents the user from looping and checking against 'ErrDeviceAuthorizationPending'.
Packit Service 4d2de5
// Deprecated: use WaitForUserCompletionWithContext() instead.
Packit Service 4d2de5
func WaitForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
Packit Service 4d2de5
	return WaitForUserCompletionWithContext(context.Background(), sender, code)
Packit Service 4d2de5
}
Packit Service 4d2de5
Packit Service 4d2de5
// WaitForUserCompletionWithContext calls CheckForUserCompletion repeatedly until a token is granted or an error
Packit Service 4d2de5
// state occurs.  This prevents the user from looping and checking against 'ErrDeviceAuthorizationPending'.
Packit Service 4d2de5
func WaitForUserCompletionWithContext(ctx context.Context, sender Sender, code *DeviceCode) (*Token, error) {
Packit Service 4d2de5
	intervalDuration := time.Duration(*code.Interval) * time.Second
Packit Service 4d2de5
	waitDuration := intervalDuration
Packit Service 4d2de5
Packit Service 4d2de5
	for {
Packit Service 4d2de5
		token, err := CheckForUserCompletionWithContext(ctx, sender, code)
Packit Service 4d2de5
Packit Service 4d2de5
		if err == nil {
Packit Service 4d2de5
			return token, nil
Packit Service 4d2de5
		}
Packit Service 4d2de5
Packit Service 4d2de5
		switch err {
Packit Service 4d2de5
		case ErrDeviceSlowDown:
Packit Service 4d2de5
			waitDuration += waitDuration
Packit Service 4d2de5
		case ErrDeviceAuthorizationPending:
Packit Service 4d2de5
			// noop
Packit Service 4d2de5
		default: // everything else is "fatal" to us
Packit Service 4d2de5
			return nil, err
Packit Service 4d2de5
		}
Packit Service 4d2de5
Packit Service 4d2de5
		if waitDuration > (intervalDuration * 3) {
Packit Service 4d2de5
			return nil, fmt.Errorf("%s Error waiting for user to complete device flow. Server told us to slow_down too much", logPrefix)
Packit Service 4d2de5
		}
Packit Service 4d2de5
Packit Service 4d2de5
		select {
Packit Service 4d2de5
		case <-time.After(waitDuration):
Packit Service 4d2de5
			// noop
Packit Service 4d2de5
		case <-ctx.Done():
Packit Service 4d2de5
			return nil, ctx.Err()
Packit Service 4d2de5
		}
Packit Service 4d2de5
	}
Packit Service 4d2de5
}