프로그래매틱 인증

이 페이지에서는 사용자 계정 또는 서비스 계정에서 Cloud IAP(Identity-Aware Proxy) 보안 리소스로 인증을 수행하는 방법을 설명합니다.

  • 사용자 계정은 개별 사용자에 속합니다. 애플리케이션이 사용자 대신 Cloud IAP 보안 리소스에 액세스해야 할 경우 사용자 계정을 인증합니다. 사용자 계정 사용자 인증 정보를 읽어보세요.
  • 서비스 계정은 개별 사용자 대신 애플리케이션에 속합니다. 애플리케이션이 Cloud IAP 보안 리소스에 액세스하도록 허용하려는 경우에 서비스 계정을 인증합니다. 서비스 계정 이해 방법을 알아보세요.

시작하기 전에

시작하기 전에 다음이 필요합니다.

  • 개발자 계정, 서비스 계정 또는 모바일 앱 사용자 인증 정보를 사용하여 프로그래매틱 방식으로 연결하려는 Cloud IAP 보안 애플리케이션

사용자 계정 인증

데스크톱 및 모바일 앱에서 개발자 앱에 대한 사용자 액세스를 사용 설정하여 프로그램이 Cloud IAP 보안 리소스와 상호 작용하도록 허용할 수 있습니다.

모바일 앱에서 인증

  1. Cloud IAP 보안 리소스와 동일한 프로젝트에서 모바일 앱을 위한 OAuth 2.0 클라이언트 ID를 만듭니다.
    1. 사용자 인증 정보 페이지로 이동합니다.
      사용자 인증 정보 페이지로 이동
    2. Cloud IAP 보안 리소스가 포함된 프로젝트를 선택합니다.
    3. 사용자 인증 정보 만들기를 클릭하고 OAuth 클라이언트 ID를 선택합니다.
    4. 사용자 인증 정보를 만들려는 애플리케이션 유형을 선택합니다.
    5. 필요에 따라 이름제한사항을 추가한 후 만들기를 클릭합니다.
  2. 표시된 OAuth 클라이언트 창에서 연결하려는 Cloud IAP 보안 리소스의 클라이언트 ID를 기록해둡니다.
  3. Cloud IAP 보안 클라이언트 ID에 대한 ID 토큰을 가져옵니다.
  4. Authorization: Bearer 헤더에 ID 토큰을 포함하여 Cloud IAP 보안 리소스에 대해 인증 요청을 수행합니다.

데스크톱 앱에서 인증

이 섹션에서는 데스크톱 명령줄에서 사용자 계정을 인증하는 방법을 설명합니다.

클라이언트 ID 설정

개발자가 명령줄에서 애플리케이션에 액세스하도록 허용하려면 먼저 기타 유형의 OAuth 클라이언트 ID 사용자 인증 정보를 만들어야 합니다.

  1. 사용자 인증 정보 페이지로 이동합니다.
    사용자 인증 정보 페이지로 이동
  2. Cloud IAP 보안 리소스가 포함된 프로젝트를 선택합니다.
  3. 사용자 인증 정보 만들기를 클릭하고 OAuth 클라이언트 ID를 선택합니다.
  4. 애플리케이션 유형 아래에서 기타를 선택하고 이름을 추가한 후 만들기를 클릭합니다.
  5. 표시된 OAuth 클라이언트 창에서 클라이언트 ID클라이언트 보안 비밀을 기록해둡니다. 사용자 인증 정보를 관리하거나 개발자들과 공유하기 위해 스크립트에서 이를 사용해야 합니다.
  6. 사용자 인증 정보 창에서 새로운 기타 사용자 인증 정보가 애플리케이션에 액세스하기 위해 사용되는 기본 클라이언트 ID와 함께 표시됩니다.

애플리케이션에 로그인

Cloud IAP 보안 앱에 액세스하려는 각 개발자는 먼저 로그인해야 합니다. Cloud SDK를 사용하는 것과 같이 프로세스를 스크립트로 패키지화할 수 있습니다. 다음은 curl을 사용하여 로그인하고 애플리케이션에 액세스하기 위해 사용할 수 있는 토큰을 만드는 예입니다.

  1. GCP 리소스에 액세스할 수 있는 계정으로 로그인합니다.
  2. 다음 URI로 이동합니다. 여기서 OTHER_CLIENT_ID는 위에서 만든 기타 클라이언트 ID입니다.

    https://accounts.google.com/o/oauth2/v2/auth?client_id=OTHER_CLIENT_ID&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob

  3. 표시된 창에서 위에서 만든 기타 클라이언트 ID 및 보안 비밀과 함께, 아래의 AUTH_CODE를 대체할 승인 코드를 기록해둡니다.

    curl --verbose \
          --data client_id=OTHER_CLIENT_ID \
          --data client_secret=OTHER_CLIENT_SECRET \
          --data code=AUTH_CODE \
          --data redirect_uri=urn:ietf:wg:oauth:2.0:oob \
          --data grant_type=authorization_code \
          https://www.googleapis.com/oauth2/v4/token

    이 코드는 애플리케이션에 액세스하기 위한 로그인 토큰으로 저장할 수 있는 refresh_token 필드가 포함된 JSON 객체를 반환합니다.

애플리케이션 액세스

애플리케이션에 액세스하기 위해서는 ID 토큰의 로그인 과정 중에 개발자가 생성한 refresh_token을 교환합니다. ID 토큰은 약 1시간 동안 유효하며, 이 시간 동안 특정 앱에 여러 번 요청을 수행할 수 있습니다. 다음은 curl을 사용하여 토큰을 사용하고 애플리케이션에 액세스하는 예입니다.

  1. 아래 코드를 사용합니다. 여기서 REFRESH_TOKEN은 로그인 과정의 토큰이고, IAP_CLIENT_ID는 애플리케이션에 액세스하기 위해 사용되는 기본 클라이언트 ID이고, OTHER_CLIENT_IDOTHER_CLIENT_SECRET은 위에서 클라이언트 ID를 설정할 때 만든 클라이언트 ID와 보안 비밀입니다.

    curl --verbose \
          --data client_id=OTHER_CLIENT_ID \
          --data client_secret=OTHER_CLIENT_SECRET \
          --data refresh_token=REFRESH_TOKEN \
          --data grant_type=refresh_token \
          --data audience=IAP_CLIENT_ID \
          https://www.googleapis.com/oauth2/v4/token

    이 코드는 앱에 액세스하기 위해 사용할 수 있는 id_token 필드가 포함된 JSON 객체를 반환합니다.

  2. 앱에 액세스하기 위해 다음과 같이 id_token을 사용합니다.

    curl --verbose --header 'Authorization: Bearer ID_TOKEN' URL

서비스 계정에서 인증

OIDC(OpenID Connect) 토큰을 사용하여 Cloud IAP 보안 리소스에 대해 서비스 계정을 인증합니다.

  1. Cloud IAP 보안 프로젝트의 액세스 목록에 서비스 계정을 추가합니다.
  2. JWT 기반 액세스 토큰을 생성합니다. 이 토큰은 클라이언트 ID가 필요한 target_audience 추가 클레임을 사용합니다. 클라이언트 ID를 찾으려면 아래 단계를 수행하세요.

    1. Cloud IAP 페이지로 이동합니다.
    2. 액세스하려는 리소스를 찾은 후 더보기 > OAuth 클라이언트 수정을 클릭합니다.
      더보기 메뉴에서 OAuth 클라이언트 편집

    3. 표시된 사용자 인증 정보 페이지에서 클라이언트 ID를 기록해둡니다.

  3. Cloud IAP 보안 클라이언트 ID에 대해 OIDC 토큰을 요청합니다.

  4. Authorization: Bearer 헤더에 OIDC 토큰을 포함하여 Cloud IAP 보안 리소스에 대해 인증된 요청을 수행합니다.

다음 샘플 코드는 Cloud IAP 보안 리소스에 대해 기본 서비스 계정을 인증합니다.

C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Json;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace GoogleCloudSamples
{
    class IAPClient
    {
        /// <summary>
        /// Authenticates using the client id and credentials, then fetches
        /// the uri.
        /// </summary>
        /// <param name="iapClientId">The client id observed on
        /// https://console.cloud.google.com/apis/credentials.</param>
        /// <param name="credentialsFilePath">Path to the credentials .json file
        /// download from https://console.cloud.google.com/apis/credentials.
        /// </param>
        /// <param name="uri">HTTP uri to fetch.</param>
        /// <returns>The http response body as a string.</returns>
        public static string InvokeRequest(string iapClientId,
            string credentialsFilePath, string uri)
        {
            // Read credentials from the credentials .json file.
            ServiceAccountCredential saCredential;
            using (var fs = new FileStream(credentialsFilePath,
                FileMode.Open, FileAccess.Read))
            {
                saCredential = ServiceAccountCredential
                    .FromServiceAccountData(fs);
            }

            // Generate a JWT signed with the service account's private key
            // containing a special "target_audience" claim.
            var jwtBasedAccessToken =
                CreateAccessToken(saCredential, iapClientId);

            // Request an OIDC token for the Cloud IAP-secured client ID.
            var req = new GoogleAssertionTokenRequest()
            {
                Assertion = jwtBasedAccessToken
            };
            var result = req.ExecuteAsync(saCredential.HttpClient,
                saCredential.TokenServerUrl, CancellationToken.None,
                saCredential.Clock).Result;
            string token = result.IdToken;

            // Include the OIDC token in an Authorization: Bearer header to
            // IAP-secured resource
            var httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("Bearer", token);
            string response = httpClient.GetStringAsync(uri).Result;
            return response;
        }

        /// <summary>
        /// Generate a JWT signed with the service account's private key
        /// containing a special "target_audience" claim.
        /// </summary>
        /// <param name="privateKey">The private key string pulled from
        /// a credentials .json file.</param>
        /// <param name="iapClientId">The client id observed on
        /// https://console.cloud.google.com/apis/credentials.</param>
        /// <param name="email">The e-mail address associated with the
        /// privateKey.</param>
        /// <returns>An access token.</returns>
        static string CreateAccessToken(ServiceAccountCredential saCredential,
            string iapClientId)
        {
            var now = saCredential.Clock.UtcNow;
            var currentTime = ToUnixEpochDate(now);
            var expTime = ToUnixEpochDate(now.AddHours(1));

            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Aud,
                    GoogleAuthConsts.OidcTokenUrl),
                new Claim(JwtRegisteredClaimNames.Sub, saCredential.Id),
                new Claim(JwtRegisteredClaimNames.Iat, currentTime.ToString()),
                new Claim(JwtRegisteredClaimNames.Exp, expTime.ToString()),
                new Claim(JwtRegisteredClaimNames.Iss, saCredential.Id),

                // We need to generate a JWT signed with the service account's
                // private key containing a special "target_audience" claim.
                // That claim should contain the clientId of IAP we eventually
                // want to access.
                new Claim("target_audience", iapClientId)
            };

            // Encryption algorithm must be RSA SHA-256, according to
            // https://developers.google.com/identity/protocols/OAuth2ServiceAccount
            var signingCredentials = new SigningCredentials(
                new RsaSecurityKey(saCredential.Key),
                SecurityAlgorithms.RsaSha256);
            var token = new JwtSecurityToken(
                claims: claims,
                signingCredentials: signingCredentials);
            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        static long ToUnixEpochDate(DateTime date)
              => (long)Math.Round((date.ToUniversalTime() -
                                   new DateTimeOffset(1970, 1, 1, 0, 0, 0,
                                        TimeSpan.Zero)).TotalSeconds);
    }
}

Python

import google.auth
import google.auth.app_engine
import google.auth.compute_engine.credentials
import google.auth.iam
from google.auth.transport.requests import Request
import google.oauth2.credentials
import google.oauth2.service_account
import requests
import requests_toolbelt.adapters.appengine

IAM_SCOPE = 'https://www.googleapis.com/auth/iam'
OAUTH_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'

def make_iap_request(url, client_id, method='GET', **kwargs):
    """Makes a request to an application protected by Identity-Aware Proxy.

    Args:
      url: The Identity-Aware Proxy-protected URL to fetch.
      client_id: The client ID used by Identity-Aware Proxy.
      method: The request method to use
              ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
      **kwargs: Any of the parameters defined for the request function:
                https://github.com/requests/requests/blob/master/requests/api.py
                If no timeout is provided, it is set to 90 by default.

    Returns:
      The page body, or raises an exception if the page couldn't be retrieved.
    """
    # Set the default timeout, if missing
    if 'timeout' not in kwargs:
        kwargs['timeout'] = 90

    # Figure out what environment we're running in and get some preliminary
    # information about the service account.
    bootstrap_credentials, _ = google.auth.default(
        scopes=[IAM_SCOPE])
    if isinstance(bootstrap_credentials,
                  google.oauth2.credentials.Credentials):
        raise Exception('make_iap_request is only supported for service '
                        'accounts.')
    elif isinstance(bootstrap_credentials,
                    google.auth.app_engine.Credentials):
        requests_toolbelt.adapters.appengine.monkeypatch()

    # For service account's using the Compute Engine metadata service,
    # service_account_email isn't available until refresh is called.
    bootstrap_credentials.refresh(Request())

    signer_email = bootstrap_credentials.service_account_email
    if isinstance(bootstrap_credentials,
                  google.auth.compute_engine.credentials.Credentials):
        # Since the Compute Engine metadata service doesn't expose the service
        # account key, we use the IAM signBlob API to sign instead.
        # In order for this to work:
        #
        # 1. Your VM needs the https://www.googleapis.com/auth/iam scope.
        #    You can specify this specific scope when creating a VM
        #    through the API or gcloud. When using Cloud Console,
        #    you'll need to specify the "full access to all Cloud APIs"
        #    scope. A VM's scopes can only be specified at creation time.
        #
        # 2. The VM's default service account needs the "Service Account Actor"
        #    role. This can be found under the "Project" category in Cloud
        #    Console, or roles/iam.serviceAccountActor in gcloud.
        signer = google.auth.iam.Signer(
            Request(), bootstrap_credentials, signer_email)
    else:
        # A Signer object can sign a JWT using the service account's key.
        signer = bootstrap_credentials.signer

    # Construct OAuth 2.0 service account credentials using the signer
    # and email acquired from the bootstrap credentials.
    service_account_credentials = google.oauth2.service_account.Credentials(
        signer, signer_email, token_uri=OAUTH_TOKEN_URI, additional_claims={
            'target_audience': client_id
        })

    # service_account_credentials gives us a JWT signed by the service
    # account. Next, we use that to obtain an OpenID Connect token,
    # which is a JWT signed by Google.
    google_open_id_connect_token = get_google_open_id_connect_token(
        service_account_credentials)

    # Fetch the Identity-Aware Proxy-protected URL, including an
    # Authorization header containing "Bearer " followed by a
    # Google-issued OpenID Connect token for the service account.
    resp = requests.request(
        method, url,
        headers={'Authorization': 'Bearer {}'.format(
            google_open_id_connect_token)}, **kwargs)
    if resp.status_code == 403:
        raise Exception('Service account {} does not have permission to '
                        'access the IAP-protected application.'.format(
                            signer_email))
    elif resp.status_code != 200:
        raise Exception(
            'Bad response from application: {!r} / {!r} / {!r}'.format(
                resp.status_code, resp.headers, resp.text))
    else:
        return resp.text

def get_google_open_id_connect_token(service_account_credentials):
    """Get an OpenID Connect token issued by Google for the service account.

    This function:

      1. Generates a JWT signed with the service account's private key
         containing a special "target_audience" claim.

      2. Sends it to the OAUTH_TOKEN_URI endpoint. Because the JWT in #1
         has a target_audience claim, that endpoint will respond with
         an OpenID Connect token for the service account -- in other words,
         a JWT signed by *Google*. The aud claim in this JWT will be
         set to the value from the target_audience claim in #1.

    For more information, see
    https://developers.google.com/identity/protocols/OAuth2ServiceAccount .
    The HTTP/REST example on that page describes the JWT structure and
    demonstrates how to call the token endpoint. (The example on that page
    shows how to get an OAuth2 access token; this code is using a
    modified version of it to get an OpenID Connect token.)
    """

    service_account_jwt = (
        service_account_credentials._make_authorization_grant_assertion())
    request = google.auth.transport.requests.Request()
    body = {
        'assertion': service_account_jwt,
        'grant_type': google.oauth2._client._JWT_GRANT_TYPE,
    }
    token_response = google.oauth2._client._token_endpoint_request(
        request, OAUTH_TOKEN_URI, body)
    return token_response['id_token']

자바

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.GenericData;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;

public class BuildIapRequest {
  private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam";
  private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
  private static final String JWT_BEARER_TOKEN_GRANT_TYPE =
      "urn:ietf:params:oauth:grant-type:jwt-bearer";
  private static final long EXPIRATION_TIME_IN_SECONDS = 3600L;

  private static final HttpTransport httpTransport = new NetHttpTransport();

  private static Clock clock = Clock.systemUTC();

  private BuildIapRequest() {}

  private static ServiceAccountCredentials getCredentials() throws Exception {
    GoogleCredentials credentials =
        GoogleCredentials.getApplicationDefault().createScoped(Collections.singleton(IAM_SCOPE));
    // service account credentials are required to sign the jwt token
    if (credentials == null || !(credentials instanceof ServiceAccountCredentials)) {
      throw new Exception("Google credentials : service accounts credentials expected");
    }
    return (ServiceAccountCredentials) credentials;
  }

  private static String getSignedJwt(ServiceAccountCredentials credentials, String iapClientId)
      throws Exception {
    Instant now = Instant.now(clock);
    long expirationTime = now.getEpochSecond() + EXPIRATION_TIME_IN_SECONDS;

    // generate jwt signed by service account
    // header must contain algorithm ("alg") and key ID ("kid")
    JWSHeader jwsHeader =
        new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(credentials.getPrivateKeyId()).build();

    // set required claims
    JWTClaimsSet claims =
        new JWTClaimsSet.Builder()
            .audience(OAUTH_TOKEN_URI)
            .issuer(credentials.getClientEmail())
            .subject(credentials.getClientEmail())
            .issueTime(Date.from(now))
            .expirationTime(Date.from(Instant.ofEpochSecond(expirationTime)))
            .claim("target_audience", iapClientId)
            .build();

    // sign using service account private key
    JWSSigner signer = new RSASSASigner(credentials.getPrivateKey());
    SignedJWT signedJwt = new SignedJWT(jwsHeader, claims);
    signedJwt.sign(signer);

    return signedJwt.serialize();
  }

  private static String getGoogleIdToken(String jwt) throws Exception {
    final GenericData tokenRequest =
        new GenericData().set("grant_type", JWT_BEARER_TOKEN_GRANT_TYPE).set("assertion", jwt);
    final UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

    final HttpRequestFactory requestFactory = httpTransport.createRequestFactory();

    final HttpRequest request =
        requestFactory
            .buildPostRequest(new GenericUrl(OAUTH_TOKEN_URI), content)
            .setParser(new JsonObjectParser(JacksonFactory.getDefaultInstance()));

    HttpResponse response;
    String idToken = null;
    response = request.execute();
    GenericData responseData = response.parseAs(GenericData.class);
    idToken = (String) responseData.get("id_token");
    return idToken;
  }

  /**
   * Clone request and add an IAP Bearer Authorization header with signed JWT token.
   *
   * @param request Request to add authorization header
   * @param iapClientId OAuth 2.0 client ID for IAP protected resource
   * @return Clone of request with Bearer style authorization header with signed jwt token.
   * @throws Exception exception creating signed JWT
   */
  public static HttpRequest buildIapRequest(HttpRequest request, String iapClientId)
      throws Exception {
    // get service account credentials
    ServiceAccountCredentials credentials = getCredentials();
    // get the base url of the request URL
    String jwt = getSignedJwt(credentials, iapClientId);
    if (jwt == null) {
      throw new Exception(
          "Unable to create a signed jwt token for : "
              + iapClientId
              + "with issuer : "
              + credentials.getClientEmail());
    }

    String idToken = getGoogleIdToken(jwt);
    if (idToken == null) {
      throw new Exception("Unable to retrieve open id token");
    }

    // Create an authorization header with bearer token
    HttpHeaders httpHeaders = request.getHeaders().clone().setAuthorization("Bearer " + idToken);

    // create request with jwt authorization header
    return httpTransport
        .createRequestFactory()
        .buildRequest(request.getRequestMethod(), request.getUrl(), request.getContent())
        .setHeaders(httpHeaders);
  }
}

PHP

namespace Google\Cloud\Samples\Iap;

# Imports Auth libraries and Guzzle HTTP libraries.
use Google\Auth\OAuth2;
use Google\Auth\Middleware\ScopedAccessTokenMiddleware;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

/**
 * Make a request to an application protected by Identity-Aware Proxy.
 *
 * @param string $url The Identity-Aware Proxy-protected URL to fetch.
 * @param string $clientId The client ID used by Identity-Aware Proxy.
 *
 * @return The response body.
 */
function make_iap_request($url, $clientId, $pathToServiceAccount)
{
    $serviceAccountKey = json_decode(file_get_contents($pathToServiceAccount), true);
    $oauth_token_uri = 'https://www.googleapis.com/oauth2/v4/token';
    $iam_scope = 'https://www.googleapis.com/auth/iam';

    # Create an OAuth object using the service account key
    $oauth = new OAuth2([
        'audience' => $oauth_token_uri,
        'issuer' => $serviceAccountKey['client_email'],
        'signingAlgorithm' => 'RS256',
        'signingKey' => $serviceAccountKey['private_key'],
        'tokenCredentialUri' => $oauth_token_uri,
    ]);
    $oauth->setGrantType(OAuth2::JWT_URN);
    $oauth->setAdditionalClaims(['target_audience' => $clientId]);

    # Obtain an OpenID Connect token, which is a JWT signed by Google.
    $token = $oauth->fetchAuthToken();
    $idToken = $oauth->getIdToken();

    # Construct a ScopedAccessTokenMiddleware with the ID token.
    $middleware = new ScopedAccessTokenMiddleware(
        function () use ($idToken) {
            return $idToken;
        },
        $iam_scope
    );

    $stack = HandlerStack::create();
    $stack->push($middleware);

    # Create an HTTP Client using Guzzle and pass in the credentials.
    $http_client = new Client([
        'handler' => $stack,
        'base_uri' => $url,
        'auth' => 'scoped'
    ]);

    # Make an authenticated HTTP Request
    $response = $http_client->request('GET', '/', []);
    return $response;
}

다음 단계

이 페이지가 도움이 되었나요? 평가를 부탁드립니다.

다음에 대한 의견 보내기...

Identity-Aware Proxy 문서