77package azcontainerregistry
88
99import (
10- "bytes"
1110 "encoding/base64"
1211 "encoding/json"
1312 "errors"
1413 "fmt"
15- "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
16- "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
1714 "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
1815 "net/http"
1916 "strings"
17+ "sync/atomic"
2018 "time"
2119
2220 "github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -32,93 +30,120 @@ const (
3230type authenticationPolicyOptions struct {
3331}
3432
33+ // authenticationPolicy is a policy to do the challenge-based authentication for container registry service. The authorization flow is as follows:
34+ // Step 1: GET /api/v1/acr/repositories
35+ // Return Header: 401: www-authenticate header - Bearer realm="{url}",service="{serviceName}",scope="{scope}",error="invalid_token"
36+ // Step 2: Retrieve the serviceName, scope from the WWW-Authenticate header.
37+ // Step 3: POST /api/oauth2/exchange
38+ // Request Body : { service, scope, grant-type, aadToken with ARM scope }
39+ // Response Body: { refreshToken }
40+ // Step 4: POST /api/oauth2/token
41+ // Request Body: { refreshToken, scope, grant-type }
42+ // Response Body: { accessToken }
43+ // Step 5: GET /api/v1/acr/repositories
44+ // Request Header: { Bearer acrTokenAccess }
45+ // Each registry service shares one refresh token, it will be cached in refreshTokenCache until expire time.
46+ // Since the scope will be different for different API/repository/artifact, accessTokenCache will only work when continuously calling same API.
3547type authenticationPolicy struct {
36- mainResource * temporal.Resource [azcore.AccessToken , acquiringResourceState ]
37- cred azcore.TokenCredential
38- aadScopes []string
39- acrScope string
40- acrService string
41- authClient * authenticationClient
48+ refreshTokenCache * temporal.Resource [azcore.AccessToken , acquiringResourceState ]
49+ accessTokenCache atomic.Value
50+ cred azcore.TokenCredential
51+ aadScopes []string
52+ authClient * authenticationClient
4253}
4354
4455func newAuthenticationPolicy (cred azcore.TokenCredential , scopes []string , authClient * authenticationClient , opts * authenticationPolicyOptions ) * authenticationPolicy {
4556 return & authenticationPolicy {
46- cred : cred ,
47- aadScopes : scopes ,
48- authClient : authClient ,
49- mainResource : temporal .NewResource (acquire ),
57+ cred : cred ,
58+ aadScopes : scopes ,
59+ authClient : authClient ,
60+ refreshTokenCache : temporal .NewResource (acquireRefreshToken ),
5061 }
5162}
5263
5364func (p * authenticationPolicy ) Do (req * policy.Request ) (* http.Response , error ) {
54- // send a copy of the original request without body content
55- challengeReq , err := p .getChallengeRequest (* req )
56- if err != nil {
57- return nil , err
65+ var resp * http.Response
66+ var err error
67+ if req .Raw ().Header .Get (headerAuthorization ) != "" {
68+ // retry request could do the request with existed token directly
69+ resp , err = req .Next ()
70+ } else if accessToken := p .accessTokenCache .Load (); accessToken != nil && accessToken != "" {
71+ // if there is a previous access token, then we try to use this token to do the request
72+ req .Raw ().Header .Set (
73+ headerAuthorization ,
74+ fmt .Sprintf ("%s%s" , bearerHeader , accessToken ),
75+ )
76+ resp , err = req .Next ()
77+ } else {
78+ // do challenge process for the initial request
79+ var challengeReq * policy.Request
80+ challengeReq , err = p .getChallengeRequest (* req )
81+ if err != nil {
82+ return nil , err
83+ }
84+ resp , err = challengeReq .Next ()
5885 }
59- resp , err := challengeReq .Next ()
6086 if err != nil {
6187 return nil , err
6288 }
6389
64- // do challenge process
65- if resp .StatusCode == 401 {
66- err := p . findServiceAndScope ( resp )
67- if err != nil {
90+ // if 401 response, then try to get access token
91+ if resp .StatusCode == http . StatusUnauthorized {
92+ var service , scope , accessToken string
93+ if service , scope , err = findServiceAndScope ( resp ); err != nil {
6894 return nil , err
6995 }
70-
71- accessToken , err := p .getAccessToken (req )
72- if err != nil {
96+ if accessToken , err = p .getAccessToken (req , service , scope ); err != nil {
7397 return nil , err
7498 }
75-
99+ p . accessTokenCache . Store ( accessToken )
76100 req .Raw ().Header .Set (
77101 headerAuthorization ,
78102 fmt .Sprintf ("%s%s" , bearerHeader , accessToken ),
79103 )
80-
81- // send the original request with auth
104+ // since the request may already been used once, body should be rewound
105+ if err = req .RewindBody (); err != nil {
106+ return nil , err
107+ }
82108 return req .Next ()
83109 }
84110
85111 return resp , nil
86112}
87113
88- func (p * authenticationPolicy ) getAccessToken (req * policy.Request ) (string , error ) {
114+ func (p * authenticationPolicy ) getAccessToken (req * policy.Request , service , scope string ) (string , error ) {
89115 // anonymous access
90116 if p .cred == nil {
91- resp , err := p .authClient .ExchangeACRRefreshTokenForACRAccessToken (req .Raw ().Context (), p . acrService , p . acrScope , "" , & authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions {GrantType : to .Ptr (tokenGrantTypePassword )})
117+ resp , err := p .authClient .ExchangeACRRefreshTokenForACRAccessToken (req .Raw ().Context (), service , scope , "" , & authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions {GrantType : to .Ptr (tokenGrantTypePassword )})
92118 if err != nil {
93119 return "" , err
94120 }
95121 return * resp .acrAccessToken .AccessToken , nil
96122 }
97123
98124 // access with token
99- as := acquiringResourceState {
100- policy : p ,
101- req : req ,
102- }
103-
104125 // get refresh token from cache/request
105- refreshToken , err := p .mainResource .Get (as )
126+ refreshToken , err := p .refreshTokenCache .Get (acquiringResourceState {
127+ policy : p ,
128+ req : req ,
129+ service : service ,
130+ })
106131 if err != nil {
107132 return "" , err
108133 }
109134
110135 // get access token from request
111- resp , err := p .authClient .ExchangeACRRefreshTokenForACRAccessToken (req .Raw ().Context (), p . acrService , p . acrScope , refreshToken .Token , & authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions {GrantType : to .Ptr (tokenGrantTypeRefreshToken )})
136+ resp , err := p .authClient .ExchangeACRRefreshTokenForACRAccessToken (req .Raw ().Context (), service , scope , refreshToken .Token , & authenticationClientExchangeACRRefreshTokenForACRAccessTokenOptions {GrantType : to .Ptr (tokenGrantTypeRefreshToken )})
112137 if err != nil {
113138 return "" , err
114139 }
115140 return * resp .acrAccessToken .AccessToken , nil
116141}
117142
118- func ( p * authenticationPolicy ) findServiceAndScope (resp * http.Response ) error {
143+ func findServiceAndScope (resp * http.Response ) ( string , string , error ) {
119144 authHeader := resp .Header .Get ("WWW-Authenticate" )
120145 if authHeader == "" {
121- return errors .New ("response has no WWW-Authenticate header for challenge authentication" )
146+ return "" , "" , errors .New ("response has no WWW-Authenticate header for challenge authentication" )
122147 }
123148
124149 authHeader = strings .ReplaceAll (authHeader , "Bearer " , "" )
@@ -131,54 +156,35 @@ func (p *authenticationPolicy) findServiceAndScope(resp *http.Response) error {
131156 }
132157 }
133158
134- if v , ok := valuesMap ["scope" ]; ok {
135- p .acrScope = v
136- }
137- if p .acrScope == "" {
138- return errors .New ("could not find a valid scope in the WWW-Authenticate header" )
159+ if _ , ok := valuesMap ["service" ]; ! ok {
160+ return "" , "" , errors .New ("could not find a valid service in the WWW-Authenticate header" )
139161 }
140162
141- if v , ok := valuesMap ["service" ]; ok {
142- p .acrService = v
143- }
144- if p .acrService == "" {
145- return errors .New ("could not find a valid service in the WWW-Authenticate header" )
163+ if _ , ok := valuesMap ["scope" ]; ! ok {
164+ return "" , "" , errors .New ("could not find a valid scope in the WWW-Authenticate header" )
146165 }
147166
148- return nil
167+ return valuesMap [ "service" ], valuesMap [ "scope" ], nil
149168}
150169
151- func (p authenticationPolicy ) getChallengeRequest (orig policy.Request ) (* policy.Request , error ) {
152- req , err := runtime .NewRequest (orig .Raw ().Context (), orig .Raw ().Method , orig .Raw ().URL .String ())
153- if err != nil {
154- return nil , err
155- }
156-
157- req .Raw ().Header = orig .Raw ().Header
158- req .Raw ().Header .Set ("Content-Length" , "0" )
159- req .Raw ().ContentLength = 0
160-
161- copied := orig .Clone (orig .Raw ().Context ())
162- copied .Raw ().Body = req .Body ()
163- copied .Raw ().ContentLength = 0
164- copied .Raw ().Header .Set ("Content-Length" , "0" )
165- err = copied .SetBody (streaming .NopCloser (bytes .NewReader ([]byte {})), "application/json" )
170+ func (p authenticationPolicy ) getChallengeRequest (oriReq policy.Request ) (* policy.Request , error ) {
171+ copied := oriReq .Clone (oriReq .Raw ().Context ())
172+ err := copied .SetBody (nil , "" )
166173 if err != nil {
167174 return nil , err
168175 }
169176 copied .Raw ().Header .Del ("Content-Type" )
170-
171- return copied , err
177+ return copied , nil
172178}
173179
174180type acquiringResourceState struct {
175- req * policy.Request
176- policy * authenticationPolicy
181+ req * policy.Request
182+ policy * authenticationPolicy
183+ service string
177184}
178185
179- // acquire acquires or updates the resource; only one
180- // thread/goroutine at a time ever calls this function
181- func acquire (state acquiringResourceState ) (newResource azcore.AccessToken , newExpiration time.Time , err error ) {
186+ // acquireRefreshToken acquires or updates the refresh token of ACR service; only one thread/goroutine at a time ever calls this function
187+ func acquireRefreshToken (state acquiringResourceState ) (newResource azcore.AccessToken , newExpiration time.Time , err error ) {
182188 // get AAD token from credential
183189 aadToken , err := state .policy .cred .GetToken (
184190 state .req .Raw ().Context (),
@@ -191,7 +197,7 @@ func acquire(state acquiringResourceState) (newResource azcore.AccessToken, newE
191197 }
192198
193199 // exchange refresh token with AAD token
194- refreshResp , err := state .policy .authClient .ExchangeAADAccessTokenForACRRefreshToken (state .req .Raw ().Context (), postContentSchemaGrantTypeAccessToken , state .policy . acrService , & authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions {
200+ refreshResp , err := state .policy .authClient .ExchangeAADAccessTokenForACRRefreshToken (state .req .Raw ().Context (), postContentSchemaGrantTypeAccessToken , state .service , & authenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions {
195201 AccessToken : & aadToken .Token ,
196202 })
197203 if err != nil {
0 commit comments