Fetch token by credentials and refresh token
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)master
							parent
							
								
									62d46ef07d
								
							
						
					
					
						commit
						c9880e6b05
					
				|  | @ -36,6 +36,14 @@ type AuthenticationHandler interface { | |||
| type CredentialStore interface { | ||||
| 	// Basic returns basic auth for the given URL
 | ||||
| 	Basic(*url.URL) (string, string) | ||||
| 
 | ||||
| 	// RefreshToken returns a refresh token for the
 | ||||
| 	// given URL and service
 | ||||
| 	RefreshToken(*url.URL, string) string | ||||
| 
 | ||||
| 	// SetRefreshToken sets the refresh token if none
 | ||||
| 	// is provided for the given url and service
 | ||||
| 	SetRefreshToken(realm *url.URL, service, token string) | ||||
| } | ||||
| 
 | ||||
| // NewAuthorizer creates an authorizer which can handle multiple authentication
 | ||||
|  | @ -196,95 +204,73 @@ func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes | |||
| 	} | ||||
| 	now := th.clock.Now() | ||||
| 	if now.After(th.tokenExpiration) || addedScopes { | ||||
| 		tr, err := th.fetchToken(params) | ||||
| 		token, expiration, err := th.fetchToken(params) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		th.tokenCache = tr.Token | ||||
| 		th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second) | ||||
| 
 | ||||
| 		// do not update cache for added scope tokens
 | ||||
| 		if !addedScopes { | ||||
| 			th.tokenCache = token | ||||
| 			th.tokenExpiration = expiration | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type tokenResponse struct { | ||||
| 	Token       string    `json:"token"` | ||||
| 	AccessToken string    `json:"access_token"` | ||||
| 	ExpiresIn   int       `json:"expires_in"` | ||||
| 	IssuedAt    time.Time `json:"issued_at"` | ||||
| type postTokenResponse struct { | ||||
| 	AccessToken  string    `json:"access_token"` | ||||
| 	RefreshToken string    `json:"refresh_token"` | ||||
| 	ExpiresIn    int       `json:"expires_in"` | ||||
| 	IssuedAt     time.Time `json:"issued_at"` | ||||
| 	Scope        string    `json:"scope"` | ||||
| } | ||||
| 
 | ||||
| func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) { | ||||
| 	realm, ok := params["realm"] | ||||
| 	if !ok { | ||||
| 		return nil, errors.New("no realm specified for token auth challenge") | ||||
| func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) { | ||||
| 	form := url.Values{} | ||||
| 	form.Set("scope", strings.Join(scopes, " ")) | ||||
| 	form.Set("service", service) | ||||
| 
 | ||||
| 	// TODO: Make this configurable
 | ||||
| 	form.Set("client_id", "docker") | ||||
| 
 | ||||
| 	if refreshToken != "" { | ||||
| 		form.Set("grant_type", "refresh_token") | ||||
| 		form.Set("refresh_token", refreshToken) | ||||
| 	} else if th.creds != nil { | ||||
| 		form.Set("grant_type", "password") | ||||
| 		username, password := th.creds.Basic(realm) | ||||
| 		form.Set("username", username) | ||||
| 		form.Set("password", password) | ||||
| 
 | ||||
| 		// attempt to get a refresh token
 | ||||
| 		form.Set("access_type", "offline") | ||||
| 	} else { | ||||
| 		// refuse to do oauth without a grant type
 | ||||
| 		return "", time.Time{}, fmt.Errorf("no supported grant type") | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(dmcgowan): Handle empty scheme
 | ||||
| 
 | ||||
| 	realmURL, err := url.Parse(realm) | ||||
| 	resp, err := th.client().PostForm(realm.String(), form) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("invalid token auth challenge realm: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", realmURL.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	reqParams := req.URL.Query() | ||||
| 	service := params["service"] | ||||
| 	scope := th.scope.String() | ||||
| 
 | ||||
| 	if service != "" { | ||||
| 		reqParams.Add("service", service) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, scopeField := range strings.Fields(scope) { | ||||
| 		reqParams.Add("scope", scopeField) | ||||
| 	} | ||||
| 
 | ||||
| 	for scope := range th.additionalScopes { | ||||
| 		reqParams.Add("scope", scope) | ||||
| 	} | ||||
| 
 | ||||
| 	if th.creds != nil { | ||||
| 		username, password := th.creds.Basic(realmURL) | ||||
| 		if username != "" && password != "" { | ||||
| 			reqParams.Add("account", username) | ||||
| 			req.SetBasicAuth(username, password) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	req.URL.RawQuery = reqParams.Encode() | ||||
| 
 | ||||
| 	resp, err := th.client().Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return "", time.Time{}, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	if !client.SuccessStatus(resp.StatusCode) { | ||||
| 		err := client.HandleErrorResponse(resp) | ||||
| 		return nil, err | ||||
| 		return "", time.Time{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	decoder := json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	tr := new(tokenResponse) | ||||
| 	if err = decoder.Decode(tr); err != nil { | ||||
| 		return nil, fmt.Errorf("unable to decode token response: %s", err) | ||||
| 	var tr postTokenResponse | ||||
| 	if err = decoder.Decode(&tr); err != nil { | ||||
| 		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// `access_token` is equivalent to `token` and if both are specified
 | ||||
| 	// the choice is undefined.  Canonicalize `access_token` by sticking
 | ||||
| 	// things in `token`.
 | ||||
| 	if tr.AccessToken != "" { | ||||
| 		tr.Token = tr.AccessToken | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.Token == "" { | ||||
| 		return nil, errors.New("authorization server did not include a token in the response") | ||||
| 	if tr.RefreshToken != "" && tr.RefreshToken != refreshToken { | ||||
| 		th.creds.SetRefreshToken(realm, service, tr.RefreshToken) | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.ExpiresIn < minimumTokenLifetimeSeconds { | ||||
|  | @ -295,10 +281,128 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon | |||
| 
 | ||||
| 	if tr.IssuedAt.IsZero() { | ||||
| 		// issued_at is optional in the token response.
 | ||||
| 		tr.IssuedAt = th.clock.Now() | ||||
| 		tr.IssuedAt = th.clock.Now().UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	return tr, nil | ||||
| 	return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil | ||||
| } | ||||
| 
 | ||||
| type getTokenResponse struct { | ||||
| 	Token        string    `json:"token"` | ||||
| 	AccessToken  string    `json:"access_token"` | ||||
| 	ExpiresIn    int       `json:"expires_in"` | ||||
| 	IssuedAt     time.Time `json:"issued_at"` | ||||
| 	RefreshToken string    `json:"refresh_token"` | ||||
| } | ||||
| 
 | ||||
| func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) { | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", realm.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return "", time.Time{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	reqParams := req.URL.Query() | ||||
| 
 | ||||
| 	if service != "" { | ||||
| 		reqParams.Add("service", service) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, scope := range scopes { | ||||
| 		reqParams.Add("scope", scope) | ||||
| 	} | ||||
| 
 | ||||
| 	if th.creds != nil { | ||||
| 		username, password := th.creds.Basic(realm) | ||||
| 		if username != "" && password != "" { | ||||
| 			reqParams.Add("account", username) | ||||
| 			req.SetBasicAuth(username, password) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	req.URL.RawQuery = reqParams.Encode() | ||||
| 
 | ||||
| 	resp, err := th.client().Do(req) | ||||
| 	if err != nil { | ||||
| 		return "", time.Time{}, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	if !client.SuccessStatus(resp.StatusCode) { | ||||
| 		err := client.HandleErrorResponse(resp) | ||||
| 		return "", time.Time{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	decoder := json.NewDecoder(resp.Body) | ||||
| 
 | ||||
| 	var tr getTokenResponse | ||||
| 	if err = decoder.Decode(&tr); err != nil { | ||||
| 		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.RefreshToken != "" && th.creds != nil { | ||||
| 		th.creds.SetRefreshToken(realm, service, tr.RefreshToken) | ||||
| 	} | ||||
| 
 | ||||
| 	// `access_token` is equivalent to `token` and if both are specified
 | ||||
| 	// the choice is undefined.  Canonicalize `access_token` by sticking
 | ||||
| 	// things in `token`.
 | ||||
| 	if tr.AccessToken != "" { | ||||
| 		tr.Token = tr.AccessToken | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.Token == "" { | ||||
| 		return "", time.Time{}, errors.New("authorization server did not include a token in the response") | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.ExpiresIn < minimumTokenLifetimeSeconds { | ||||
| 		// The default/minimum lifetime.
 | ||||
| 		tr.ExpiresIn = minimumTokenLifetimeSeconds | ||||
| 		logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.IssuedAt.IsZero() { | ||||
| 		// issued_at is optional in the token response.
 | ||||
| 		tr.IssuedAt = th.clock.Now().UTC() | ||||
| 	} | ||||
| 
 | ||||
| 	return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil | ||||
| } | ||||
| 
 | ||||
| func (th *tokenHandler) fetchToken(params map[string]string) (token string, expiration time.Time, err error) { | ||||
| 	realm, ok := params["realm"] | ||||
| 	if !ok { | ||||
| 		return "", time.Time{}, errors.New("no realm specified for token auth challenge") | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(dmcgowan): Handle empty scheme and relative realm
 | ||||
| 	realmURL, err := url.Parse(realm) | ||||
| 	if err != nil { | ||||
| 		return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	service := params["service"] | ||||
| 
 | ||||
| 	scopes := make([]string, 0, 1+len(th.additionalScopes)) | ||||
| 	if len(th.scope.Actions) > 0 { | ||||
| 		scopes = append(scopes, th.scope.String()) | ||||
| 	} | ||||
| 	for scope := range th.additionalScopes { | ||||
| 		scopes = append(scopes, scope) | ||||
| 	} | ||||
| 
 | ||||
| 	var refreshToken string | ||||
| 
 | ||||
| 	if th.creds != nil { | ||||
| 		refreshToken = th.creds.RefreshToken(realmURL, service) | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO(dmcgowan): define parameter to force oauth with password
 | ||||
| 	if refreshToken != "" { | ||||
| 		return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) | ||||
| 	} | ||||
| 
 | ||||
| 	return th.fetchTokenWithBasicAuth(realmURL, service, scopes) | ||||
| } | ||||
| 
 | ||||
| type basicHandler struct { | ||||
|  |  | |||
|  | @ -80,14 +80,25 @@ func ping(manager ChallengeManager, endpoint, versionHeader string) ([]APIVersio | |||
| } | ||||
| 
 | ||||
| type testCredentialStore struct { | ||||
| 	username string | ||||
| 	password string | ||||
| 	username      string | ||||
| 	password      string | ||||
| 	refreshTokens map[string]string | ||||
| } | ||||
| 
 | ||||
| func (tcs *testCredentialStore) Basic(*url.URL) (string, string) { | ||||
| 	return tcs.username, tcs.password | ||||
| } | ||||
| 
 | ||||
| func (tcs *testCredentialStore) RefreshToken(u *url.URL, service string) string { | ||||
| 	return tcs.refreshTokens[service] | ||||
| } | ||||
| 
 | ||||
| func (tcs *testCredentialStore) SetRefreshToken(u *url.URL, service string, token string) { | ||||
| 	if tcs.refreshTokens != nil { | ||||
| 		tcs.refreshTokens[service] = token | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestEndpointAuthorizeToken(t *testing.T) { | ||||
| 	service := "localhost.localdomain" | ||||
| 	repo1 := "some/registry" | ||||
|  | @ -162,14 +173,11 @@ func TestEndpointAuthorizeToken(t *testing.T) { | |||
| 		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) | ||||
| 	} | ||||
| 
 | ||||
| 	badCheck := func(a string) bool { | ||||
| 		return a == "Bearer statictoken" | ||||
| 	} | ||||
| 	e2, c2 := testServerWithAuth(m, authenicate, badCheck) | ||||
| 	e2, c2 := testServerWithAuth(m, authenicate, validCheck) | ||||
| 	defer c2() | ||||
| 
 | ||||
| 	challengeManager2 := NewSimpleChallengeManager() | ||||
| 	versions, err = ping(challengeManager2, e+"/v2/", "x-multi-api-version") | ||||
| 	versions, err = ping(challengeManager2, e2+"/v2/", "x-multi-api-version") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | @ -199,6 +207,161 @@ func TestEndpointAuthorizeToken(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestEndpointAuthorizeRefreshToken(t *testing.T) { | ||||
| 	service := "localhost.localdomain" | ||||
| 	repo1 := "some/registry" | ||||
| 	repo2 := "other/registry" | ||||
| 	scope1 := fmt.Sprintf("repository:%s:pull,push", repo1) | ||||
| 	scope2 := fmt.Sprintf("repository:%s:pull,push", repo2) | ||||
| 	refreshToken1 := "0123456790abcdef" | ||||
| 	refreshToken2 := "0123456790fedcba" | ||||
| 	tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ | ||||
| 		{ | ||||
| 			Request: testutil.Request{ | ||||
| 				Method: "POST", | ||||
| 				Route:  "/token", | ||||
| 				Body:   []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)), | ||||
| 			}, | ||||
| 			Response: testutil.Response{ | ||||
| 				StatusCode: http.StatusOK, | ||||
| 				Body:       []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken1)), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// In the future this test may fail and require using basic auth to get a different refresh token
 | ||||
| 			Request: testutil.Request{ | ||||
| 				Method: "POST", | ||||
| 				Route:  "/token", | ||||
| 				Body:   []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope2), service)), | ||||
| 			}, | ||||
| 			Response: testutil.Response{ | ||||
| 				StatusCode: http.StatusOK, | ||||
| 				Body:       []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken2)), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Request: testutil.Request{ | ||||
| 				Method: "POST", | ||||
| 				Route:  "/token", | ||||
| 				Body:   []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken2, url.QueryEscape(scope2), service)), | ||||
| 			}, | ||||
| 			Response: testutil.Response{ | ||||
| 				StatusCode: http.StatusOK, | ||||
| 				Body:       []byte(`{"access_token":"badtoken","refresh_token":"%s"}`), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| 	te, tc := testServer(tokenMap) | ||||
| 	defer tc() | ||||
| 
 | ||||
| 	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ | ||||
| 		{ | ||||
| 			Request: testutil.Request{ | ||||
| 				Method: "GET", | ||||
| 				Route:  "/v2/hello", | ||||
| 			}, | ||||
| 			Response: testutil.Response{ | ||||
| 				StatusCode: http.StatusAccepted, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| 
 | ||||
| 	authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) | ||||
| 	validCheck := func(a string) bool { | ||||
| 		return a == "Bearer statictoken" | ||||
| 	} | ||||
| 	e, c := testServerWithAuth(m, authenicate, validCheck) | ||||
| 	defer c() | ||||
| 
 | ||||
| 	challengeManager1 := NewSimpleChallengeManager() | ||||
| 	versions, err := ping(challengeManager1, e+"/v2/", "x-api-version") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if len(versions) != 1 { | ||||
| 		t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) | ||||
| 	} | ||||
| 	if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { | ||||
| 		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) | ||||
| 	} | ||||
| 	creds := &testCredentialStore{ | ||||
| 		refreshTokens: map[string]string{ | ||||
| 			service: refreshToken1, | ||||
| 		}, | ||||
| 	} | ||||
| 	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, creds, repo1, "pull", "push"))) | ||||
| 	client := &http.Client{Transport: transport1} | ||||
| 
 | ||||
| 	req, _ := http.NewRequest("GET", e+"/v2/hello", nil) | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error sending get request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusAccepted { | ||||
| 		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) | ||||
| 	} | ||||
| 
 | ||||
| 	// Try with refresh token setting
 | ||||
| 	e2, c2 := testServerWithAuth(m, authenicate, validCheck) | ||||
| 	defer c2() | ||||
| 
 | ||||
| 	challengeManager2 := NewSimpleChallengeManager() | ||||
| 	versions, err = ping(challengeManager2, e2+"/v2/", "x-api-version") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if len(versions) != 1 { | ||||
| 		t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) | ||||
| 	} | ||||
| 	if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { | ||||
| 		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) | ||||
| 	} | ||||
| 
 | ||||
| 	transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, creds, repo2, "pull", "push"))) | ||||
| 	client2 := &http.Client{Transport: transport2} | ||||
| 
 | ||||
| 	req, _ = http.NewRequest("GET", e2+"/v2/hello", nil) | ||||
| 	resp, err = client2.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error sending get request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusAccepted { | ||||
| 		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) | ||||
| 	} | ||||
| 
 | ||||
| 	if creds.refreshTokens[service] != refreshToken2 { | ||||
| 		t.Fatalf("Refresh token not set after change") | ||||
| 	} | ||||
| 
 | ||||
| 	// Try with bad token
 | ||||
| 	e3, c3 := testServerWithAuth(m, authenicate, validCheck) | ||||
| 	defer c3() | ||||
| 
 | ||||
| 	challengeManager3 := NewSimpleChallengeManager() | ||||
| 	versions, err = ping(challengeManager3, e3+"/v2/", "x-api-version") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { | ||||
| 		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) | ||||
| 	} | ||||
| 
 | ||||
| 	transport3 := transport.NewTransport(nil, NewAuthorizer(challengeManager3, NewTokenHandler(nil, creds, repo2, "pull", "push"))) | ||||
| 	client3 := &http.Client{Transport: transport3} | ||||
| 
 | ||||
| 	req, _ = http.NewRequest("GET", e3+"/v2/hello", nil) | ||||
| 	resp, err = client3.Do(req) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Error sending get request: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusUnauthorized { | ||||
| 		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func basicAuth(username, password string) string { | ||||
| 	auth := username + ":" + password | ||||
| 	return base64.StdEncoding.EncodeToString([]byte(auth)) | ||||
|  |  | |||
|  | @ -25,6 +25,13 @@ func (c credentials) Basic(u *url.URL) (string, string) { | |||
| 	return up.username, up.password | ||||
| } | ||||
| 
 | ||||
| func (c credentials) RefreshToken(u *url.URL, service string) string { | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (c credentials) SetRefreshToken(u *url.URL, service, token string) { | ||||
| } | ||||
| 
 | ||||
| // configureAuth stores credentials for challenge responses
 | ||||
| func configureAuth(username, password string) (auth.CredentialStore, error) { | ||||
| 	creds := map[string]userpass{ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue