| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- /*
- Copyright 2020 Docker, Inc.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package login
- import (
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
- "os"
- "strconv"
- "time"
- "github.com/Azure/go-autorest/autorest"
- "github.com/Azure/go-autorest/autorest/adal"
- auth2 "github.com/Azure/go-autorest/autorest/azure/auth"
- "github.com/Azure/go-autorest/autorest/date"
- "github.com/pkg/errors"
- "golang.org/x/oauth2"
- "github.com/docker/api/errdefs"
- )
- //go login process, derived from code sample provided by MS at https://github.com/devigned/go-az-cli-stuff
- const (
- authorizeFormat = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
- tokenEndpoint = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
- authorizationURL = "https://management.azure.com/tenants?api-version=2019-11-01"
- // scopes for a multi-tenant app works for openid, email, other common scopes, but fails when trying to add a token
- // v1 scope like "https://management.azure.com/.default" for ARM access
- scopes = "offline_access https://management.azure.com/.default"
- clientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" // Azure CLI client id
- )
- type (
- azureToken struct {
- Type string `json:"token_type"`
- Scope string `json:"scope"`
- ExpiresIn int `json:"expires_in"`
- ExtExpiresIn int `json:"ext_expires_in"`
- AccessToken string `json:"access_token"`
- RefreshToken string `json:"refresh_token"`
- Foci string `json:"foci"`
- }
- tenantResult struct {
- Value []tenantValue `json:"value"`
- }
- tenantValue struct {
- TenantID string `json:"tenantId"`
- }
- )
- // AzureLoginService Service to log into azure and get authentifier for azure APIs
- type AzureLoginService struct {
- tokenStore tokenStore
- apiHelper apiHelper
- }
- const tokenStoreFilename = "dockerAccessToken.json"
- // NewAzureLoginService creates a NewAzureLoginService
- func NewAzureLoginService() (*AzureLoginService, error) {
- return newAzureLoginServiceFromPath(GetTokenStorePath(), azureAPIHelper{})
- }
- func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) (*AzureLoginService, error) {
- store, err := newTokenStore(tokenStorePath)
- if err != nil {
- return nil, err
- }
- return &AzureLoginService{
- tokenStore: store,
- apiHelper: helper,
- }, nil
- }
- // TestLoginFromServicePrincipal login with clientId / clientSecret from a previously created service principal.
- // The resulting token does not include a refresh token, used for tests only
- func (login *AzureLoginService) TestLoginFromServicePrincipal(clientID string, clientSecret string, tenantID string) error {
- // Tried with auth2.NewUsernamePasswordConfig() but could not make this work with username / password, setting this for CI with clientID / clientSecret
- creds := auth2.NewClientCredentialsConfig(clientID, clientSecret, tenantID)
- spToken, err := creds.ServicePrincipalToken()
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "could not login with service principal: %s", err)
- }
- err = spToken.Refresh()
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "could not login with service principal: %s", err)
- }
- token, err := spToOAuthToken(spToken.Token())
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "could not read service principal token expiry: %s", err)
- }
- loginInfo := TokenInfo{TenantID: tenantID, Token: token}
- if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "could not store login info: %s", err)
- }
- return nil
- }
- // Logout remove azure token data
- func (login *AzureLoginService) Logout(ctx context.Context) error {
- err := login.tokenStore.removeData()
- if os.IsNotExist(err) {
- return errors.New("No Azure login data to be removed")
- }
- return err
- }
- // Login performs an Azure login through a web browser
- func (login *AzureLoginService) Login(ctx context.Context, requestedTenantID string) error {
- queryCh := make(chan localResponse, 1)
- s, err := NewLocalServer(queryCh)
- if err != nil {
- return err
- }
- s.Serve()
- defer s.Close()
- redirectURL := s.Addr()
- if redirectURL == "" {
- return errors.Wrap(errdefs.ErrLoginFailed, "empty redirect URL")
- }
- if err = login.apiHelper.openAzureLoginPage(redirectURL); err != nil {
- return err
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- case q := <-queryCh:
- if q.err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "unhandled local login server error: %s", err)
- }
- code, hasCode := q.values["code"]
- if !hasCode {
- return errors.Wrap(errdefs.ErrLoginFailed, "no login code")
- }
- data := url.Values{
- "grant_type": []string{"authorization_code"},
- "client_id": []string{clientID},
- "code": code,
- "scope": []string{scopes},
- "redirect_uri": []string{redirectURL},
- }
- token, err := login.apiHelper.queryToken(data, "organizations")
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "access token request failed: %s", err)
- }
- bits, statusCode, err := login.apiHelper.queryAuthorizationAPI(authorizationURL, fmt.Sprintf("Bearer %s", token.AccessToken))
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "check auth failed: %s", err)
- }
- switch statusCode {
- case http.StatusOK:
- var t tenantResult
- if err := json.Unmarshal(bits, &t); err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "unable to unmarshal tenant: %s", err)
- }
- tenantID, err := getTenantID(t.Value, requestedTenantID)
- if err != nil {
- return errors.Wrap(errdefs.ErrLoginFailed, err.Error())
- }
- tToken, err := login.refreshToken(token.RefreshToken, tenantID)
- if err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "unable to refresh token: %s", err)
- }
- loginInfo := TokenInfo{TenantID: tenantID, Token: tToken}
- if err := login.tokenStore.writeLoginInfo(loginInfo); err != nil {
- return errors.Wrapf(errdefs.ErrLoginFailed, "could not store login info: %s", err)
- }
- default:
- return errors.Wrapf(errdefs.ErrLoginFailed, "unable to login status code %d: %s", statusCode, bits)
- }
- }
- return nil
- }
- func getTenantID(tenantValues []tenantValue, requestedTenantID string) (string, error) {
- if requestedTenantID == "" {
- if len(tenantValues) < 1 {
- return "", errors.Errorf("could not find azure tenant")
- }
- return tenantValues[0].TenantID, nil
- }
- for _, tValue := range tenantValues {
- if tValue.TenantID == requestedTenantID {
- return tValue.TenantID, nil
- }
- }
- return "", errors.Errorf("could not find requested azure tenant %s", requestedTenantID)
- }
- func toOAuthToken(token azureToken) oauth2.Token {
- expireTime := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
- oauthToken := oauth2.Token{
- RefreshToken: token.RefreshToken,
- AccessToken: token.AccessToken,
- Expiry: expireTime,
- TokenType: token.Type,
- }
- return oauthToken
- }
- func spToOAuthToken(token adal.Token) (oauth2.Token, error) {
- expiresIn, err := token.ExpiresIn.Int64()
- if err != nil {
- return oauth2.Token{}, err
- }
- expireTime := time.Now().Add(time.Duration(expiresIn) * time.Second)
- oauthToken := oauth2.Token{
- RefreshToken: token.RefreshToken,
- AccessToken: token.AccessToken,
- Expiry: expireTime,
- TokenType: token.Type,
- }
- return oauthToken, nil
- }
- // NewAuthorizerFromLogin creates an authorizer based on login access token
- func NewAuthorizerFromLogin() (autorest.Authorizer, error) {
- return newAuthorizerFromLoginStorePath(GetTokenStorePath())
- }
- func newAuthorizerFromLoginStorePath(storeTokenPath string) (autorest.Authorizer, error) {
- login, err := newAzureLoginServiceFromPath(storeTokenPath, azureAPIHelper{})
- if err != nil {
- return nil, err
- }
- oauthToken, err := login.GetValidToken()
- if err != nil {
- return nil, errors.Wrap(err, "not logged in to azure, you need to run \"docker login azure\" first")
- }
- token := adal.Token{
- AccessToken: oauthToken.AccessToken,
- Type: oauthToken.TokenType,
- ExpiresIn: json.Number(strconv.Itoa(int(time.Until(oauthToken.Expiry).Seconds()))),
- ExpiresOn: json.Number(strconv.Itoa(int(oauthToken.Expiry.Sub(date.UnixEpoch()).Seconds()))),
- RefreshToken: "",
- Resource: "",
- }
- return autorest.NewBearerAuthorizer(&token), nil
- }
- // GetValidToken returns an access token. Refresh token if needed
- func (login *AzureLoginService) GetValidToken() (oauth2.Token, error) {
- loginInfo, err := login.tokenStore.readToken()
- if err != nil {
- return oauth2.Token{}, err
- }
- token := loginInfo.Token
- if token.Valid() {
- return token, nil
- }
- tenantID := loginInfo.TenantID
- token, err = login.refreshToken(token.RefreshToken, tenantID)
- if err != nil {
- return oauth2.Token{}, errors.Wrap(err, "access token request failed. Maybe you need to login to azure again.")
- }
- err = login.tokenStore.writeLoginInfo(TokenInfo{TenantID: tenantID, Token: token})
- if err != nil {
- return oauth2.Token{}, err
- }
- return token, nil
- }
- func (login *AzureLoginService) refreshToken(currentRefreshToken string, tenantID string) (oauth2.Token, error) {
- data := url.Values{
- "grant_type": []string{"refresh_token"},
- "client_id": []string{clientID},
- "scope": []string{scopes},
- "refresh_token": []string{currentRefreshToken},
- }
- token, err := login.apiHelper.queryToken(data, tenantID)
- if err != nil {
- return oauth2.Token{}, err
- }
- return toOAuthToken(token), nil
- }
|