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 }