storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/docs/sts/web-identity.go (about)

     1  //go:build ignore
     2  // +build ignore
     3  
     4  /*
     5   * MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc.
     6   *
     7   * Licensed under the Apache License, Version 2.0 (the "License");
     8   * you may not use this file except in compliance with the License.
     9   * You may obtain a copy of the License at
    10   *
    11   *     http://www.apache.org/licenses/LICENSE-2.0
    12   *
    13   * Unless required by applicable law or agreed to in writing, software
    14   * distributed under the License is distributed on an "AS IS" BASIS,
    15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16   * See the License for the specific language governing permissions and
    17   * limitations under the License.
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"context"
    24  	"crypto/rand"
    25  	"encoding/base64"
    26  	"encoding/json"
    27  	"encoding/xml"
    28  	"errors"
    29  	"flag"
    30  	"fmt"
    31  	"log"
    32  	"net/http"
    33  	"net/url"
    34  	"strings"
    35  	"time"
    36  
    37  	"golang.org/x/oauth2"
    38  
    39  	"github.com/minio/minio-go/v7"
    40  	"github.com/minio/minio-go/v7/pkg/credentials"
    41  
    42  	"storj.io/minio/pkg/auth"
    43  )
    44  
    45  // AssumedRoleUser - The identifiers for the temporary security credentials that
    46  // the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
    47  type AssumedRoleUser struct {
    48  	Arn           string
    49  	AssumedRoleID string `xml:"AssumeRoleId"`
    50  	// contains filtered or unexported fields
    51  }
    52  
    53  // AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
    54  type AssumeRoleWithWebIdentityResponse struct {
    55  	XMLName          xml.Name          `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
    56  	Result           WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
    57  	ResponseMetadata struct {
    58  		RequestID string `xml:"RequestId,omitempty"`
    59  	} `xml:"ResponseMetadata,omitempty"`
    60  }
    61  
    62  // WebIdentityResult - Contains the response to a successful AssumeRoleWithWebIdentity
    63  // request, including temporary credentials that can be used to make MinIO API requests.
    64  type WebIdentityResult struct {
    65  	AssumedRoleUser             AssumedRoleUser  `xml:",omitempty"`
    66  	Audience                    string           `xml:",omitempty"`
    67  	Credentials                 auth.Credentials `xml:",omitempty"`
    68  	PackedPolicySize            int              `xml:",omitempty"`
    69  	Provider                    string           `xml:",omitempty"`
    70  	SubjectFromWebIdentityToken string           `xml:",omitempty"`
    71  }
    72  
    73  // Returns a base64 encoded random 32 byte string.
    74  func randomState() string {
    75  	b := make([]byte, 32)
    76  	rand.Read(b)
    77  	return base64.RawURLEncoding.EncodeToString(b)
    78  }
    79  
    80  var (
    81  	stsEndpoint    string
    82  	configEndpoint string
    83  	clientID       string
    84  	clientSec      string
    85  	clientScopes   string
    86  	port           int
    87  )
    88  
    89  // DiscoveryDoc - parses the output from openid-configuration
    90  // for example http://localhost:8080/auth/realms/minio/.well-known/openid-configuration
    91  type DiscoveryDoc struct {
    92  	Issuer                           string   `json:"issuer,omitempty"`
    93  	AuthEndpoint                     string   `json:"authorization_endpoint,omitempty"`
    94  	TokenEndpoint                    string   `json:"token_endpoint,omitempty"`
    95  	UserInfoEndpoint                 string   `json:"userinfo_endpoint,omitempty"`
    96  	RevocationEndpoint               string   `json:"revocation_endpoint,omitempty"`
    97  	JwksURI                          string   `json:"jwks_uri,omitempty"`
    98  	ResponseTypesSupported           []string `json:"response_types_supported,omitempty"`
    99  	SubjectTypesSupported            []string `json:"subject_types_supported,omitempty"`
   100  	IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
   101  	ScopesSupported                  []string `json:"scopes_supported,omitempty"`
   102  	TokenEndpointAuthMethods         []string `json:"token_endpoint_auth_methods_supported,omitempty"`
   103  	ClaimsSupported                  []string `json:"claims_supported,omitempty"`
   104  	CodeChallengeMethodsSupported    []string `json:"code_challenge_methods_supported,omitempty"`
   105  }
   106  
   107  func parseDiscoveryDoc(ustr string) (DiscoveryDoc, error) {
   108  	d := DiscoveryDoc{}
   109  	req, err := http.NewRequest(http.MethodGet, ustr, nil)
   110  	if err != nil {
   111  		return d, err
   112  	}
   113  	clnt := http.Client{
   114  		Transport: http.DefaultTransport,
   115  	}
   116  	resp, err := clnt.Do(req)
   117  	if err != nil {
   118  		return d, err
   119  	}
   120  	defer resp.Body.Close()
   121  	if resp.StatusCode != http.StatusOK {
   122  		return d, err
   123  	}
   124  	dec := json.NewDecoder(resp.Body)
   125  	if err = dec.Decode(&d); err != nil {
   126  		return d, err
   127  	}
   128  	return d, nil
   129  }
   130  
   131  func init() {
   132  	flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
   133  	flag.StringVar(&configEndpoint, "config-ep",
   134  		"http://localhost:8080/auth/realms/minio/.well-known/openid-configuration",
   135  		"OpenID discovery document endpoint")
   136  	flag.StringVar(&clientID, "cid", "", "Client ID")
   137  	flag.StringVar(&clientSec, "csec", "", "Client Secret")
   138  	flag.StringVar(&clientScopes, "cscopes", "openid", "Client Scopes")
   139  	flag.IntVar(&port, "port", 8080, "Port")
   140  }
   141  
   142  func main() {
   143  	flag.Parse()
   144  	if clientID == "" || clientSec == "" {
   145  		flag.PrintDefaults()
   146  		return
   147  	}
   148  
   149  	ddoc, err := parseDiscoveryDoc(configEndpoint)
   150  	if err != nil {
   151  		log.Println(fmt.Errorf("Failed to parse OIDC discovery document %s", err))
   152  		fmt.Println(err)
   153  		return
   154  	}
   155  
   156  	scopes := ddoc.ScopesSupported
   157  	if clientScopes != "" {
   158  		scopes = strings.Split(clientScopes, ",")
   159  	}
   160  
   161  	ctx := context.Background()
   162  
   163  	config := oauth2.Config{
   164  		ClientID:     clientID,
   165  		ClientSecret: clientSec,
   166  		Endpoint: oauth2.Endpoint{
   167  			AuthURL:  ddoc.AuthEndpoint,
   168  			TokenURL: ddoc.TokenEndpoint,
   169  		},
   170  		RedirectURL: fmt.Sprintf("http://localhost:%d/oauth2/callback", port),
   171  		Scopes:      scopes,
   172  	}
   173  
   174  	state := randomState()
   175  
   176  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   177  		log.Printf("%s %s", r.Method, r.RequestURI)
   178  		if r.RequestURI != "/" {
   179  			http.NotFound(w, r)
   180  			return
   181  		}
   182  		http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound)
   183  	})
   184  
   185  	http.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) {
   186  		log.Printf("%s %s", r.Method, r.RequestURI)
   187  		if r.URL.Query().Get("state") != state {
   188  			http.Error(w, "state did not match", http.StatusBadRequest)
   189  			return
   190  		}
   191  
   192  		getWebTokenExpiry := func() (*credentials.WebIdentityToken, error) {
   193  			oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
   194  			if err != nil {
   195  				return nil, err
   196  			}
   197  			if !oauth2Token.Valid() {
   198  				return nil, errors.New("invalid token")
   199  			}
   200  
   201  			return &credentials.WebIdentityToken{
   202  				Token:  oauth2Token.Extra("id_token").(string),
   203  				Expiry: int(oauth2Token.Expiry.Sub(time.Now().UTC()).Seconds()),
   204  			}, nil
   205  		}
   206  
   207  		sts, err := credentials.NewSTSWebIdentity(stsEndpoint, getWebTokenExpiry)
   208  		if err != nil {
   209  			log.Println(fmt.Errorf("Could not get STS credentials: %s", err))
   210  			http.Error(w, err.Error(), http.StatusBadRequest)
   211  			return
   212  		}
   213  
   214  		opts := &minio.Options{
   215  			Creds:        sts,
   216  			BucketLookup: minio.BucketLookupAuto,
   217  		}
   218  
   219  		u, err := url.Parse(stsEndpoint)
   220  		if err != nil {
   221  			log.Println(fmt.Errorf("Failed to parse STS Endpoint: %s", err))
   222  			http.Error(w, err.Error(), http.StatusBadRequest)
   223  			return
   224  		}
   225  
   226  		clnt, err := minio.New(u.Host, opts)
   227  		if err != nil {
   228  			log.Println(fmt.Errorf("Error while initializing Minio client, %s", err))
   229  			http.Error(w, err.Error(), http.StatusBadRequest)
   230  			return
   231  		}
   232  		buckets, err := clnt.ListBuckets(r.Context())
   233  		if err != nil {
   234  			log.Println(fmt.Errorf("Error while listing buckets, %s", err))
   235  			http.Error(w, err.Error(), http.StatusBadRequest)
   236  			return
   237  		}
   238  		creds, _ := sts.Get()
   239  
   240  		bucketNames := []string{}
   241  
   242  		for _, bucket := range buckets {
   243  			log.Println(fmt.Sprintf("Bucket discovered: %s", bucket.Name))
   244  			bucketNames = append(bucketNames, bucket.Name)
   245  		}
   246  		response := make(map[string]interface{})
   247  		response["credentials"] = creds
   248  		response["buckets"] = bucketNames
   249  		c, err := json.MarshalIndent(response, "", "\t")
   250  		if err != nil {
   251  			http.Error(w, err.Error(), http.StatusInternalServerError)
   252  			return
   253  		}
   254  		w.Write(c)
   255  	})
   256  
   257  	address := fmt.Sprintf("localhost:%v", port)
   258  	log.Printf("listening on http://%s/", address)
   259  	log.Fatal(http.ListenAndServe(address, nil))
   260  }