Skip to content

Commit 96b7c35

Browse files
authored
Persistent caching and user authentication API (Azure#21724)
1 parent 144c931 commit 96b7c35

40 files changed

+2184
-44
lines changed

eng/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
"Name": "azidentity",
2929
"CoverageGoal": 0.87
3030
},
31+
{
32+
"Name": "azidentity/cache",
33+
"CoverageGoal": 0.35
34+
},
3135
{
3236
"Name": "azqueue",
3337
"CoverageGoal": 0.60

sdk/azidentity/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "go",
44
"TagPrefix": "go/azidentity",
5-
"Tag": "go/azidentity_6225ab0470"
5+
"Tag": "go/azidentity_ae45facec3"
66
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package azidentity
8+
9+
import (
10+
"encoding/json"
11+
"errors"
12+
"fmt"
13+
"net/url"
14+
"strings"
15+
16+
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
17+
)
18+
19+
var supportedAuthRecordVersions = []string{"1.0"}
20+
21+
// AuthenticationRecord is non-secret account information about an authenticated user that user credentials such as
22+
// [DeviceCodeCredential] and [InteractiveBrowserCredential] can use to access previously cached authentication
23+
// data. Call these credentials' Authenticate method to get an AuthenticationRecord for a user.
24+
type AuthenticationRecord struct {
25+
// Authority is the URL of the authority that issued the token.
26+
Authority string `json:"authority"`
27+
28+
// ClientID is the ID of the application that authenticated the user.
29+
ClientID string `json:"clientId"`
30+
31+
// HomeAccountID uniquely identifies the account.
32+
HomeAccountID string `json:"homeAccountId"`
33+
34+
// TenantID identifies the tenant in which the user authenticated.
35+
TenantID string `json:"tenantId"`
36+
37+
// Username is the user's preferred username.
38+
Username string `json:"username"`
39+
40+
// Version of the AuthenticationRecord.
41+
Version string `json:"version"`
42+
}
43+
44+
// UnmarshalJSON implements json.Unmarshaler for AuthenticationRecord
45+
func (a *AuthenticationRecord) UnmarshalJSON(b []byte) error {
46+
// Default unmarshaling is fine but we want to return an error if the record's version isn't supported i.e., we
47+
// want to inspect the unmarshalled values before deciding whether to return an error. Unmarshaling a formally
48+
// different type enables this by assigning all the fields without recursing into this method.
49+
type r AuthenticationRecord
50+
err := json.Unmarshal(b, (*r)(a))
51+
if err != nil {
52+
return err
53+
}
54+
if a.Version == "" {
55+
return errors.New("AuthenticationRecord must have a version")
56+
}
57+
for _, v := range supportedAuthRecordVersions {
58+
if a.Version == v {
59+
return nil
60+
}
61+
}
62+
return fmt.Errorf("unsupported AuthenticationRecord version %q. This module supports %v", a.Version, supportedAuthRecordVersions)
63+
}
64+
65+
// account returns the AuthenticationRecord as an MSAL Account. The account is zero-valued when the AuthenticationRecord is zero-valued.
66+
func (a *AuthenticationRecord) account() public.Account {
67+
return public.Account{
68+
Environment: a.Authority,
69+
HomeAccountID: a.HomeAccountID,
70+
PreferredUsername: a.Username,
71+
}
72+
}
73+
74+
func newAuthenticationRecord(ar public.AuthResult) (AuthenticationRecord, error) {
75+
u, err := url.Parse(ar.IDToken.Issuer)
76+
if err != nil {
77+
return AuthenticationRecord{}, fmt.Errorf("Authenticate expected a URL issuer but got %q", ar.IDToken.Issuer)
78+
}
79+
tenant := ar.IDToken.TenantID
80+
if tenant == "" {
81+
tenant = strings.Trim(u.Path, "/")
82+
}
83+
username := ar.IDToken.PreferredUsername
84+
if username == "" {
85+
username = ar.IDToken.UPN
86+
}
87+
return AuthenticationRecord{
88+
Authority: fmt.Sprintf("%s://%s", u.Scheme, u.Host),
89+
ClientID: ar.IDToken.Audience,
90+
HomeAccountID: ar.Account.HomeAccountID,
91+
TenantID: tenant,
92+
Username: username,
93+
Version: "1.0",
94+
}, nil
95+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package azidentity
8+
9+
import (
10+
"encoding/json"
11+
"reflect"
12+
"strings"
13+
"testing"
14+
)
15+
16+
func TestAuthenticationRecord_MarshalUnmarshal(t *testing.T) {
17+
for _, test := range []struct {
18+
desc, version string
19+
err bool
20+
}{
21+
{desc: "no version", err: true},
22+
{desc: "supported version", version: supportedAuthRecordVersions[0]},
23+
{desc: "unsupported version", err: true, version: "42"},
24+
} {
25+
t.Run(test.desc, func(t *testing.T) {
26+
record := AuthenticationRecord{
27+
Authority: "authority",
28+
ClientID: "client",
29+
HomeAccountID: "oid.tid",
30+
TenantID: "tenant",
31+
Username: "user",
32+
Version: test.version,
33+
}
34+
marshaled, err := json.Marshal(record)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
var unmarshaled AuthenticationRecord
39+
err = json.Unmarshal(marshaled, &unmarshaled)
40+
if err != nil {
41+
if !test.err {
42+
t.Fatal(err)
43+
}
44+
if actual := err.Error(); !strings.Contains(actual, "version") {
45+
t.Fatalf("unexpected error %q", actual)
46+
}
47+
return
48+
} else if test.err {
49+
t.Fatal("expected an error")
50+
}
51+
if !reflect.DeepEqual(unmarshaled, record) {
52+
t.Fatal("records should be equal")
53+
}
54+
})
55+
}
56+
}

sdk/azidentity/azidentity.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
2222
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
2323
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
24+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity/internal"
2425
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
2526
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
2627
)
@@ -49,6 +50,9 @@ var (
4950
errInvalidTenantID = errors.New("invalid tenantID. You can locate your tenantID by following the instructions listed here: https://learn.microsoft.com/partner-center/find-ids-and-domain-names")
5051
)
5152

53+
// TokenCachePersistenceOptions contains options for persistent token caching
54+
type TokenCachePersistenceOptions = internal.TokenCachePersistenceOptions
55+
5256
// setAuthorityHost initializes the authority host for credentials. Precedence is:
5357
// 1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
5458
// 2. value of AZURE_AUTHORITY_HOST

0 commit comments

Comments
 (0)