Add support for AssumeRoleWithWebIdentity (#6985)

This commit is contained in:
Harshavardhana 2019-01-04 13:48:12 -08:00 committed by kannappanr
parent e82dcd195c
commit 2d19011a1d
14 changed files with 553 additions and 91 deletions

View File

@ -101,7 +101,7 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
registerDistXLRouters(router, endpoints) registerDistXLRouters(router, endpoints)
} }
// Add STS router only enabled if etcd is configured. // Add STS router always.
registerSTSRouter(router) registerSTSRouter(router)
// Add Admin RPC router // Add Admin RPC router

143
cmd/sts-datatypes.go Normal file
View File

@ -0,0 +1,143 @@
/*
* Minio Cloud Storage, (C) 2018 Minio, 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 cmd
import (
"encoding/xml"
"github.com/minio/minio/pkg/auth"
)
// AssumedRoleUser - The identifiers for the temporary security credentials that
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
type AssumedRoleUser struct {
// The ARN of the temporary security credentials that are returned from the
// AssumeRole action. For more information about ARNs and how to use them in
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
// in Using IAM.
//
// Arn is a required field
Arn string
// A unique identifier that contains the role ID and the role session name of
// the role that is being assumed. The role ID is generated by AWS when the
// role is created.
//
// AssumedRoleId is a required field
AssumedRoleID string `xml:"AssumeRoleId"`
// contains filtered or unexported fields
}
// AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
type AssumeRoleWithWebIdentityResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
Result WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
ResponseMetadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// WebIdentityResult - Contains the response to a successful AssumeRoleWithWebIdentity
// request, including temporary credentials that can be used to make Minio API requests.
type WebIdentityResult struct {
// The identifiers for the temporary security credentials that the operation
// returns.
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
// The intended audience (also known as client ID) of the web identity token.
// This is traditionally the client identifier issued to the application that
// requested the client grants.
Audience string `xml:",omitempty"`
// The temporary security credentials, which include an access key ID, a secret
// access key, and a security (or session) token.
//
// Note: The size of the security token that STS APIs return is not fixed. We
// strongly recommend that you make no assumptions about the maximum size. As
// of this writing, the typical size is less than 4096 bytes, but that can vary.
// Also, future updates to AWS might require larger sizes.
Credentials auth.Credentials `xml:",omitempty"`
// A percentage value that indicates the size of the policy in packed form.
// The service rejects any policy with a packed size greater than 100 percent,
// which means the policy exceeded the allowed space.
PackedPolicySize int `xml:",omitempty"`
// The issuing authority of the web identity token presented. For OpenID Connect
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
// this contains the value of the ProviderId parameter that was passed in the
// AssumeRoleWithWebIdentity request.
Provider string `xml:",omitempty"`
// The unique user identifier that is returned by the identity provider.
// This identifier is associated with the Token that was submitted
// with the AssumeRoleWithWebIdentity call. The identifier is typically unique to
// the user and the application that acquired the WebIdentityToken (pairwise identifier).
// For OpenID Connect ID tokens, this field contains the value returned by the identity
// provider as the token's sub (Subject) claim.
SubjectFromWebIdentityToken string `xml:",omitempty"`
}
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
type AssumeRoleWithClientGrantsResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
ResponseMetadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
// request, including temporary credentials that can be used to make Minio API requests.
type ClientGrantsResult struct {
// The identifiers for the temporary security credentials that the operation
// returns.
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
// The intended audience (also known as client ID) of the web identity token.
// This is traditionally the client identifier issued to the application that
// requested the client grants.
Audience string `xml:",omitempty"`
// The temporary security credentials, which include an access key ID, a secret
// access key, and a security (or session) token.
//
// Note: The size of the security token that STS APIs return is not fixed. We
// strongly recommend that you make no assumptions about the maximum size. As
// of this writing, the typical size is less than 4096 bytes, but that can vary.
// Also, future updates to AWS might require larger sizes.
Credentials auth.Credentials `xml:",omitempty"`
// A percentage value that indicates the size of the policy in packed form.
// The service rejects any policy with a packed size greater than 100 percent,
// which means the policy exceeded the allowed space.
PackedPolicySize int `xml:",omitempty"`
// The issuing authority of the web identity token presented. For OpenID Connect
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
// this contains the value of the ProviderId parameter that was passed in the
// AssumeRoleWithClientGrants request.
Provider string `xml:",omitempty"`
// The unique user identifier that is returned by the identity provider.
// This identifier is associated with the Token that was submitted
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
// For OpenID Connect ID tokens, this field contains the value returned by the identity
// provider as the token's sub (Subject) claim.
SubjectFromToken string `xml:",omitempty"`
}

View File

@ -56,6 +56,7 @@ const (
ErrSTSNone STSErrorCode = iota ErrSTSNone STSErrorCode = iota
ErrSTSMissingParameter ErrSTSMissingParameter
ErrSTSInvalidParameterValue ErrSTSInvalidParameterValue
ErrSTSWebIdentityExpiredToken
ErrSTSClientGrantsExpiredToken ErrSTSClientGrantsExpiredToken
ErrSTSInvalidClientGrantsToken ErrSTSInvalidClientGrantsToken
ErrSTSMalformedPolicyDocument ErrSTSMalformedPolicyDocument
@ -76,9 +77,14 @@ var stsErrCodeResponse = map[STSErrorCode]STSError{
Description: "An invalid or out-of-range value was supplied for the input parameter.", Description: "An invalid or out-of-range value was supplied for the input parameter.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrSTSWebIdentityExpiredToken: {
Code: "ExpiredToken",
Description: "The web identity token that was passed is expired or is not valid. Get a new identity token from the identity provider and then retry the request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrSTSClientGrantsExpiredToken: { ErrSTSClientGrantsExpiredToken: {
Code: "ExpiredToken", Code: "ExpiredToken",
Description: "The client grants that was passed is expired or is not valid.", Description: "The client grants that was passed is expired or is not valid. Get a new client grants token from the identity provider and then retry the request.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrSTSInvalidClientGrantsToken: { ErrSTSInvalidClientGrantsToken: {

View File

@ -17,7 +17,6 @@
package cmd package cmd
import ( import (
"encoding/xml"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -45,82 +44,104 @@ func registerSTSRouter(router *mux.Router) {
// AssumeRoleWithClientGrants // AssumeRoleWithClientGrants
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)). stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
Queries("Action", "AssumeRoleWithClientGrants"). Queries("Action", "AssumeRoleWithClientGrants").
Queries("Version", stsAPIVersion).
Queries("Token", "{Token:.*}") Queries("Token", "{Token:.*}")
// AssumeRoleWithWebIdentity
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)).
Queries("Action", "AssumeRoleWithWebIdentity").
Queries("Version", stsAPIVersion).
Queries("WebIdentityToken", "{Token:.*}")
} }
// AssumedRoleUser - The identifiers for the temporary security credentials that // AssumeRoleWithWebIdentity - implementation of AWS STS API supporting OAuth2.0
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser // users from web identity provider such as Facebook, Google, or any OpenID
type AssumedRoleUser struct { // Connect-compatible identity provider.
// The ARN of the temporary security credentials that are returned from the //
// AssumeRole action. For more information about ARNs and how to use them in // Eg:-
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html) // $ curl https://minio:9000/?Action=AssumeRoleWithWebIdentity&WebIdentityToken=<jwt>
// in Using IAM. func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) {
// ctx := newContext(r, w, "AssumeRoleWithWebIdentity")
// Arn is a required field
Arn string
// A unique identifier that contains the role ID and the role session name of defer logger.AuditLog(w, r, "AssumeRoleWithWebIdentity", nil)
// the role that is being assumed. The role ID is generated by AWS when the
// role is created.
//
// AssumedRoleId is a required field
AssumedRoleID string `xml:"AssumeRoleId"`
// contains filtered or unexported fields
}
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request. if globalIAMValidators == nil {
type AssumeRoleWithClientGrantsResponse struct { writeSTSErrorResponse(w, ErrSTSNotInitialized)
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"` return
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"` }
ResponseMetadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants // NOTE: this API only accepts JWT tokens.
// request, including temporary credentials that can be used to make Minio API requests. v, err := globalIAMValidators.Get("jwt")
type ClientGrantsResult struct { if err != nil {
// The identifiers for the temporary security credentials that the operation writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
// returns. return
AssumedRoleUser AssumedRoleUser `xml:",omitempty"` }
// The intended audience (also known as client ID) of the web identity token. vars := mux.Vars(r)
// This is traditionally the client identifier issued to the application that m, err := v.Validate(vars["Token"], r.URL.Query().Get("DurationSeconds"))
// requested the client grants. if err != nil {
Audience string `xml:",omitempty"` switch err {
case validator.ErrTokenExpired:
writeSTSErrorResponse(w, ErrSTSWebIdentityExpiredToken)
case validator.ErrInvalidDuration:
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
default:
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
}
return
}
// The temporary security credentials, which include an access key ID, a secret secret := globalServerConfig.GetCredential().SecretKey
// access key, and a security (or session) token. cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
// if err != nil {
// Note: The size of the security token that STS APIs return is not fixed. We logger.LogIf(ctx, err)
// strongly recommend that you make no assumptions about the maximum size. As writeSTSErrorResponse(w, ErrSTSInternalError)
// of this writing, the typical size is less than 4096 bytes, but that can vary. return
// Also, future updates to AWS might require larger sizes. }
Credentials auth.Credentials `xml:",omitempty"`
// A percentage value that indicates the size of the policy in packed form. // JWT has requested a custom claim with policy value set.
// The service rejects any policy with a packed size greater than 100 percent, // This is a Minio STS API specific value, this value should
// which means the policy exceeded the allowed space. // be set and configured on your identity provider as part of
PackedPolicySize int `xml:",omitempty"` // JWT custom claims.
var policyName string
if v, ok := m["policy"]; ok {
policyName, _ = v.(string)
}
// The issuing authority of the web identity token presented. For OpenID Connect var subFromToken string
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens, if v, ok := m["sub"]; ok {
// this contains the value of the ProviderId parameter that was passed in the subFromToken, _ = v.(string)
// AssumeRoleWithClientGrants request. }
Provider string `xml:",omitempty"`
// The unique user identifier that is returned by the identity provider. // Set the newly generated credentials.
// This identifier is associated with the Token that was submitted if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to logger.LogIf(ctx, err)
// the user and the application that acquired the ClientGrantsToken (pairwise identifier). writeSTSErrorResponse(w, ErrSTSInternalError)
// For OpenID Connect ID tokens, this field contains the value returned by the identity return
// provider as the token's sub (Subject) claim. }
SubjectFromToken string `xml:",omitempty"`
// Notify all other Minio peers to reload temp users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
encodedSuccessResponse := encodeResponse(&AssumeRoleWithWebIdentityResponse{
Result: WebIdentityResult{
Credentials: cred,
SubjectFromWebIdentityToken: subFromToken,
},
})
writeSuccessResponseXML(w, encodedSuccessResponse)
} }
// AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting // AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting
// OAuth2.0 client grants. // OAuth2.0 client credential grants.
// //
// Eg:- // Eg:-
// $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt> // $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt>
@ -173,6 +194,11 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
policyName, _ = v.(string) policyName, _ = v.(string)
} }
var subFromToken string
if v, ok := m["sub"]; ok {
subFromToken, _ = v.(string)
}
// Set the newly generated credentials. // Set the newly generated credentials.
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil { if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
@ -180,8 +206,19 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
return return
} }
// Notify all other Minio peers to reload temp users
for host, err := range globalNotificationSys.LoadUsers() {
if err != nil {
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
logger.LogIf(ctx, err)
}
}
encodedSuccessResponse := encodeResponse(&AssumeRoleWithClientGrantsResponse{ encodedSuccessResponse := encodeResponse(&AssumeRoleWithClientGrantsResponse{
Result: ClientGrantsResult{Credentials: cred}, Result: ClientGrantsResult{
Credentials: cred,
SubjectFromToken: subFromToken,
},
}) })
writeSuccessResponseXML(w, encodedSuccessResponse) writeSuccessResponseXML(w, encodedSuccessResponse)

View File

@ -11,7 +11,8 @@ Following are advantages for using temporary credentials:
- Temporary credentials have a limited lifetime, there is no need to rotate them or explicitly revoke them. Expired temporary credentials cannot be reused. - Temporary credentials have a limited lifetime, there is no need to rotate them or explicitly revoke them. Expired temporary credentials cannot be reused.
## Identity Federation ## Identity Federation
[**Client grants**](https://github.com/minio/minio/blob/master/docs/sts/client-grants.md) - Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, WSO2. This is known as the client grants approach to temporary access. Using this approach helps clients keep Minio credentials to be secured. Minio STS supports client grants, tested against identity providers such as WSO2, KeyCloak. - [**Client grants**](https://github.com/minio/minio/blob/master/docs/sts/client-grants.md) - Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, WSO2. This is known as the client grants approach to temporary access. Using this approach helps clients keep Minio credentials to be secured. Minio STS supports client grants, tested against identity providers such as WSO2, KeyCloak.
- [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) - Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as Facebook, Google etc.
## Get started ## Get started
In this document we will explain in detail on how to configure all the prerequisites, primarily WSO2, OPA (open policy agent). In this document we will explain in detail on how to configure all the prerequisites, primarily WSO2, OPA (open policy agent).
@ -46,11 +47,11 @@ export MINIO_ETCD_ENDPOINTS=http://localhost:2379
minio gateway s3 minio gateway s3
``` ```
### 4. Test using full-example.go ### 4. Test using client-grants.go
On another terminal run `full-example.go` a sample client application which obtains JWT access tokens from an identity provider, in our case its WSO2. Uses the returned access token response to get new temporary credentials from the Minio server using the STS API call `AssumeRoleWithClientGrants`. On another terminal run `client-grants.go` a sample client application which obtains JWT access tokens from an identity provider, in our case its WSO2. Uses the returned access token response to get new temporary credentials from the Minio server using the STS API call `AssumeRoleWithClientGrants`.
``` ```
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
##### Credentials ##### Credentials
{ {

View File

@ -1,3 +1,5 @@
// +build ignore
/* /*
* Minio Cloud Storage, (C) 2018 Minio, Inc. * Minio Cloud Storage, (C) 2018 Minio, Inc.
* *
@ -122,6 +124,7 @@ func main() {
v.Set("Action", "AssumeRoleWithClientGrants") v.Set("Action", "AssumeRoleWithClientGrants")
v.Set("Token", idpToken.AccessToken) v.Set("Token", idpToken.AccessToken)
v.Set("DurationSeconds", fmt.Sprintf("%d", idpToken.Expiry)) v.Set("DurationSeconds", fmt.Sprintf("%d", idpToken.Expiry))
v.Set("Version", "2011-06-15")
u, err := url.Parse(stsEndpoint) u, err := url.Parse(stsEndpoint)
if err != nil { if err != nil {

View File

@ -1,5 +1,5 @@
## AssumeRoleWithClientGrants [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) ## AssumeRoleWithClientGrants [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
Returns a set of temporary security credentials for applications/clients who have been authenticated through client grants provided by identity provider. Example providers include WSO2, KeyCloak etc. Returns a set of temporary security credentials for applications/clients who have been authenticated through client credential grants provided by identity provider. Example providers include WSO2, KeyCloak etc.
Calling AssumeRoleWithClientGrants does not require the use of Minio default credentials. Therefore, client application can be distributed that requests temporary security credentials without including Minio default credentials. Instead, the identity of the caller is validated by using a JWT access token from the identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. Calling AssumeRoleWithClientGrants does not require the use of Minio default credentials. Therefore, client application can be distributed that requests temporary security credentials without including Minio default credentials. Instead, the identity of the caller is validated by using a JWT access token from the identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations.
@ -16,7 +16,7 @@ The duration, in seconds. The value can range from 900 seconds (15 minutes) up t
| *Required* | *No* | | *Required* | *No* |
#### Token #### Token
The OAuth 2.0 access token that is provided by the identity provider. Application must get this token by authenticating the application using client grants before the application makes an AssumeRoleWithClientGrants call. The OAuth 2.0 access token that is provided by the identity provider. Application must get this token by authenticating the application using client credential grants before the application makes an AssumeRoleWithClientGrants call.
| Params | Value | | Params | Value |
| :-- | :-- | | :-- | :-- |
@ -84,7 +84,7 @@ Testing with an example
> Obtaining client ID and secrets follow [WSO2 configuring documentation](https://github.com/minio/minio/blob/master/docs/sts/wso2.md) > Obtaining client ID and secrets follow [WSO2 configuring documentation](https://github.com/minio/minio/blob/master/docs/sts/wso2.md)
``` ```
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
##### Credentials ##### Credentials
{ {

View File

@ -43,7 +43,7 @@ NOTE: If `etcd` is configured with `Client-to-server authentication with HTTPS c
### 4. Test with Minio STS API ### 4. Test with Minio STS API
Assuming that you have configured Minio server to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) and once you have obtained the JWT from WSO2 as mentioned in [WSO2 Quickstart Guide](https://github.com/minio/minio/blob/master/docs/sts/wso2.md). Assuming that you have configured Minio server to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) and once you have obtained the JWT from WSO2 as mentioned in [WSO2 Quickstart Guide](https://github.com/minio/minio/blob/master/docs/sts/wso2.md).
``` ```
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
##### Credentials ##### Credentials
{ {

View File

@ -67,7 +67,7 @@ minio server /mnt/data
### 5. Test with Minio STS API ### 5. Test with Minio STS API
Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server. Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server.
``` ```
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
##### Credentials ##### Credentials
{ {

178
docs/sts/web-identity.go Normal file
View File

@ -0,0 +1,178 @@
// +build ignore
/*
* Minio Cloud Storage, (C) 2018 Minio, 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 main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"encoding/xml"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"time"
"golang.org/x/oauth2"
googleOAuth2 "golang.org/x/oauth2/google"
"github.com/minio/minio/pkg/auth"
)
// AssumedRoleUser - The identifiers for the temporary security credentials that
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
type AssumedRoleUser struct {
Arn string
AssumedRoleID string `xml:"AssumeRoleId"`
// contains filtered or unexported fields
}
// AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
type AssumeRoleWithWebIdentityResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
Result WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
ResponseMetadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// WebIdentityResult - Contains the response to a successful AssumeRoleWithWebIdentity
// request, including temporary credentials that can be used to make Minio API requests.
type WebIdentityResult struct {
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
Audience string `xml:",omitempty"`
Credentials auth.Credentials `xml:",omitempty"`
PackedPolicySize int `xml:",omitempty"`
Provider string `xml:",omitempty"`
SubjectFromWebIdentityToken string `xml:",omitempty"`
}
// Returns a base64 encoded random 32 byte string.
func randomState() string {
b := make([]byte, 32)
rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
var (
stsEndpoint string
authEndpoint string
tokenEndpoint string
clientID string
clientSecret string
)
func init() {
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
flag.StringVar(&authEndpoint, "auth-ep", googleOAuth2.Endpoint.AuthURL, "Auth endpoint")
flag.StringVar(&tokenEndpoint, "token-ep", googleOAuth2.Endpoint.TokenURL, "Token endpoint")
flag.StringVar(&clientID, "cid", "", "Client ID")
flag.StringVar(&clientSecret, "csec", "", "Client secret")
}
func main() {
flag.Parse()
if clientID == "" || clientSecret == "" {
flag.PrintDefaults()
return
}
ctx := context.Background()
config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authEndpoint,
TokenURL: tokenEndpoint,
},
RedirectURL: "http://localhost:8080/oauth2/callback",
Scopes: []string{"openid", "profile", "email"},
}
state := randomState()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound)
})
http.HandleFunc("/oauth2/callback", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
if oauth2Token.Valid() {
v := url.Values{}
v.Set("Action", "AssumeRoleWithWebIdentity")
v.Set("WebIdentityToken", fmt.Sprintf("%s", oauth2Token.Extra("id_token")))
v.Set("DurationSeconds", fmt.Sprintf("%d", int64(oauth2Token.Expiry.Sub(time.Now().UTC()).Seconds())))
v.Set("Version", "2011-06-15")
u, err := url.Parse("http://localhost:9000")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
u.RawQuery = v.Encode()
req, err := http.NewRequest("POST", u.String(), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, resp.Status, resp.StatusCode)
return
}
a := AssumeRoleWithWebIdentityResponse{}
if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("##### Credentials\n"))
c, err := json.MarshalIndent(a.Result.Credentials, "", "\t")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(c)
}
})
log.Printf("listening on http://%s/", "localhost:8080")
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

88
docs/sts/web-identity.md Normal file
View File

@ -0,0 +1,88 @@
## AssumeRoleWithWebIdentity [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
Calling AssumeRoleWithWebIdentity does not require the use of Minio default credentials. Therefore, you can distribute an application (for example, on mobile devices) that requests temporary security credentials without including Minio default credentials in the application. Instead, the identity of the caller is validated by using a JWT access token from the web identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations.
By default, the temporary security credentials created by AssumeRoleWithWebIdentity last for one hour. However, use the optional DurationSeconds parameter to specify the duration of the credentials. This value varies from 900 seconds (15 minutes) up to the maximum session duration to 12 hours.
### Request Parameters
#### DurationSeconds
The duration, in seconds. The value can range from 900 seconds (15 minutes) up to the 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds.
| Params | Value |
| :-- | :-- |
| *Type* | *Integer* |
| *Valid Range* | *Minimum value of 900. Maximum value of 43200.* |
| *Required* | *No* |
#### WebIdentityToken
The OAuth 2.0 access token that is provided by the web identity provider. Application must get this token by authenticating the user who is using your application with a web identity provider before the application makes an AssumeRoleWithWebIdentity call.
| Params | Value |
| :-- | :-- |
| *Type* | *String* |
| *Length Constraints* | *Minimum length of 4. Maximum length of 2048.* |
| *Required* | *Yes* |
#### Response Elements
XML response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements)
#### Errors
XML error response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_Errors)
#### Sample Request
```
http://minio.cluster:9000?Action=AssumeRoleWithWebIdentity&DurationSeconds=3600&WebIdentityToken=eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiYXpwIjoiUG9FZ1hQNnVWTzQ1SXNFTlJuZ0RYajVBdTVZYSIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTU0MTgwOTU4MiwiaWF0IjoxNTQxODA1OTgyLCJqdGkiOiI2Y2YyMGIwZS1lNGZmLTQzZmQtYTdiYS1kYTc3YTE3YzM2MzYifQ.Jm29jPliRvrK6Os34nSK3rhzIYLFjE__zdVGNng3uGKXGKzP3We_i6NPnhA0szJXMOKglXzUF1UgSz8MctbaxFS8XDusQPVe4LkB_45hwBm6TmBxzui911nt-1RbBLN_jZIlvl2lPrbTUH5hSn9kEkph6seWanTNQpz9tNEoVa6R_OX3kpJqxe8tLQUWw453A1JTwFNhdHa6-f1K8_Q_eEZ_4gOYINQ9t_fhTibdbkXZkJQFLop-Jwoybi9s4nwQU_dATocgcufq5eCeNItQeleT-23lGxIz0X7CiJrJynYLdd-ER0F77SumqEb5iCxhxuf4H7dovwd1kAmyKzLxpw
```
#### Sample Response
```
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
<AssumedRoleUser>
<Arn/>
<AssumeRoleId/>
</AssumedRoleUser>
<Credentials>
<AccessKeyId>Y4RJU1RNFGK48LGO9I2S</AccessKeyId>
<SecretAccessKey>sYLRKS1Z7hSjluf6gEbb9066hnx315wHTiACPAjg</SecretAccessKey>
<Expiration>2018-11-09T16:51:11-08:00</Expiration>
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZNFJKVTFSTkZHSzQ4TEdPOUkyUyIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTQxODExMDcxLCJpYXQiOjE1NDE4MDc0NzEsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiYTBiMjc2MjktZWUxYS00M2JmLTg3MzktZjMzNzRhNGNkYmMwIn0.ewHqKVFTaP-j_kgZrcOEKroNUjk10GEp8bqQjxBbYVovV0nHO985VnRESFbcT6XMDDKHZiWqN2vi_ETX_u3Q-w</SessionToken>
</Credentials>
</AssumeRoleWithWebIdentityResult>
<ResponseMetadata/>
</AssumeRoleWithWebIdentityResponse>
```
#### Testing
```
$ export MINIO_ACCESS_KEY=minio
$ export MINIO_SECRET_KEY=minio123
$ export MINIO_IAM_JWKS_URL=https://www.googleapis.com/oauth2/v3/certs
$ minio server /mnt/export
$ mc admin config get myminio
...
{
"openid": {
"jwks": {
"url": "https://www.googleapis.com/oauth2/v3/certs"
}
}
}
```
Testing with an example
> Visit [Google Developer Console](https://console.cloud.google.com) under Project, APIs, Credentials to get your OAuth2 client credentials. Add `http://localhost:8080/oauth2/callback` as a valid OAuth2 Redirect URL.
```
go run web-identity.go -cid 204367807228-ok7601k6gj1pgge7m09h7d79co8p35xx.apps.googleusercontent.com -csec XsT_PgPdT1nO9DD45rMLJw7G
2018/12/26 17:49:36 listening on http://localhost:8080/
```
### Authorization Flow
- Visit http://localhost:8080, login will direct the user to the Google OAuth2 Auth URL to obtain a permission grant.
- The redirection URI (callback handler) receives the OAuth2 callback, verifies the state parameter, and obtains a Token.
- Using the access token the callback handler further talks to Google OAuth2 Token URL to obtain an JWT id_token.
- Once obtained the JWT id_token is further sent to STS endpoint i.e Minio to retrive temporary credentials.
- Temporary credentials are displayed on the browser upon successful retrieval.

View File

@ -75,7 +75,7 @@ minio server /mnt/data
Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server. Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server.
``` ```
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
##### Credentials ##### Credentials
{ {

View File

@ -34,8 +34,8 @@ import (
// JWKSArgs - RSA authentication target arguments // JWKSArgs - RSA authentication target arguments
type JWKSArgs struct { type JWKSArgs struct {
URL *xnet.URL `json:"url"` URL *xnet.URL `json:"url"`
publicKey crypto.PublicKey publicKeys map[string]crypto.PublicKey
} }
// Validate JWT authentication target arguments // Validate JWT authentication target arguments
@ -64,9 +64,14 @@ func (r *JWKSArgs) PopulatePublicKey() error {
return err return err
} }
r.publicKey, err = jwk.Keys[0].DecodePublicKey() r.publicKeys = make(map[string]crypto.PublicKey)
if err != nil { for _, key := range jwk.Keys {
return err var publicKey crypto.PublicKey
publicKey, err = key.DecodePublicKey()
if err != nil {
return err
}
r.publicKeys[key.Kid] = publicKey
} }
return nil return nil
@ -172,18 +177,19 @@ func newCustomHTTPTransport(insecure bool) *http.Transport {
// Validate - validates the access token. // Validate - validates the access token.
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) { func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
jp := new(jwtgo.Parser)
jp.ValidMethods = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) { keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodRSA); !ok { kid, ok := jwtToken.Header["kid"].(string)
if _, ok = jwtToken.Method.(*jwtgo.SigningMethodECDSA); ok { if !ok {
return p.args.publicKey, nil return nil, fmt.Errorf("Invalid kid value %v", jwtToken.Header["kid"])
}
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
} }
return p.args.publicKey, nil return p.args.publicKeys[kid], nil
} }
var claims jwtgo.MapClaims var claims jwtgo.MapClaims
jwtToken, err := jwtgo.ParseWithClaims(token, &claims, keyFuncCallback) jwtToken, err := jp.ParseWithClaims(token, &claims, keyFuncCallback)
if err != nil { if err != nil {
if err = p.args.PopulatePublicKey(); err != nil { if err = p.args.PopulatePublicKey(); err != nil {
return nil, err return nil, err

View File

@ -44,10 +44,10 @@ func TestJWT(t *testing.T) {
t.Fatalf("Expected 1 keys, got %d", len(jk.Keys)) t.Fatalf("Expected 1 keys, got %d", len(jk.Keys))
} }
keys := make([]crypto.PublicKey, len(jk.Keys)) keys := make(map[string]crypto.PublicKey, len(jk.Keys))
for ii, jks := range jk.Keys { for ii, jks := range jk.Keys {
var err error var err error
keys[ii], err = jks.DecodePublicKey() keys[jks.Kid], err = jks.DecodePublicKey()
if err != nil { if err != nil {
t.Fatalf("Failed to decode key %d: %v", ii, err) t.Fatalf("Failed to decode key %d: %v", ii, err)
} }
@ -59,8 +59,8 @@ func TestJWT(t *testing.T) {
} }
jwt := NewJWT(JWKSArgs{ jwt := NewJWT(JWKSArgs{
URL: u1, URL: u1,
publicKey: keys[0], publicKeys: keys,
}) })
if jwt.ID() != "jwt" { if jwt.ID() != "jwt" {
t.Fatalf("Uexpected id %s for the validator", jwt.ID()) t.Fatalf("Uexpected id %s for the validator", jwt.ID())