Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing AzureCLI / CloudShell authentication #169

Merged
merged 5 commits into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 0 additions & 59 deletions autorest/adal/cli.go

This file was deleted.

39 changes: 0 additions & 39 deletions autorest/adal/persist.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package adal

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/dimchansky/utfbom"
)

// LoadToken restores a Token object from a file located at 'path'.
Expand All @@ -28,42 +25,6 @@ func LoadToken(path string) (*Token, error) {
return &token, nil
}

// LoadCLITokens restores a set of AzureCLIToken objects from a file located at 'path'.
func LoadCLITokens(path string) ([]AzureCLIToken, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
}
defer file.Close()

var tokens []AzureCLIToken

dec := json.NewDecoder(file)
if err = dec.Decode(&tokens); err != nil {
return nil, fmt.Errorf("failed to decode contents of file (%s) into a AzureCLIToken representation: %v", path, err)
}

return tokens, nil
}

// LoadCLIProfile restores an AzureCLIProfile object from a file located at 'path'.
func LoadCLIProfile(path string) (AzureCLIProfile, error) {
var profile AzureCLIProfile

contents, err := ioutil.ReadFile(path)
if err != nil {
return profile, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
}
reader := utfbom.SkipOnly(bytes.NewReader(contents))

dec := json.NewDecoder(reader)
if err = dec.Decode(&profile); err != nil {
return profile, fmt.Errorf("failed to decode contents of file (%s) into a AzureCLIProfile representation: %v", path, err)
}

return profile, nil
}

// SaveToken persists an oauth token at the given location on disk.
// It moves the new file into place so it can safely be used to replace an existing file
// that maybe accessed by multiple processes.
Expand Down
13 changes: 5 additions & 8 deletions autorest/adal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import (
"strings"
"time"

"github.com/Azure/go-autorest/autorest/date"
"github.com/dgrijalva/jwt-go"
)

const (
defaultRefresh = 5 * time.Minute
tokenBaseDate = "1970-01-01T00:00:00Z"

// OAuthGrantTypeDeviceCode is the "grant_type" identifier used in device flow
OAuthGrantTypeDeviceCode = "device_code"
Expand All @@ -35,12 +35,6 @@ const (
managedIdentitySettingsPath = "/var/lib/waagent/ManagedIdentity-Settings"
)

var expirationBase time.Time

func init() {
expirationBase, _ = time.Parse(time.RFC3339, tokenBaseDate)
}

// OAuthTokenProvider is an interface which should be implemented by an access token retriever
type OAuthTokenProvider interface {
OAuthToken() string
Expand Down Expand Up @@ -76,7 +70,10 @@ func (t Token) Expires() time.Time {
if err != nil {
s = -3600
}
return expirationBase.Add(time.Duration(s) * time.Second).UTC()

expiration := date.NewUnixTimeFromSeconds(float64(s))

return time.Time(expiration).UTC()
}

// IsExpired returns true if the Token is expired, false otherwise.
Expand Down
9 changes: 5 additions & 4 deletions autorest/adal/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"testing"
"time"

"github.com/Azure/go-autorest/autorest/date"
"github.com/Azure/go-autorest/autorest/mocks"
)

Expand Down Expand Up @@ -342,7 +343,7 @@ func TestServicePrincipalTokenRefreshReturnsErrorIfNotOk(t *testing.T) {
func TestServicePrincipalTokenRefreshUnmarshals(t *testing.T) {
spt := newServicePrincipalToken()

expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds()))
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))
j := newTokenJSON(expiresOn, "resource")
resp := mocks.NewResponseWithContent(j)
c := mocks.NewSender()
Expand Down Expand Up @@ -430,7 +431,7 @@ func TestRefreshCallback(t *testing.T) {
return nil
})

expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds()))
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))

sender := mocks.NewSender()
j := newTokenJSON(expiresOn, "resource")
Expand All @@ -451,7 +452,7 @@ func TestRefreshCallbackErrorPropagates(t *testing.T) {
return fmt.Errorf(errorText)
})

expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds()))
expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds()))

sender := mocks.NewSender()
j := newTokenJSON(expiresOn, "resource")
Expand Down Expand Up @@ -554,7 +555,7 @@ func expireToken(t *Token) *Token {

func setTokenToExpireAt(t *Token, expireAt time.Time) *Token {
t.ExpiresIn = "3600"
t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(expirationBase).Seconds()))
t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(date.UnixEpoch()).Seconds()))
t.NotBefore = t.ExpiresOn
return t
}
Expand Down
51 changes: 51 additions & 0 deletions autorest/azure/cli/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"

"github.com/dimchansky/utfbom"
"github.com/mitchellh/go-homedir"
)

// Profile represents a Profile from the Azure CLI
type Profile struct {
InstallationID string `json:"installationId"`
Subscriptions []Subscription `json:"subscriptions"`
}

// Subscription represents a Subscription from the Azure CLI
type Subscription struct {
EnvironmentName string `json:"environmentName"`
ID string `json:"id"`
IsDefault bool `json:"isDefault"`
Name string `json:"name"`
State string `json:"state"`
TenantID string `json:"tenantId"`
}

// ProfilePath returns the path where the Azure Profile is stored from the Azure CLI
func ProfilePath() (string, error) {
return homedir.Expand("~/.azure/azureProfile.json")
}

// LoadProfile restores a Profile object from a file located at 'path'.
func LoadProfile(path string) (result Profile, err error) {
var contents []byte
contents, err = ioutil.ReadFile(path)
if err != nil {
err = fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
return
}
reader := utfbom.SkipOnly(bytes.NewReader(contents))

dec := json.NewDecoder(reader)
if err = dec.Decode(&result); err != nil {
err = fmt.Errorf("failed to decode contents of file (%s) into a Profile representation: %v", path, err)
return
}

return
}
89 changes: 89 additions & 0 deletions autorest/azure/cli/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package cli

import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/date"
"github.com/mitchellh/go-homedir"
)

// Token represents an AccessToken from the Azure CLI
type Token struct {
AccessToken string `json:"accessToken"`
Authority string `json:"_authority"`
ClientID string `json:"_clientId"`
ExpiresOn string `json:"expiresOn"`
IdentityProvider string `json:"identityProvider"`
IsMRRT bool `json:"isMRRT"`
RefreshToken string `json:"refreshToken"`
Resource string `json:"resource"`
TokenType string `json:"tokenType"`
UserID string `json:"userId"`
}

// ToADALToken converts an Azure CLI `Token`` to an `adal.Token``
func (t Token) ToADALToken() (converted adal.Token, err error) {
tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn)
if err != nil {
err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
return
}

difference := tokenExpirationDate.Sub(date.UnixEpoch())

converted = adal.Token{
AccessToken: t.AccessToken,
Type: t.TokenType,
ExpiresIn: "3600",
ExpiresOn: strconv.Itoa(int(difference.Seconds())),
RefreshToken: t.RefreshToken,
Resource: t.Resource,
}
return
}

// AccessTokensPath returns the path where access tokens are stored from the Azure CLI
func AccessTokensPath() (string, error) {
return homedir.Expand("~/.azure/accessTokens.json")
}

// ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object
func ParseExpirationDate(input string) (*time.Time, error) {
// CloudShell (and potentially the Azure CLI in future)
expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
if cloudShellErr != nil {
// Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone)
const cliFormat = "2006-01-02 15:04:05.999999"
expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
if cliErr == nil {
return &expirationDate, nil
}

return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
}

return &expirationDate, nil
}

// LoadTokens restores a set of Token objects from a file located at 'path'.
func LoadTokens(path string) ([]Token, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
}
defer file.Close()

var tokens []Token

dec := json.NewDecoder(file)
if err = dec.Decode(&tokens); err != nil {
return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err)
}

return tokens, nil
}