Index: server/auth/delegation/minter.go |
diff --git a/server/auth/delegation/minter.go b/server/auth/delegation/minter.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2e3b6598236b7bf4886ae4e2d763590f28830d9e |
--- /dev/null |
+++ b/server/auth/delegation/minter.go |
@@ -0,0 +1,218 @@ |
+// Copyright 2016 The LUCI Authors. All rights reserved. |
+// Use of this source code is governed under the Apache License, Version 2.0 |
+// that can be found in the LICENSE file. |
+ |
+package delegation |
+ |
+import ( |
+ "encoding/gob" |
+ "fmt" |
+ "time" |
+ |
+ "golang.org/x/net/context" |
+ |
+ "github.com/luci/luci-go/common/clock" |
+ "github.com/luci/luci-go/server/auth/identity" |
+ "github.com/luci/luci-go/server/auth/internal" |
+) |
+ |
+// TokenRequest describes parameters of a new delegation token. |
+type TokenRequest struct { |
+ // AuthServiceURL is root URL (e.g. https://<host>) of the service to use for |
+ // minting the delegation token. |
+ // |
+ // The token will be signed by the service's private key. Only services that |
+ // trust this auth service would be able to accept the new token. |
+ // |
+ // Required. |
+ AuthServiceURL string |
+ |
+ // Audience is to whom caller's identity is delegated. |
+ // |
+ // Only clients that can prove they are intended audience (e.g. by presenting |
+ // valid access token) would be able to use the delegation token. |
+ // |
+ // Must be empty if UnlimitedAudience is true. |
+ Audience []identity.Identity |
+ |
+ // AudienceGroups can be used to specify a group (or a bunch of groups) as |
+ // a target audience of the token. |
+ // |
+ // It works in addition to Audience. |
+ // |
+ // Must be empty if UnlimitedAudience is true. |
+ AudienceGroups []string |
+ |
+ // UnlimitedAudience, if true, indicates that the delegation token can be |
+ // used by any bearer. |
+ // |
+ // Tokens with unlimited audience are roughly as powerful as OAuth access |
+ // tokens and should be used only if absolutely necessary. |
+ // |
+ // Prefer to limit token's audience as much as possible. See Audience and |
+ // AudienceGroups fields above. |
+ // |
+ // If UnlimitedAudience is true, Audience and AudienceGroups must be empty. |
+ UnlimitedAudience bool |
+ |
+ // TargetServices is a list of 'service:...' identities with services that |
+ // accept the token. |
+ // |
+ // This can be used to limit the scope of the token. |
+ // |
+ // Must be empty if Untargeted is true. |
+ TargetServices []identity.Identity |
+ |
+ // Untargeted, if true, indicates that the delegation token should be accepted |
+ // by any LUCI service. |
+ // |
+ // Use this if you are preparing a token in advance, not yet knowing where it |
+ // is going to be used. |
+ // |
+ // If Untargeted is true, TargetServices must be empty. |
+ Untargeted bool |
+ |
+ // Impersonate defines on whose behalf the token is being created. |
+ // |
+ // By default the token delegates an identity of whoever requested it. This |
+ // identity is extracted from credentials that accompany token minting request |
+ // (e.g. OAuth access token). |
+ // |
+ // By using 'Impersonate' a caller can make a token that delegates someone |
+ // else's identity, effectively producing an impersonation token. |
+ // |
+ // Only limited set of callers can do this. The set of who can be impersonated |
+ // is also limited. The rules are enforced by the auth service. |
+ Impersonate identity.Identity |
+ |
+ // ValidityDuration defines for how long the token would be valid. |
+ // |
+ // Maximum theoretical TTL is limited by the lifetime of the signing key of |
+ // the auth service (~= 24 hours). Minimum acceptable value is 30 sec. |
+ // |
+ // Required. |
+ ValidityDuration time.Duration |
+ |
+ // Intent is a reason why the token is created. |
+ // |
+ // Used only for logging purposes on the auth service, will be indexed. Should |
+ // be a short identifier-like string. |
+ // |
+ // Optional. |
+ Intent string |
+} |
+ |
+// Token is actual delegation token with its expiration time and ID. |
+type Token struct { |
+ Token string // base64-encoded URL-safe blob with the token |
+ Expiry time.Time // UTC time when it expires (also encoded in Token) |
+ SubtokenID string // identifier of the token (also encoded in Token) |
+} |
+ |
+// CreateToken makes a request to the auth service to generate the token. |
+// |
+// If uses current service's credentials to authenticate the request. |
+// |
+// If req.Impersonate is not used, the identity encoded in the authentication |
+// credentials will be delegated by the token, otherwise the service will check |
+// that caller is allowed to do the impersonation and will return a token that |
+// delegates the identity specified by req.Impersonate. |
+func CreateToken(c context.Context, req TokenRequest) (*Token, error) { |
+ // See https://github.com/luci/luci-py/blob/master/appengine/auth_service/delegation.py. |
+ var params struct { |
+ Audience []string `json:"audience,omitempty"` |
+ Services []string `json:"services,omitempty"` |
+ ValidityDuration int `json:"validity_duration"` |
+ Impersonate string `json:"impersonate,omitempty"` |
+ Intent string `json:"intent,omitempty"` |
+ } |
+ |
+ // Audience. |
+ params.Audience = make([]string, 0, len(req.Audience)+len(req.AudienceGroups)) |
+ for _, aud := range req.Audience { |
+ if err := aud.Validate(); err != nil { |
+ return nil, err |
+ } |
+ params.Audience = append(params.Audience, string(aud)) |
+ } |
+ for _, group := range req.AudienceGroups { |
+ params.Audience = append(params.Audience, "group:"+group) |
+ } |
+ switch { |
+ case req.UnlimitedAudience && len(params.Audience) != 0: |
+ return nil, fmt.Errorf("delegation: can't specify audience for UnlimitedAudience=true token") |
+ case !req.UnlimitedAudience && len(params.Audience) == 0: |
+ return nil, fmt.Errorf("delegation: either Audience/AudienceGroups or UnlimitedAudience=true are required") |
+ } |
+ if req.UnlimitedAudience { |
+ params.Audience = []string{"*"} |
+ } |
+ |
+ // Services. |
+ params.Services = make([]string, 0, len(req.TargetServices)) |
+ for _, srv := range req.TargetServices { |
+ if err := srv.Validate(); err != nil { |
+ return nil, err |
+ } |
+ params.Services = append(params.Services, string(srv)) |
+ } |
+ switch { |
+ case req.Untargeted && len(params.Services) != 0: |
+ return nil, fmt.Errorf("delegation: can't specify TargetServices for Untargeted=true token") |
+ case !req.Untargeted && len(params.Services) == 0: |
+ return nil, fmt.Errorf("delegation: either TargetServices or Untargeted=true are required") |
+ } |
+ if req.Untargeted { |
+ params.Services = []string{"*"} |
+ } |
+ |
+ // Validity duration. |
+ params.ValidityDuration = int(req.ValidityDuration / time.Second) |
+ if params.ValidityDuration < 30 { |
+ return nil, fmt.Errorf("ValidityDuration must be >= 30 sec, got %s", req.ValidityDuration) |
+ } |
+ if params.ValidityDuration > 24*3600 { |
+ return nil, fmt.Errorf("ValidityDuration must be <= 24h, got %s", req.ValidityDuration) |
+ } |
+ |
+ // The rest of the fields. |
+ if req.Impersonate != "" { |
+ if err := req.Impersonate.Validate(); err != nil { |
+ return nil, err |
+ } |
+ params.Impersonate = string(req.Impersonate) |
+ } |
+ params.Intent = req.Intent |
+ |
+ var response struct { |
+ DelegationToken string `json:"delegation_token,omitempty"` |
+ ValidityDuration int `json:"validity_duration,omitempty"` |
+ SubtokenID string `json:"subtoken_id,omitempty"` |
+ Text string `json:"text,omitempty"` // for error responses |
+ } |
+ |
+ httpReq := internal.Request{ |
+ Method: "POST", |
+ URL: req.AuthServiceURL + "/auth_service/api/v1/delegation/token/create", |
+ Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, |
+ Body: ¶ms, |
+ Out: &response, |
+ } |
+ if err := httpReq.Do(c); err != nil { |
+ if response.Text != "" { |
+ err = fmt.Errorf("%s - %s", err, response.Text) |
+ } |
+ return nil, err |
+ } |
+ |
+ return &Token{ |
+ Token: response.DelegationToken, |
+ Expiry: clock.Now(c).Add(time.Duration(response.ValidityDuration) * time.Second).UTC(), |
+ SubtokenID: response.SubtokenID, |
+ }, nil |
+} |
+ |
+func init() { |
+ // For token cache. |
+ gob.Register(Token{}) |
+} |