OAuth2 client implementation for Go with support for authorization code flow, client credentials, device authorization, and JWT flows
Create downscoped tokens with restricted IAM permissions for Google Cloud Storage, enabling principle of least privilege and secure third-party access.
import "golang.org/x/oauth2/google/downscope"The downscope package implements the ability to downscope (restrict) the Identity and Access Management permissions that a short-lived token can use. This feature is currently only supported by Google Cloud Storage.
Use cases:
Documentation: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error)Returns a configured downscopingTokenSource that creates tokens with restricted permissions based on the provided configuration.
type DownscopingConfig struct {
// RootSource is the TokenSource used to create the downscoped token
// The downscoped token has a subset of the RootSource's access
RootSource oauth2.TokenSource
// Rules defines the access held by the new downscoped token
// One or more AccessBoundaryRules required (maximum 10)
Rules []AccessBoundaryRule
// UniverseDomain is the default service domain for a given Cloud universe
// Default: "googleapis.com" (optional)
UniverseDomain string
}type AccessBoundaryRule struct {
// AvailableResource is the full resource name of the GCS bucket
// Format: //storage.googleapis.com/projects/_/buckets/bucket-name
AvailableResource string `json:"availableResource"`
// AvailablePermissions defines upper bound on available permissions
// Each value is an IAM role identifier with the prefix "inRole:"
// Example: "inRole:roles/storage.objectViewer"
AvailablePermissions []string `json:"availablePermissions"`
// Condition restricts availability of permissions to specific objects (optional)
Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"`
}type AvailabilityCondition struct {
// Expression specifies the Cloud Storage objects where permissions are available
// Uses IAM Conditions syntax
Expression string `json:"expression"`
// Title is a short identifier for the condition (optional)
Title string `json:"title,omitempty"`
// Description details the purpose of the condition (optional)
Description string `json:"description,omitempty"`
}IAM Conditions documentation: https://cloud.google.com/iam/docs/conditions-overview
package main
import (
"context"
"log"
"cloud.google.com/go/storage"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/downscope"
"google.golang.org/api/option"
)
func main() {
ctx := context.Background()
// Get root credentials with full access
rootSource, err := google.DefaultTokenSource(ctx,
"https://www.googleapis.com/auth/cloud-platform")
if err != nil {
log.Fatal(err)
}
// Define access boundary - read-only access to specific bucket
config := downscope.DownscopingConfig{
RootSource: rootSource,
Rules: []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/my-bucket",
AvailablePermissions: []string{
"inRole:roles/storage.objectViewer",
},
},
},
}
// Create downscoped token source
downscopedTS, err := downscope.NewTokenSource(ctx, config)
if err != nil {
log.Fatal(err)
}
// Use downscoped token with GCS client
client, err := storage.NewClient(ctx, option.WithTokenSource(downscopedTS))
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Client now has only read access to my-bucket
bucket := client.Bucket("my-bucket")
// ... use bucket with restricted permissions ...
}config := downscope.DownscopingConfig{
RootSource: rootSource,
Rules: []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/bucket-1",
AvailablePermissions: []string{
"inRole:roles/storage.objectViewer", // Read-only
},
},
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/bucket-2",
AvailablePermissions: []string{
"inRole:roles/storage.objectCreator", // Write-only
},
},
},
}
downscopedTS, err := downscope.NewTokenSource(ctx, config)Restrict access to objects matching specific conditions:
config := downscope.DownscopingConfig{
RootSource: rootSource,
Rules: []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/my-bucket",
AvailablePermissions: []string{
"inRole:roles/storage.objectViewer",
},
Condition: &downscope.AvailabilityCondition{
Title: "Access to specific prefix",
Expression: `resource.name.startsWith("projects/_/buckets/my-bucket/objects/documents/")`,
Description: "Only allow access to objects under documents/ prefix",
},
},
},
}A token broker issues downscoped tokens to workloads:
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/downscope"
)
// TokenRequest from a workload
type TokenRequest struct {
Bucket string `json:"bucket"`
Permissions []string `json:"permissions"`
Prefix string `json:"prefix,omitempty"`
}
// TokenResponse to the workload
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
func main() {
ctx := context.Background()
// Broker's root credentials
rootSource, err := google.DefaultTokenSource(ctx,
"https://www.googleapis.com/auth/cloud-platform")
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
// Authenticate the workload (implementation specific)
// ...
// Parse token request
var req TokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// Create downscoped configuration
rule := downscope.AccessBoundaryRule{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + req.Bucket,
AvailablePermissions: req.Permissions,
}
// Add condition if prefix specified
if req.Prefix != "" {
rule.Condition = &downscope.AvailabilityCondition{
Title: "Prefix restriction",
Expression: `resource.name.startsWith("projects/_/buckets/` +
req.Bucket + `/objects/` + req.Prefix + `")`,
}
}
config := downscope.DownscopingConfig{
RootSource: rootSource,
Rules: []downscope.AccessBoundaryRule{rule},
}
// Generate downscoped token
downscopedTS, err := downscope.NewTokenSource(ctx, config)
if err != nil {
http.Error(w, "Token generation failed", http.StatusInternalServerError)
return
}
// Get token
token, err := downscopedTS.Token()
if err != nil {
http.Error(w, "Token retrieval failed", http.StatusInternalServerError)
return
}
// Return token to workload
resp := TokenResponse{
AccessToken: token.AccessToken,
ExpiresIn: int(token.Expiry.Sub(time.Now()).Seconds()),
TokenType: token.TokenType,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}// Read-only access
"inRole:roles/storage.objectViewer"
// Write-only access (create objects)
"inRole:roles/storage.objectCreator"
// Read and write access
"inRole:roles/storage.objectUser"
// Full control (including delete)
"inRole:roles/storage.objectAdmin"Expression: `resource.name.startsWith("projects/_/buckets/my-bucket/objects/folder/")`Expression: `resource.name.endsWith(".pdf") || resource.name.endsWith(".txt")`Expression: `request.time < timestamp("2024-12-31T23:59:59Z")`Expression: `resource.name.startsWith("projects/_/buckets/my-bucket/objects/public/") &&
request.time < timestamp("2024-12-31T23:59:59Z")`downscopedTS, err := downscope.NewTokenSource(ctx, config)
if err != nil {
switch {
case strings.Contains(err.Error(), "rootSource cannot be nil"):
log.Fatal("Root token source is required")
case strings.Contains(err.Error(), "length of AccessBoundaryRules"):
log.Fatal("Must have 1-10 access boundary rules")
case strings.Contains(err.Error(), "nonempty AvailableResource"):
log.Fatal("All rules must specify AvailableResource")
case strings.Contains(err.Error(), "nonempty AvailablePermissions"):
log.Fatal("All rules must specify AvailablePermissions")
default:
log.Fatalf("Downscope error: %v", err)
}
}package main
import (
"bytes"
"context"
"encoding/json"
"io"
"log"
"net/http"
"cloud.google.com/go/storage"
"golang.org/x/oauth2"
"google.golang.org/api/option"
)
// TokenRequest sent to broker
type TokenRequest struct {
Bucket string `json:"bucket"`
Permissions []string `json:"permissions"`
Prefix string `json:"prefix,omitempty"`
}
func main() {
ctx := context.Background()
// Request downscoped token from broker
token, err := requestTokenFromBroker(ctx, TokenRequest{
Bucket: "my-data-bucket",
Permissions: []string{"inRole:roles/storage.objectViewer"},
Prefix: "reports/",
})
if err != nil {
log.Fatal(err)
}
// Use downscoped token
client, err := storage.NewClient(ctx,
option.WithTokenSource(oauth2.StaticTokenSource(token)))
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Access only allowed objects
bucket := client.Bucket("my-data-bucket")
it := bucket.Objects(ctx, &storage.Query{Prefix: "reports/"})
for {
attrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
log.Fatal(err)
}
log.Printf("Object: %s\n", attrs.Name)
}
}
func requestTokenFromBroker(ctx context.Context, req TokenRequest) (*oauth2.Token, error) {
body, _ := json.Marshal(req)
resp, err := http.Post("https://token-broker.example.com/token",
"application/json", bytes.NewReader(body))
if err != nil {
return nil, err)
}
defer resp.Body.Close()
var tokenResp struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, err
}
return &oauth2.Token{
AccessToken: tokenResp.AccessToken,
TokenType: tokenResp.TokenType,
Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
}, nil
}Install with Tessl CLI
npx tessl i tessl/golang-golang-org-x--oauth2