권한이 축소된 단기 사용자 인증 정보 만들기

이 페이지에서는 사용자 인증 정보 액세스 경계를 사용하여 Cloud Storage 권한이 축소된 OAuth 2.0 액세스 토큰을 만드는 방법을 설명합니다.

권한이 축소된 토큰을 만드는 프로세스에는 다음 단계가 포함됩니다.

  1. 사용자나 서비스 계정에 적절한 IAM 역할을 부여합니다.
  2. 사용자나 서비스 계정에 사용할 수 있는 권한의 상한값을 설정하는 사용자 인증 정보 액세스 경계를 정의합니다.
  3. 사용자나 서비스 계정에 대한 OAuth 2.0 액세스 토큰을 만듭니다.
  4. 사용자 인증 정보 액세스 경계를 준수하는 새 토큰으로 OAuth 2.0 액세스 토큰을 교환합니다.

그런 다음 권한이 축소된 새로운 OAuth 2.0 액세스 토큰을 사용하여 Cloud Storage에 대한 요청을 인증할 수 있습니다.

시작하기 전에

사용자 인증 정보 액세스 경계를 사용하기 전에 다음 요구사항을 충족하는지 확인하세요.

  • 다른Google Cloud 서비스가 아닌 Cloud Storage의 권한만 축소해야 합니다.

    다른 Google Cloud서비스의 권한을 축소해야 하는 경우 서비스 계정을 여러 개 만들고 서비스 계정마다 서로 다른 역할 집합을 부여하면 됩니다.

  • 인증에 OAuth 2.0 액세스 토큰을 사용할 수 있습니다. 다른 유형의 단기 사용자 인증 정보는 사용자 인증 정보 액세스 경계를 지원하지 않습니다.

또한 필수 API를 사용 설정해야 합니다.

  • Enable the IAM and Security Token Service APIs.

    Enable the APIs

IAM 역할 부여

사용자 인증 정보 액세스 경계는 리소스에 사용 가능한 권한의 상한값을 설정합니다. 주 구성원의 권한을 뺄 수 있지만 주 구성원이 아직 보유하지 않은 권한을 추가할 수는 없습니다.

따라서 주 구성원에게 Cloud Storage 버킷에서 또는 프로젝트와 같은 상위 수준의 리소스에서 필요한 권한을 제공하는 역할을 부여해야 합니다.

예를 들어 서비스 계정이 버킷에 객체를 만들 수 있도록 권한을 축소한 단기 사용자 인증 정보를 만들어야 한다고 가정합니다.

  • 최소한 스토리지 객체 생성자 역할(roles/storage.objectCreator)과 같이 storage.objects.create 권한이 포함된 역할을 서비스 계정에 부여해야 합니다. 또한 사용자 인증 정보 액세스 경계에 이 권한이 있어야 합니다.
  • 스토리지 객체 관리자 역할(roles/storage.objectAdmin)과 같이 더 많은 권한이 포함된 역할을 부여할 수도 있습니다. 서비스 계정은 역할 부여 및 사용자 인증 정보 액세스 경계 모두에 표시되는 권한만 사용할 수 있습니다.

Cloud Storage의 사전 정의된 역할은 Cloud Storage 역할을 참조하세요.

사용자 인증 정보 액세스 경계 정의

사용자 인증 정보 액세스 경계는 액세스 경계 규칙 목록을 포함하는 객체입니다. 규칙은 사용자나 서비스 계정에 사용할 수 있는 권한의 상한값을 지정하는 파라미터로 구성됩니다. 사용자 인증 정보 액세스 경계를 정의하려면 액세스 경계 규칙과 해당 파라미터를 나열하는 JSON 객체를 만듭니다.

다음은 사용자 인증 정보 액세스 경계의 예시입니다.

{
  "accessBoundary": {
    "accessBoundaryRules": [
      {
        "availablePermissions": [
          "inRole:ROLE_ID"
        ],
        "availableResource": "//storage.googleapis.com/projects/_/buckets/BUCKET_NAME"
         "availabilityCondition": {
            "expression" : "CONDITION"
      }
    ]
  }
}

다음을 바꿉니다.

  • ROLE_ID: 리소스에 사용 가능한 권한의 상한값을 정의하는 사전 정의된 역할이나 커스텀 역할의 ID로 바꿉니다. 예를 들면 roles/storage.objectViewer입니다. 여러 역할을 지정하려면 inRole: 프리픽스 뒤에 역할 ID가 오는 새 줄을 추가합니다. 지정된 역할의 권한만 사용할 수 있습니다.
  • BUCKET_NAME: 규칙이 적용되는 Cloud Storage 버킷의 이름으로 바꿉니다.
  • CONDITION: 선택사항. 권한이 있는 Cloud Storage 객체를 지정하는 조건 표현식으로 바꿉니다. 예를 들어 다음 조건은 이름이 customer-a로 시작하는 객체에 권한을 부여합니다.

    resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')
    

사용자 인증 정보 액세스 경계를 만들고 맞춤설정하는 방법에 대한 자세한 내용은 사용자 인증 정보 액세스 경계 구성요소를 참조하세요.

사용자 인증 정보 액세스 경계의 잠재적 사용 사례 예시는 사용자 인증 정보 액세스 경계 예시를 참조하세요.

OAuth 2.0 액세스 토큰 만들기

권한이 축소된 단기 사용자 인증 정보를 만들려면 일반 OAuth 2.0 액세스 토큰을 만들어야 합니다. 그러면 일반 사용자 인증 정보를 권한이 축소된 사용자 인증 정보와 교환할 수 있습니다. 액세스 토큰을 만들 때 OAuth 2.0 범위 https://www.googleapis.com/auth/cloud-platform을 사용합니다.

서비스 계정에 대한 액세스 토큰을 만들려면 서버 간 OAuth 2.0 흐름을 완료하거나 Service Account Credentials API를 사용하여 OAuth 2.0 액세스 토큰을 생성할 수 있습니다.

사용자의 액세스 토큰을 만들려면 OAuth 2.0 액세스 토큰 가져오기를 참조하세요. OAuth 2.0 Playground를 사용하여 자신의 Google 계정에 대한 액세스 토큰을 만들 수도 있습니다.

OAuth 2.0 액세스 토큰 교환

OAuth 2.0 액세스 토큰을 만든 후에 액세스 토큰을 사용자 인증 정보 액세스 경계를 준수하는 권한이 축소된 토큰으로 교환할 수 있습니다. 이 프로세스에는 일반적으로 토큰 브로커토큰 소비자가 포함됩니다.

  • 토큰 브로커는 사용자 인증 정보 액세스 경계를 정의하고 액세스 토큰을 범위가 축소된 토큰으로 교환합니다.

    토큰 브로커는 지원되는 인증 라이브러리를 사용하여 액세스 토큰을 자동으로 교환할 수도 있고, 보안 토큰 서비스를 호출하여 수동으로 토큰을 교환할 수도 있습니다.

  • 토큰 소비자는 토큰 브로커에서 권한이 축소된 액세스 토큰을 요청한 다음 권한이 축소된 액세스 토큰을 사용하여 다른 작업을 수행합니다.

    토큰 소비자는 지원되는 인증 라이브러리를 사용하여 만료되기 전에 액세스 토큰을 자동으로 새로고침할 수 있습니다. 또는 토큰을 수동으로 새로고침하거나 토큰을 갱신하지 않고 만료되도록 할 수도 있습니다.

권한이 축소된 토큰의 액세스 토큰을 교환하는 데에는 2가지 방법이 있습니다.

  • 클라이언트 측 토큰 교환(권장): 클라이언트가 보안 토큰 서비스 API 서버에서 암호화 자료를 가져옵니다. 클라이언트는 암호화 자료를 통해 일정 기간(예: 1시간) 동안 클라이언트 측에서 다양한 사용자 인증 정보 액세스 경계 규칙을 독립적으로 사용하여 권한이 축소된 토큰을 생성할 수 있습니다. 이러한 접근 방법은 특히 사용자 인증 정보 액세스 경계 규칙을 자주 업데이트해야 하는 클라이언트에서 지연 시간을 줄여주고 효율성을 높여줍니다. 또한 권한 범위가 축소된 많은 고유 토큰을 생성해야 하는 애플리케이션에도 효율적입니다. 이 방법은 더 나은 성능, 확장성, 미래 기능 호환성을 제공하므로 이 방법을 사용하는 것이 좋습니다.

  • 서버 측 토큰 교환: 사용자 인증 정보 액세스 경계 규칙이 변경될 때마다 클라이언트가 보안 토큰 서비스 API 서버에서 권한이 축소된 새 토큰을 요청합니다. 이 접근 방법은 직관적이지만 권한 범위가 축소된 토큰 요청이 있을 때마다 보안 토큰 서비스 API 서버에 대한 왕복 작업이 필요합니다. 이 접근 방법은 권한 범위가 축소된 토큰 요청이 있을 때마다 보안 토큰 서비스 API에 대한 왕복 작업으로 인해 클라이언트 측 토큰 교환을 지원하지 않는 클라이언트 라이브러리가 필요한 고객에게만 권장됩니다.

클라이언트 측 토큰 교화

다음 언어를 사용하여 토큰 브로커와 토큰 소비자를 만들 경우 Google의 인증 라이브러리를 사용하여 클라이언트 측 접근 방법을 통해 토큰 교환 및 새로고침을 자동으로 수행할 수 있습니다.

Java

Java의 경우 com.google.auth:google-auth-library-cab-token-generator 아티팩트 버전 1.32.1 이상을 사용하여 자동으로 토큰을 교환하고 새로고침할 수 있습니다.

아티팩트 버전을 확인하려면 애플리케이션 디렉터리에서 다음 Maven 명령어를 실행합니다.

mvn dependency:list -DincludeArtifactIds=google-auth-library-cab-token-generator

다음 예시에서는 토큰 브로커가 권한이 축소된 토큰을 생성하는 방법을 보여줍니다.

import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.CredentialAccessBoundary;
import com.google.auth.oauth2.GoogleCredentials;
import dev.cel.common.CelValidationException;
import java.io.IOException;
import java.security.GeneralSecurityException;
  public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
      throws IOException {
    // Retrieve the source credentials from ADC.
    GoogleCredentials sourceCredentials =
        GoogleCredentials.getApplicationDefault()
            .createScoped("https://www.googleapis.com/auth/cloud-platform");

    // Initialize the Credential Access Boundary rules.
    String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

    // Downscoped credentials will have readonly access to the resource.
    String availablePermission = "inRole:roles/storage.objectViewer";

    // Only objects starting with the specified prefix string in the object name will be allowed
    // read access.
    String expression =
        "resource.name.startsWith('projects/_/buckets/"
            + bucketName
            + "/objects/"
            + objectPrefix
            + "')";

    // Build the AvailabilityCondition.
    CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
        CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
            .setExpression(expression)
            .build();

    // Define the single access boundary rule using the above properties.
    CredentialAccessBoundary.AccessBoundaryRule rule =
        CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
            .setAvailableResource(availableResource)
            .addAvailablePermission(availablePermission)
            .setAvailabilityCondition(availabilityCondition)
            .build();

    // Define the Credential Access Boundary with all the relevant rules.
    CredentialAccessBoundary credentialAccessBoundary =
        CredentialAccessBoundary.newBuilder().addRule(rule).build();

    // Create an instance of ClientSideCredentialAccessBoundaryFactory.
    ClientSideCredentialAccessBoundaryFactory factory =
        ClientSideCredentialAccessBoundaryFactory.newBuilder()
            .setSourceCredential(sourceCredentials)
            .build();

    // Generate the token and pass it to the Token Consumer.
    try {
      return factory.generateToken(credentialAccessBoundary);
    } catch (GeneralSecurityException | CelValidationException e) {
      throw new IOException("Error generating downscoped token", e);
    }
  }

다음 예시에서는 토큰 소비자가 새로고침 핸들러를 사용하여 권한이 축소된 토큰을 자동으로 가져오고 새로고침하는 방법을 보여줍니다.

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.io.IOException;
  public static String retrieveBlobWithDownscopedToken(
      final String bucketName, final String objectName) throws IOException {
    // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
    // library to seamlessly handle downscoped token refreshes on expiration.
    OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
        new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
          @Override
          public AccessToken refreshAccessToken() throws IOException {
            // The common pattern of usage is to have a token broker pass the downscoped short-lived
            // access tokens to a token consumer via some secure authenticated channel.
            // For illustration purposes, we are generating the downscoped token locally.
            // We want to test the ability to limit access to objects with a certain prefix string
            // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
            // not required if access to all bucket resources are allowed. If access to limited
            // resources in the bucket is needed, this mechanism can be used.
            return DownscopedAccessTokenGenerator
                .getTokenFromBroker(bucketName, objectName);
          }
        };

    AccessToken downscopedToken = handler.refreshAccessToken();

    OAuth2CredentialsWithRefresh credentials =
        OAuth2CredentialsWithRefresh.newBuilder()
            .setAccessToken(downscopedToken)
            .setRefreshHandler(handler)
            .build();

    StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
    Storage storage = options.getService();

    Blob blob = storage.get(bucketName, objectName);
    if (blob == null) {
      return null;
    }
    return new String(blob.getContent());
  }

서버 측 토큰 교환

이 섹션에서는 서비스 측 방법을 통해 토큰을 교환하는 데 사용할 수 있는 다음 방법을 설명합니다.

서버 측 접근 방법을 사용하여 액세스 토큰 교환 및 새로고침 자동 수행

다음 언어 중 하나를 사용하여 토큰 브로커와 토큰 소비자를 만들 경우 Google의 인증 라이브러리를 사용하여 서버 측 토큰 생성 방법에 따라 토큰 교환 및 새로고침을 자동으로 수행할 수 있습니다.

Go

Go의 경우 golang.org/x/oauth2 패키지 v0.0.0-20210819190943-2bc19b11175f 버전 이상으로 토큰을 자동으로 교환하고 새로고침할 수 있습니다.

패키지 버전을 확인하려면 애플리케이션 디렉터리에서 다음 명령어를 실행합니다.

go list -m golang.org/x/oauth2

다음 예시에서는 토큰 브로커가 권한이 축소된 토큰을 생성하는 방법을 보여줍니다.


import (
	"context"
	"fmt"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"
)

// createDownscopedToken would be run on the token broker in order to generate
// a downscoped access token that only grants access to objects whose name begins with prefix.
// The token broker would then pass the newly created token to the requesting token consumer for use.
func createDownscopedToken(bucketName string, prefix string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()
	// A condition can optionally be provided to further restrict access permissions.
	condition := downscope.AvailabilityCondition{
		Expression:  "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "')",
		Title:       prefix + " Only",
		Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
	}
	// Initializes an accessBoundary with one Rule which restricts the downscoped
	// token to only be able to access the bucket "bucketName" and only grants it the
	// permission "storage.objectViewer".
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
			Condition:            &condition, // Optional
		},
	}

	// This Source can be initialized in multiple ways; the following example uses
	// Application Default Credentials.
	var rootSource oauth2.TokenSource

	// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
	rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return fmt.Errorf("failed to generate rootSource: %w", err)
	}

	// downscope.NewTokenSource constructs the token source with the configuration provided.
	dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	tok, err := dts.Token()
	if err != nil {
		return fmt.Errorf("failed to generate token: %w", err)
	}
	// Pass this token back to the token consumer.
	_ = tok
	return nil
}

다음 예시에서는 토큰 소비자가 새로고침 핸들러를 사용하여 권한이 축소된 토큰을 자동으로 가져오고 새로고침하는 방법을 보여줍니다.


import (
	"context"
	"fmt"
	"io"

	"golang.org/x/oauth2/google"
	"golang.org/x/oauth2/google/downscope"

	"cloud.google.com/go/storage"
	"golang.org/x/oauth2"
	"google.golang.org/api/option"
)

// A token consumer should define their own tokenSource. In the Token() method,
// it should send a query to a token broker requesting a downscoped token.
// The token broker holds the root credential that is used to generate the
// downscoped token.
type localTokenSource struct {
	ctx        context.Context
	bucketName string
	brokerURL  string
}

func (lts localTokenSource) Token() (*oauth2.Token, error) {
	var remoteToken *oauth2.Token
	// Usually you would now retrieve remoteToken, an oauth2.Token, from token broker.
	// This snippet performs the same functionality locally.
	accessBoundary := []downscope.AccessBoundaryRule{
		{
			AvailableResource:    "//storage.googleapis.com/projects/_/buckets/" + lts.bucketName,
			AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
		},
	}
	rootSource, err := google.DefaultTokenSource(lts.ctx, "https://www.googleapis.com/auth/cloud-platform")
	if err != nil {
		return nil, fmt.Errorf("failed to generate rootSource: %w", err)
	}
	dts, err := downscope.NewTokenSource(lts.ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
	if err != nil {
		return nil, fmt.Errorf("failed to generate downscoped token source: %w", err)
	}
	// Token() uses the previously declared TokenSource to generate a downscoped token.
	remoteToken, err = dts.Token()
	if err != nil {
		return nil, fmt.Errorf("failed to generate token: %w", err)
	}

	return remoteToken, nil
}

// getObjectContents will read the contents of an object in Google Storage
// named objectName, contained in the bucket "bucketName".
func getObjectContents(output io.Writer, bucketName string, objectName string) error {
	// bucketName := "foo"
	// prefix := "profile-picture-"

	ctx := context.Background()

	thisTokenSource := localTokenSource{
		ctx:        ctx,
		bucketName: bucketName,
		brokerURL:  "yourURL.com/internal/broker",
	}

	// Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing.
	refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource)
	// You can now use the token source to access Google Cloud Storage resources as follows.
	storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS))
	if err != nil {
		return fmt.Errorf("failed to create the storage client: %w", err)
	}
	defer storageClient.Close()
	bkt := storageClient.Bucket(bucketName)
	obj := bkt.Object(objectName)
	rc, err := obj.NewReader(ctx)
	if err != nil {
		return fmt.Errorf("failed to retrieve the object: %w", err)
	}
	defer rc.Close()
	data, err := io.ReadAll(rc)
	if err != nil {
		return fmt.Errorf("could not read the object's contents: %w", err)
	}
	// Data now contains the contents of the requested object.
	output.Write(data)
	return nil
}

Java

Java의 경우 com.google.auth:google-auth-library-oauth2-http 아티팩트 버전 1.1.0 이상을 사용하여 자동으로 토큰을 교환하고 새로고침할 수 있습니다.

아티팩트 버전을 확인하려면 애플리케이션 디렉터리에서 다음 Maven 명령어를 실행합니다.

mvn dependency:list -DincludeArtifactIds=google-auth-library-oauth2-http

다음 예시에서는 토큰 브로커가 권한이 축소된 토큰을 생성하는 방법을 보여줍니다.

public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
    throws IOException {
  // Retrieve the source credentials from ADC.
  GoogleCredentials sourceCredentials =
      GoogleCredentials.getApplicationDefault()
          .createScoped("https://www.googleapis.com/auth/cloud-platform");

  // Initialize the Credential Access Boundary rules.
  String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;

  // Downscoped credentials will have readonly access to the resource.
  String availablePermission = "inRole:roles/storage.objectViewer";

  // Only objects starting with the specified prefix string in the object name will be allowed
  // read access.
  String expression =
      "resource.name.startsWith('projects/_/buckets/"
          + bucketName
          + "/objects/"
          + objectPrefix
          + "')";

  // Build the AvailabilityCondition.
  CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
      CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
          .setExpression(expression)
          .build();

  // Define the single access boundary rule using the above properties.
  CredentialAccessBoundary.AccessBoundaryRule rule =
      CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
          .setAvailableResource(availableResource)
          .addAvailablePermission(availablePermission)
          .setAvailabilityCondition(availabilityCondition)
          .build();

  // Define the Credential Access Boundary with all the relevant rules.
  CredentialAccessBoundary credentialAccessBoundary =
      CredentialAccessBoundary.newBuilder().addRule(rule).build();

  // Create the downscoped credentials.
  DownscopedCredentials downscopedCredentials =
      DownscopedCredentials.newBuilder()
          .setSourceCredential(sourceCredentials)
          .setCredentialAccessBoundary(credentialAccessBoundary)
          .build();

  // Retrieve the token.
  // This will need to be passed to the Token Consumer.
  AccessToken accessToken = downscopedCredentials.refreshAccessToken();
  return accessToken;
}

다음 예시에서는 토큰 소비자가 새로고침 핸들러를 사용하여 권한이 축소된 토큰을 자동으로 가져오고 새로고침하는 방법을 보여줍니다.

public static void tokenConsumer(final String bucketName, final String objectName)
    throws IOException {
  // You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
  // library to seamlessly handle downscoped token refreshes on expiration.
  OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
      new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
        @Override
        public AccessToken refreshAccessToken() throws IOException {
          // The common pattern of usage is to have a token broker pass the downscoped short-lived
          // access tokens to a token consumer via some secure authenticated channel.
          // For illustration purposes, we are generating the downscoped token locally.
          // We want to test the ability to limit access to objects with a certain prefix string
          // in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
          // not required if access to all bucket resources are allowed. If access to limited
          // resources in the bucket is needed, this mechanism can be used.
          return getTokenFromBroker(bucketName, objectName.substring(0, 3));
        }
      };

  // Downscoped token retrieved from token broker.
  AccessToken downscopedToken = handler.refreshAccessToken();

  // Create the OAuth2CredentialsWithRefresh from the downscoped token and pass a refresh handler
  // which will handle token expiration.
  // This will allow the consumer to seamlessly obtain new downscoped tokens on demand every time
  // token expires.
  OAuth2CredentialsWithRefresh credentials =
      OAuth2CredentialsWithRefresh.newBuilder()
          .setAccessToken(downscopedToken)
          .setRefreshHandler(handler)
          .build();

  // Use the credentials with the Cloud Storage SDK.
  StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
  Storage storage = options.getService();

  // Call Cloud Storage APIs.
  Blob blob = storage.get(bucketName, objectName);
  String content = new String(blob.getContent());
  System.out.println(
      "Retrieved object, "
          + objectName
          + ", from bucket,"
          + bucketName
          + ", with content: "
          + content);
}

Node.js

Node.js의 경우 google-auth-library 패키지 버전 7.9.0 이상을 사용하여 토큰을 자동으로 교환하고 새로고침할 수 있습니다.

패키지 버전을 확인하려면 애플리케이션 디렉터리에서 다음 명령어를 실행합니다.

npm list google-auth-library

다음 예시에서는 토큰 브로커가 권한이 축소된 토큰을 생성하는 방법을 보여줍니다.

// Imports the Google Auth libraries.
const {GoogleAuth, DownscopedClient} = require('google-auth-library');
/**
 * Simulates token broker generating downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectPrefix The prefix string of the object name. This is used
 *        to ensure access is restricted to only objects starting with this
 *        prefix string.
 */
async function getTokenFromBroker(bucketName, objectPrefix) {
  const googleAuth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/cloud-platform',
  });

  // Define the Credential Access Boundary object.
  const cab = {
    // Define the access boundary.
    accessBoundary: {
      // Define the single access boundary rule.
      accessBoundaryRules: [
        {
          availableResource: `//storage.googleapis.com/projects/_/buckets/${bucketName}`,
          // Downscoped credentials will have readonly access to the resource.
          availablePermissions: ['inRole:roles/storage.objectViewer'],
          // Only objects starting with the specified prefix string in the object name
          // will be allowed read access.
          availabilityCondition: {
            expression:
              "resource.name.startsWith('projects/_/buckets/" +
              `${bucketName}/objects/${objectPrefix}')`,
          },
        },
      ],
    },
  };

  // Obtain an authenticated client via ADC.
  const client = await googleAuth.getClient();

  // Use the client to create a DownscopedClient.
  const cabClient = new DownscopedClient(client, cab);

  // Refresh the tokens.
  const refreshedAccessToken = await cabClient.getAccessToken();

  // This will need to be passed to the token consumer.
  return refreshedAccessToken;
}

다음 예시에서는 토큰 소비자가 권한이 축소된 토큰을 자동으로 가져오고 새로고침하는 새로고침 핸들러를 제공하는 방법을 보여줍니다.

// Imports the Google Auth and Google Cloud libraries.
const {OAuth2Client} = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');
/**
 * Simulates token consumer generating calling GCS APIs using generated
 * downscoped tokens for specified bucket.
 *
 * @param bucketName The name of the Cloud Storage bucket.
 * @param objectName The name of the object in the Cloud Storage bucket
 *        to read.
 */
async function tokenConsumer(bucketName, objectName) {
  // Create the OAuth credentials (the consumer).
  const oauth2Client = new OAuth2Client();
  // We are defining a refresh handler instead of a one-time access
  // token/expiry pair.
  // This will allow the consumer to obtain new downscoped tokens on
  // demand every time a token is expired, without any additional code
  // changes.
  oauth2Client.refreshHandler = async () => {
    // The common pattern of usage is to have a token broker pass the
    // downscoped short-lived access tokens to a token consumer via some
    // secure authenticated channel. For illustration purposes, we are
    // generating the downscoped token locally. We want to test the ability
    // to limit access to objects with a certain prefix string in the
    // resource bucket. objectName.substring(0, 3) is the prefix here. This
    // field is not required if access to all bucket resources are allowed.
    // If access to limited resources in the bucket is needed, this mechanism
    // can be used.
    const refreshedAccessToken = await getTokenFromBroker(
      bucketName,
      objectName.substring(0, 3)
    );
    return {
      access_token: refreshedAccessToken.token,
      expiry_date: refreshedAccessToken.expirationTime,
    };
  };

  const storageOptions = {
    projectId: process.env.GOOGLE_CLOUD_PROJECT,
    authClient: oauth2Client,
  };

  const storage = new Storage(storageOptions);
  const downloadFile = await storage
    .bucket(bucketName)
    .file(objectName)
    .download();
  console.log(downloadFile.toString('utf8'));
}

Python

Python의 경우 google-auth 패키지 버전 2.0.0 이상을 사용하여 토큰을 자동으로 교환하고 새로고침할 수 있습니다.

패키지 버전을 확인하려면 패키지가 설치된 환경에서 다음 명령어를 실행합니다.

pip show google-auth

다음 예시에서는 토큰 브로커가 권한이 축소된 토큰을 생성하는 방법을 보여줍니다.

import google.auth

from google.auth import downscoped
from google.auth.transport import requests

def get_token_from_broker(bucket_name, object_prefix):
    """Simulates token broker generating downscoped tokens for specified bucket.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_prefix (str): The prefix string of the object name. This is used
            to ensure access is restricted to only objects starting with this
            prefix string.

    Returns:
        Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
    """
    # Initialize the Credential Access Boundary rules.
    available_resource = f"//storage.googleapis.com/projects/_/buckets/{bucket_name}"
    # Downscoped credentials will have readonly access to the resource.
    available_permissions = ["inRole:roles/storage.objectViewer"]
    # Only objects starting with the specified prefix string in the object name
    # will be allowed read access.
    availability_expression = (
        "resource.name.startsWith('projects/_/buckets/{}/objects/{}')".format(
            bucket_name, object_prefix
        )
    )
    availability_condition = downscoped.AvailabilityCondition(availability_expression)
    # Define the single access boundary rule using the above properties.
    rule = downscoped.AccessBoundaryRule(
        available_resource=available_resource,
        available_permissions=available_permissions,
        availability_condition=availability_condition,
    )
    # Define the Credential Access Boundary with all the relevant rules.
    credential_access_boundary = downscoped.CredentialAccessBoundary(rules=[rule])

    # Retrieve the source credentials via ADC.
    source_credentials, _ = google.auth.default()
    if source_credentials.requires_scopes:
        source_credentials = source_credentials.with_scopes(
            ["https://www.googleapis.com/auth/cloud-platform"]
        )

    # Create the downscoped credentials.
    downscoped_credentials = downscoped.Credentials(
        source_credentials=source_credentials,
        credential_access_boundary=credential_access_boundary,
    )

    # Refresh the tokens.
    downscoped_credentials.refresh(requests.Request())

    # These values will need to be passed to the token consumer.
    access_token = downscoped_credentials.token
    expiry = downscoped_credentials.expiry
    return (access_token, expiry)

다음 예시에서는 토큰 소비자가 권한이 축소된 토큰을 자동으로 가져오고 새로고침하는 새로고침 핸들러를 제공하는 방법을 보여줍니다.

from google.cloud import storage
from google.oauth2 import credentials

def token_consumer(bucket_name, object_name):
    """Tests token consumer readonly access to the specified object.

    Args:
        bucket_name (str): The name of the Cloud Storage bucket.
        object_name (str): The name of the object in the Cloud Storage bucket
            to read.
    """

    # Create the OAuth credentials from the downscoped token and pass a
    # refresh handler to handle token expiration. We are passing a
    # refresh_handler instead of a one-time access token/expiry pair.
    # This will allow the consumer to obtain new downscoped tokens on
    # demand every time a token is expired, without any additional code
    # changes.
    def refresh_handler(request, scopes=None):
        # The common pattern of usage is to have a token broker pass the
        # downscoped short-lived access tokens to a token consumer via some
        # secure authenticated channel.
        # For illustration purposes, we are generating the downscoped token
        # locally.
        # We want to test the ability to limit access to objects with a certain
        # prefix string in the resource bucket. object_name[0:3] is the prefix
        # here. This field is not required if access to all bucket resources are
        # allowed. If access to limited resources in the bucket is needed, this
        # mechanism can be used.
        return get_token_from_broker(bucket_name, object_prefix=object_name[0:3])

    creds = credentials.Credentials(
        None,
        scopes=["https://www.googleapis.com/auth/cloud-platform"],
        refresh_handler=refresh_handler,
    )

    # Initialize a Cloud Storage client with the oauth2 credentials.
    storage_client = storage.Client(credentials=creds)
    # The token broker has readonly access to the specified bucket object.
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(object_name)
    print(blob.download_as_bytes().decode("utf-8"))

수동으로 액세스 토큰 교환 및 새로고침

토큰 브로커는 Security Token Service API를 사용하여 액세스 토큰을 권한이 축소된 액세스 토큰으로 교환할 수 있습니다. 그런 다음 권한이 축소된 토큰을 토큰 소비자에게 제공할 수 있습니다.

액세스 토큰을 교환하려면 다음 HTTP 메서드와 URL을 사용합니다.

POST https://sts.googleapis.com/v1/token

요청의 Content-Type 헤더를 application/x-www-form-urlencoded로 설정합니다. 요청 본문에 다음 필드를 포함합니다.

필드
grant_type

string

urn:ietf:params:oauth:grant-type:token-exchange 값을 사용합니다.

options

string

백분율 인코딩으로 인코딩된 JSON 형식의 사용자 인증 정보 액세스 경계입니다.

requested_token_type

string

urn:ietf:params:oauth:token-type:access_token 값을 사용합니다.

subject_token

string

교환하려는 OAuth 2.0 액세스 토큰입니다.

subject_token_type

string

urn:ietf:params:oauth:token-type:access_token 값을 사용합니다.

응답은 다음 필드를 포함하는 JSON 객체입니다.

필드
access_token

string

사용자 인증 정보 액세스 경계를 준수하는 권한이 축소된 OAuth 2.0 액세스 토큰입니다.

expires_in

number

권한이 축소된 토큰이 만료될 때까지의 시간(초)입니다.

이 필드는 원본 액세스 토큰이 서비스 계정을 나타내는 경우에만 포함됩니다. 이 필드가 포함되지 않으면 권한이 축소된 토큰은 원본 액세스 토큰과 동시에 만료됩니다.

issued_token_type

string

urn:ietf:params:oauth:token-type:access_token 값을 포함합니다.

token_type

string

Bearer 값을 포함합니다.

예를 들어 JSON 형식의 사용자 인증 정보 액세스 경계가 ./access-boundary.json 파일에 저장된 경우 다음 curl 명령어를 사용하여 액세스 토큰을 교환합니다. original-token을 원본 액세스 토큰과 바꿉니다.

curl -H "Content-Type:application/x-www-form-urlencoded" \
    -X POST \
    https://sts.googleapis.com/v1/token \
    -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token_type=urn:ietf:params:oauth:token-type:access_token&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=original-token" \
    --data-urlencode "options=$(cat ./access-boundary.json)"

응답은 다음 예시와 비슷합니다.

{
  "access_token": "ya29.dr.AbCDeFg-123456...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}

토큰 소비자가 권한이 축소된 토큰을 요청하면 토큰 브로커는 권한이 축소된 토큰과 만료될 때까지의 시간(초)으로 응답합니다. 토큰이 만료되면 서버에서 요청을 거부합니다. 권한이 축소된 토큰을 갱신하려면 소비자는 기존 토큰이 만료되기 전에 브로커에서 권한이 축소된 토큰을 요청하면 됩니다.

다음 단계