Skip to content

Commit

Permalink
Merge pull request #277 from AzureAD/release-0.4.0
Browse files Browse the repository at this point in the history
MSAL Go 0.4.0
  • Loading branch information
abhidnya13 authored Nov 18, 2021
2 parents f9e02e1 + 0602944 commit 97e99ae
Show file tree
Hide file tree
Showing 19 changed files with 1,440 additions and 44 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ jobs:
env :
clientId: ${{ secrets.LAB_APP_CLIENT_ID }}
clientSecret: ${{ secrets.LAB_APP_CLIENT_SECRET }}
oboConfidentialClientId: ${{ secrets.OBO_CONFIDENTIAL_APP_CLIENT_ID }}
oboConfidentialClientSecret: ${{ secrets.OBO_CONFIDENTIAL_APP_CLIENT_SECRET }}
oboPublicClientId: ${{ secrets.OBO_PUBLIC_APP_CLIENT_ID }}
CI: ${{secrets.ENABLECI}}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Acquiring tokens with MSAL Go follows this general three step pattern. There mig
* Initializing a public client:

```go
publicClientapp, err := public.New("client_id", public.WithAuthority("https://login.microsoftonline.com/Enter_The_Tenant_Name_Here"))
publicClientApp, err := public.New("client_id", public.WithAuthority("https://login.microsoftonline.com/Enter_The_Tenant_Name_Here"))
```

* Initializing a confidential client:
Expand Down
11 changes: 11 additions & 0 deletions apps/confidential/confidential.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,17 @@ func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string)
return cca.base.AuthResultFromToken(ctx, authParams, token, true)
}

// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
// Refer https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow.
func (cca Client) AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string) (AuthResult, error) {
params := base.AcquireTokenOnBehalfOfParameters{
Scopes: scopes,
UserAssertion: userAssertion,
Credential: cca.cred,
}
return cca.base.AcquireTokenOnBehalfOf(ctx, params)
}

// Account gets the account in the token cache with the specified homeAccountID.
func (cca Client) Account(homeAccountID string) Account {
return cca.base.Account(homeAccountID)
Expand Down
125 changes: 98 additions & 27 deletions apps/internal/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,27 @@ type manager interface {
RemoveAccount(account shared.Account, clientID string)
}

// partitionedManager provides an internal cache. It is defined to allow faking the cache in tests.
// In all production use it is a *storage.Manager.
type partitionedManager interface {
Read(ctx context.Context, authParameters authority.AuthParams, account shared.Account) (storage.TokenResponse, error)
Write(authParameters authority.AuthParams, tokenResponse accesstokens.TokenResponse) (shared.Account, error)
}

type noopCacheAccessor struct{}

func (n noopCacheAccessor) Replace(cache cache.Unmarshaler, key string) {}
func (n noopCacheAccessor) Export(cache cache.Marshaler, key string) {}

// AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache).
type AcquireTokenSilentParameters struct {
Scopes []string
Account shared.Account
RequestType accesstokens.AppType
Credential *accesstokens.Credential
IsAppCache bool
Scopes []string
Account shared.Account
RequestType accesstokens.AppType
Credential *accesstokens.Credential
IsAppCache bool
UserAssertion string
AuthorizationType authority.AuthorizeType
}

// AcquireTokenAuthCodeParameters contains the parameters required to acquire an access token using the auth code flow.
Expand All @@ -63,6 +72,12 @@ type AcquireTokenAuthCodeParameters struct {
Credential *accesstokens.Credential
}

type AcquireTokenOnBehalfOfParameters struct {
Scopes []string
Credential *accesstokens.Credential
UserAssertion string
}

// AuthResult contains the results of one token acquisition operation in PublicClientApplication
// or ConfidentialClientApplication. For details see https://aka.ms/msal-net-authenticationresult
type AuthResult struct {
Expand Down Expand Up @@ -112,8 +127,9 @@ func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Acco
// Client is a base client that provides access to common methods and primatives that
// can be used by multiple clients.
type Client struct {
Token *oauth.Client
manager manager // *storage.Manager or fakeManager in tests
Token *oauth.Client
manager manager // *storage.Manager or fakeManager in tests
pmanager partitionedManager // *storage.PartitionedManager or fakeManager in tests

AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
cacheAccessor cache.ExportReplace
Expand Down Expand Up @@ -150,6 +166,7 @@ func New(clientID string, authorityURI string, token *oauth.Client, options ...O
AuthParams: authParams,
cacheAccessor: noopCacheAccessor{},
manager: storage.New(token),
pmanager: storage.NewPartitionedManager(token),
}
for _, o := range options {
o(&client)
Expand Down Expand Up @@ -207,18 +224,33 @@ func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, s
func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilentParameters) (AuthResult, error) {
authParams := b.AuthParams // This is a copy, as we dont' have a pointer receiver and authParams is not a pointer.
authParams.Scopes = silent.Scopes
authParams.AuthorizationType = authority.ATRefreshToken
authParams.HomeaccountID = silent.Account.HomeAccountID

if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}

storageTokenResponse, err := b.manager.Read(ctx, authParams, silent.Account)
if err != nil {
return AuthResult{}, err
authParams.AuthorizationType = silent.AuthorizationType
authParams.UserAssertion = silent.UserAssertion

var storageTokenResponse storage.TokenResponse
var err error
if authParams.AuthorizationType == authority.ATOnBehalfOf {
if s, ok := b.pmanager.(cache.Serializer); ok {
suggestedCacheKey := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
storageTokenResponse, err = b.pmanager.Read(ctx, authParams, silent.Account)
if err != nil {
return AuthResult{}, err
}
} else {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
authParams.AuthorizationType = authority.ATRefreshToken
storageTokenResponse, err = b.manager.Read(ctx, authParams, silent.Account)
if err != nil {
return AuthResult{}, err
}
}

result, err := AuthResultFromStorage(storageTokenResponse)
Expand Down Expand Up @@ -267,20 +299,59 @@ func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams Acqui
return b.AuthResultFromToken(ctx, authParams, token, true)
}

// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams AcquireTokenOnBehalfOfParameters) (AuthResult, error) {
authParams := b.AuthParams // This is a copy, as we dont' have a pointer receiver and .AuthParams is not a pointer.
authParams.Scopes = onBehalfOfParams.Scopes
authParams.AuthorizationType = authority.ATOnBehalfOf
authParams.UserAssertion = onBehalfOfParams.UserAssertion

silentParameters := AcquireTokenSilentParameters{
Scopes: onBehalfOfParams.Scopes,
RequestType: accesstokens.ATConfidential,
Credential: onBehalfOfParams.Credential,
UserAssertion: onBehalfOfParams.UserAssertion,
AuthorizationType: authority.ATOnBehalfOf,
}
token, err := b.AcquireTokenSilent(ctx, silentParameters)
if err != nil {
fmt.Println("Acquire Token Silent failed ")
token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential)
if err != nil {
return AuthResult{}, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
}
return token, err
}

func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse, cacheWrite bool) (AuthResult, error) {
if !cacheWrite {
return NewAuthResult(token, shared.Account{})
}

if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := token.CacheKey(authParams)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}

account, err := b.manager.Write(authParams, token)
if err != nil {
return AuthResult{}, err
var account shared.Account
var err error
if authParams.AuthorizationType == authority.ATOnBehalfOf {
if s, ok := b.pmanager.(cache.Serializer); ok {
suggestedCacheKey := token.CacheKey(authParams)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
account, err = b.pmanager.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
} else {
if s, ok := b.manager.(cache.Serializer); ok {
suggestedCacheKey := token.CacheKey(authParams)
b.cacheAccessor.Replace(s, suggestedCacheKey)
defer b.cacheAccessor.Export(s, suggestedCacheKey)
}
account, err = b.manager.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
}
return NewAuthResult(token, account)
}
Expand Down
38 changes: 31 additions & 7 deletions apps/internal/base/internal/storage/items.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ type Contract struct {
AdditionalFields map[string]interface{}
}

// Contract is the JSON structure that is written to any storage medium when serializing
// the internal cache. This design is shared between MSAL versions in many languages.
// This cannot be changed without design that includes other SDKs.
type InMemoryContract struct {
AccessTokensPartition map[string]map[string]AccessToken
RefreshTokensPartition map[string]map[string]accesstokens.RefreshToken
IDTokensPartition map[string]map[string]IDToken
AccountsPartition map[string]map[string]shared.Account
AppMetaData map[string]AppMetaData
}

// NewContract is the constructor for Contract.
func NewInMemoryContract() *InMemoryContract {
return &InMemoryContract{
AccessTokensPartition: map[string]map[string]AccessToken{},
RefreshTokensPartition: map[string]map[string]accesstokens.RefreshToken{},
IDTokensPartition: map[string]map[string]IDToken{},
AccountsPartition: map[string]map[string]shared.Account{},
AppMetaData: map[string]AppMetaData{},
}
}

// NewContract is the constructor for Contract.
func NewContract() *Contract {
return &Contract{
Expand All @@ -52,6 +74,7 @@ type AccessToken struct {
ExpiresOn internalTime.Unix `json:"expires_on,omitempty"`
ExtendedExpiresOn internalTime.Unix `json:"extended_expires_on,omitempty"`
CachedAt internalTime.Unix `json:"cached_at,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`

AdditionalFields map[string]interface{}
}
Expand Down Expand Up @@ -96,13 +119,14 @@ func (a AccessToken) Validate() error {

// IDToken is the JSON representation of an MSAL id token for encoding to storage.
type IDToken struct {
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
Realm string `json:"realm,omitempty"`
CredentialType string `json:"credential_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
Secret string `json:"secret,omitempty"`
AdditionalFields map[string]interface{}
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
Realm string `json:"realm,omitempty"`
CredentialType string `json:"credential_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
Secret string `json:"secret,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`
AdditionalFields map[string]interface{}
}

// IsZero determines if IDToken is the zero value.
Expand Down
Loading

0 comments on commit 97e99ae

Please sign in to comment.