Skip to content

Commit fd6bc7f

Browse files
bgavrilMSCopilot
andcommitted
fix: use singleflight to prevent concurrent map race in ResolveEndpoints
The code in ResolveEndpoints read m.cache without holding m.mu after addCachedEndpoints released the lock, causing a fatal concurrent map read/write when multiple goroutines resolved the same authority. Use golang.org/x/sync/singleflight to deduplicate concurrent discovery calls by authority URI. This ensures only one goroutine performs discovery per authority at a time, eliminating the race and redundant network calls. addCachedEndpoints now returns the aliases so callers never access the map without the lock. Fixes #620 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 259a11c commit fd6bc7f

3 files changed

Lines changed: 44 additions & 23 deletions

File tree

apps/internal/oauth/resolvers.go

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
1818
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
19+
"golang.org/x/sync/singleflight"
1920
)
2021

2122
type cacheEntry struct {
@@ -35,6 +36,8 @@ type authorityEndpoint struct {
3536

3637
mu sync.Mutex
3738
cache map[string]cacheEntry
39+
40+
resolveGroup singleflight.Group
3841
}
3942

4043
// newAuthorityEndpoint is the constructor for AuthorityEndpoint.
@@ -50,35 +53,49 @@ func (m *authorityEndpoint) ResolveEndpoints(ctx context.Context, authorityInfo
5053
return endpoints, nil
5154
}
5255

53-
endpoint, err := m.openIDConfigurationEndpoint(ctx, authorityInfo)
54-
if err != nil {
55-
return authority.Endpoints{}, err
56-
}
56+
key := authorityInfo.CanonicalAuthorityURI
57+
v, err, _ := m.resolveGroup.Do(key, func() (interface{}, error) {
58+
// Double-check inside the singleflight group: another goroutine may
59+
// have populated the cache while we were waiting.
60+
if endpoints, found := m.cachedEndpoints(authorityInfo, userPrincipalName); found {
61+
return endpoints, nil
62+
}
5763

58-
resp, err := m.rest.Authority().GetTenantDiscoveryResponse(ctx, endpoint)
59-
if err != nil {
60-
return authority.Endpoints{}, err
61-
}
62-
if err := resp.Validate(); err != nil {
63-
return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err)
64-
}
64+
endpoint, err := m.openIDConfigurationEndpoint(ctx, authorityInfo)
65+
if err != nil {
66+
return authority.Endpoints{}, err
67+
}
68+
69+
resp, err := m.rest.Authority().GetTenantDiscoveryResponse(ctx, endpoint)
70+
if err != nil {
71+
return authority.Endpoints{}, err
72+
}
73+
if err := resp.Validate(); err != nil {
74+
return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err)
75+
}
6576

66-
tenant := authorityInfo.Tenant
77+
tenant := authorityInfo.Tenant
6778

68-
endpoints := authority.NewEndpoints(
69-
strings.Replace(resp.AuthorizationEndpoint, "{tenant}", tenant, -1),
70-
strings.Replace(resp.TokenEndpoint, "{tenant}", tenant, -1),
71-
strings.Replace(resp.Issuer, "{tenant}", tenant, -1),
72-
authorityInfo.Host)
79+
endpoints := authority.NewEndpoints(
80+
strings.Replace(resp.AuthorizationEndpoint, "{tenant}", tenant, -1),
81+
strings.Replace(resp.TokenEndpoint, "{tenant}", tenant, -1),
82+
strings.Replace(resp.Issuer, "{tenant}", tenant, -1),
83+
authorityInfo.Host)
7384

74-
m.addCachedEndpoints(authorityInfo, userPrincipalName, endpoints)
85+
aliases := m.addCachedEndpoints(authorityInfo, userPrincipalName, endpoints)
7586

76-
if err := resp.ValidateIssuerMatchesAuthority(authorityInfo.CanonicalAuthorityURI,
77-
m.cache[authorityInfo.CanonicalAuthorityURI].Aliases); err != nil {
78-
return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err)
87+
if err := resp.ValidateIssuerMatchesAuthority(authorityInfo.CanonicalAuthorityURI,
88+
aliases); err != nil {
89+
return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err)
90+
}
91+
92+
return endpoints, nil
93+
})
94+
if err != nil {
95+
return authority.Endpoints{}, err
7996
}
8097

81-
return endpoints, nil
98+
return v.(authority.Endpoints), nil
8299
}
83100

84101
// cachedEndpoints returns the cached endpoints if they exist. If not, we return false.
@@ -100,7 +117,7 @@ func (m *authorityEndpoint) cachedEndpoints(authorityInfo authority.Info, userPr
100117
return authority.Endpoints{}, false
101118
}
102119

103-
func (m *authorityEndpoint) addCachedEndpoints(authorityInfo authority.Info, userPrincipalName string, endpoints authority.Endpoints) {
120+
func (m *authorityEndpoint) addCachedEndpoints(authorityInfo authority.Info, userPrincipalName string, endpoints authority.Endpoints) map[string]bool {
104121
m.mu.Lock()
105122
defer m.mu.Unlock()
106123

@@ -128,6 +145,7 @@ func (m *authorityEndpoint) addCachedEndpoints(authorityInfo authority.Info, use
128145
}
129146

130147
m.cache[authorityInfo.CanonicalAuthorityURI] = updatedCacheEntry
148+
return updatedCacheEntry.Aliases
131149
}
132150

133151
func (m *authorityEndpoint) openIDConfigurationEndpoint(ctx context.Context, authorityInfo authority.Info) (string, error) {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/kylelemons/godebug v1.1.0
99
github.com/montanaflynn/stats v0.7.0
1010
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
11+
golang.org/x/sync v0.10.0
1112
)
1213

1314
require golang.org/x/sys v0.5.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY
88
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
99
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
1010
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
11+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
12+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1113
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1214
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
1315
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

0 commit comments

Comments
 (0)