ユーザーのログインを行う

これは Classroom アドオンに関する 2 番目のチュートリアルです 説明します。

このチュートリアルでは、Google ログインをウェブ アプリケーションに追加します。これは Classroom アドオンで必要な動作について説明しました。認証情報を使用する API への今後のすべての呼び出しでこの承認フローを管理できます。

このチュートリアルでは、次のことを行います。

  • セッション データを iframe 内に維持するようにウェブアプリを構成します。
  • Google OAuth 2.0 サーバー間ログインフローを実装する。
  • OAuth 2.0 API の呼び出しを発行します。
  • 承認、ログアウト、テストをサポートする追加のルートを作成する API 呼び出し。

完了したら、ウェブアプリでユーザーを完全に認証して、 Google API

承認フローについて

Google API は認証と認可に OAuth 2.0 プロトコルを使用します。 Google の OAuth 実装について詳しくは、 Google Identity OAuth ガイド

アプリケーションの認証情報は Google Cloud で管理されます。これらが 認証、認可するための 4 ステップのプロセスを実装し、 user:

  1. 承認をリクエストします。このリクエストの一部としてコールバック URL を指定します。 完了すると、認証 URL が表示されます。
  2. ユーザーを認証 URL にリダイレクトします。表示されるページで アプリが必要とする権限をユーザーに付与し、アクセスを許可するよう求めるプロンプトを表示します。 完了すると、ユーザーはコールバック URL に転送されます。
  3. コールバック ルートで認証コードを受け取ります。交換する アクセス トークン更新トークンの認証コード。
  4. トークンを使用して Google API を呼び出します。
で確認できます。

OAuth 2.0 認証情報を取得する

OAuth 認証情報が作成、ダウンロード済みであることを確認します。 概要ページをご覧ください。プロジェクトでこれらの認証情報を使用してユーザーのログインを行う必要があります。

認可フローを実装する

ウェブアプリにロジックとルートを追加して、記述されているフローを実現します。例: 次の特長があります。

  • ランディング ページが表示されたら、承認フローを開始します。
  • 認可をリクエストし、認可サーバーのレスポンスを処理します。
  • 保存されている認証情報を消去します。
  • アプリの権限を取り消します。
  • API 呼び出しをテストする。
で確認できます。

承認を開始する

必要に応じて、ランディング ページを変更して承認フローを開始します。「 アドオンには次の 2 つの状態があります。保存されているトークンがあるか、 OAuth 2.0 サーバーからトークンを取得する必要がある場合です。パフォーマンス セッション内にトークンがある場合はテスト用の API 呼び出し。それ以外の場合はユーザーにメッセージを表示 ログインします。

Python

routes.py ファイルを開きます。 まず、いくつかの定数と Cookie を設定します。 iframe のセキュリティに関する推奨事項に沿って設定されていること。

# The file that contains the OAuth 2.0 client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scopes to request.
# These scopes must match the scopes in your Google Cloud project's OAuth Consent
# Screen: https://console.cloud.google.com/apis/credentials/consent
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/classroom.addons.teacher",
    "https://www.googleapis.com/auth/classroom.addons.student"
]

# Flask cookie configurations.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="None",
)

アドオンのランディング ルートに移動します(この例では /classroom-addon)。 。セッションに含まれない場合にログインページをレンダリングするロジックを追加する 「Credentials」を] キーを押します。

@app.route("/classroom-addon")
def classroom_addon():
    if "credentials" not in flask.session:
        return flask.render_template("authorization.html")

    return flask.render_template(
        "addon-discovery.html",
        message="You've reached the addon discovery page.")

Java

このチュートリアルのコードは、step_02_sign_in モジュールにあります。

application.properties ファイルを開き、セッション構成を追加します。 iframe のセキュリティに関する推奨事項に準拠している。

# iFrame security recommendations call for cookies to have the HttpOnly and
# secure attribute set
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

# Ensures that the session is maintained across the iframe and sign-in pop-up.
server.servlet.session.cookie.same-site=none

サービスクラスを作成する(step_02_sign_in モジュールの AuthService.java) コントローラ ファイル内のエンドポイントの背後にあるロジックを処理し、 リダイレクト URI、クライアント シークレット ファイルの場所、アドオンのスコープ 必要があります。リダイレクト URI は、ユーザーを特定の URI に再ルーティングするために使用します。 ユーザーがアプリを認可した後です詳しくは、 ソースコード内の README.md: client_secret.json ファイル。

@Service
public class AuthService {
    private static final String REDIRECT_URI = "https://localhost:5000/callback";
    private static final String CLIENT_SECRET_FILE = "client_secret.json";
    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
    private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    private static final String[] REQUIRED_SCOPES = {
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/classroom.addons.teacher",
        "https://www.googleapis.com/auth/classroom.addons.student"
    };

    /** Creates and returns a Collection object with all requested scopes.
    *   @return Collection of scopes requested by the application.
    */
    public static Collection<String> getScopes() {
        return new ArrayList<>(Arrays.asList(REQUIRED_SCOPES));
    }
}

コントローラ ファイル(step_02_sign_inAuthController.java)を開きます。 モジュールなど)を指定し、ランディング ルートにロジックを追加して、 セッションに credentials キーが含まれていないことを確認します。

@GetMapping(value = {"/start-auth-flow"})
public String startAuthFlow(Model model) {
    try {
        return "authorization";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

@GetMapping(value = {"/addon-discovery"})
public String addon_discovery(HttpSession session, Model model) {
    try {
        if (session == null || session.getAttribute("credentials") == null) {
            return startAuthFlow(model);
        }
        return "addon-discovery";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

認証ページには、ユーザーが「署名」するためのリンクまたはボタンが必要です 表示されます。これをクリックすると、ユーザーが authorize ルートにリダイレクトされます。

承認をリクエストする

承認をリクエストするには、ユーザーを認証ページにリダイレクトします。 URL。この URL には、スコープや Cloud Storage、 リクエストされたルート、認可の後の宛先ルート、ウェブアプリの できます。これらはこちらのサンプル URL で確認できます。

Python

次のインポートを routes.py ファイルに追加します。

import google_auth_oauthlib.flow

新しいルート /authorize を作成します。インスタンスを google_auth_oauthlib.flow.Flow;付属のドキュメントを使って from_client_secrets_file メソッドを使用します。

@app.route("/authorize")
def authorize():
    # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)

flowredirect_uri を設定します。これはユーザーが想定するルートです。 アプリの承認後に戻る必要があります。次のバージョンでは /callback です。 例です。

# The URI created here must exactly match one of the authorized redirect
# URIs for the OAuth 2.0 client, which you configured in the API Console. If
# this value doesn't match an authorized URI, you will get a
# "redirect_uri_mismatch" error.
flow.redirect_uri = flask.url_for("callback", _external=True)

フロー オブジェクトを使用して authorization_urlstate を作成します。保存 セッション内の state真正性を検証するために使用され、 サーバーが応答します。最後に、ユーザーを authorization_url

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type="offline",
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes="true")

# Store the state so the callback can verify the auth server response.
flask.session["state"] = state

# Redirect the user to the OAuth authorization URL.
return flask.redirect(authorization_url)

Java

次のメソッドを AuthService.java ファイルに追加して、 使用して認証 URL を取得します。

  • getClientSecrets() メソッドはクライアント シークレット ファイルを読み取り、構成します。 GoogleClientSecrets オブジェクト。
  • getFlow() メソッドは GoogleAuthorizationCodeFlow のインスタンスを作成します。
  • authorize() メソッドは GoogleAuthorizationCodeFlow オブジェクト( state パラメータ、リダイレクト URI を指定して、認証 URL を取得します。 state パラメータは、レスポンスの信頼性を検証するために使用されます。 認可サーバーから取得できます。このメソッドは、 認証 URL と state パラメータ。
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

コンストラクタ インジェクションを使用して、 コントローラ クラスを使用します。

/** Declare AuthService to be used in the Controller class constructor. */
private final AuthService authService;

/** AuthController constructor. Uses constructor injection to instantiate
*   the AuthService and UserRepository classes.
*   @param authService the service class that handles the implementation logic
*   of requests.
*/
public AuthController(AuthService authService) {
    this.authService = authService;
}

/authorize エンドポイントをコントローラ クラスに追加します。このエンドポイントは state パラメータを取得する AuthService authorize() メソッド 認可 URL が含まれます。その後、エンドポイントは state を保存します。 パラメータを追加し、ユーザーを認証 URL にリダイレクトします。

/** Redirects the sign-in pop-up to the authorization URL.
*   @param response the current response to pass information to.
*   @param session the current session.
*   @throws Exception if redirection to the authorization URL is unsuccessful.
*/
@GetMapping(value = {"/authorize"})
public void authorize(HttpServletResponse response, HttpSession session)
    throws Exception {
    try {
        HashMap authDataMap = authService.authorize();
        String authUrl = authDataMap.get("url").toString();
        String state = authDataMap.get("state").toString();
        session.setAttribute("state", state);
        response.sendRedirect(authUrl);
    } catch (Exception e) {
        throw e;
    }
}

サーバー レスポンスを処理する

承認後、ユーザーはルートからredirect_uriルートに戻ります 確認できます。上記の例では、このルートは /callback です。

ユーザーがcode 表示されます。次に、コードをアクセス トークンと更新トークンと交換します。

Python

Flask サーバー ファイルに次のインポートを追加します。

import google.oauth2.credentials
import googleapiclient.discovery

ルートをサーバーに追加します。別のインスタンスを google_auth_oauthlib.flow.Flow ですが、今回は 確認できます。

@app.route("/callback")
def callback():
    state = flask.session["state"]

    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    flow.redirect_uri = flask.url_for("callback", _external=True)

次に、アクセス トークンと更新トークンをリクエストします。幸いなことに、flow オブジェクトは これを実現する fetch_token メソッドが含まれています。このメソッドでは、 code 引数か authorization_response 引数のいずれかを指定できます。こちらの authorization_response。これはリクエストの完全な URL であるためです。

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

これで認証情報は完成です。それらをセッションに保存し、 他の方法またはルートで取得してから、アドオンにリダイレクトできる ランディングページに誘導できます

credentials = flow.credentials
flask.session["credentials"] = {
    "token": credentials.token,
    "refresh_token": credentials.refresh_token,
    "token_uri": credentials.token_uri,
    "client_id": credentials.client_id,
    "client_secret": credentials.client_secret,
    "scopes": credentials.scopes
}

# Close the pop-up by rendering an HTML page with a script that redirects
# the owner and closes itself. This can be done with a bit of JavaScript:
# <script>
#     window.opener.location.href = "{{ url_for('classroom_addon') }}";
#     window.close();
# </script>
return flask.render_template("close-me.html")

Java

Credentials オブジェクトを返すメソッドをサービスクラスに追加します。 アプリケーションによって実行されたリダイレクトから取得された認証コードを渡します。 指定します。この Credentials オブジェクトは、後で取得するために使用されます。 更新トークンが含まれています。

/** Returns the required credentials to access Google APIs.
*   @param authorizationCode the authorization code provided by the
*   authorization URL that's used to obtain credentials.
*   @return the credentials that were retrieved from the authorization flow.
*   @throws Exception if retrieving credentials is unsuccessful.
*/
public Credential getAndSaveCredentials(String authorizationCode) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        GoogleClientSecrets googleClientSecrets = getClientSecrets();
        TokenResponse tokenResponse = flow.newTokenRequest(authorizationCode)
            .setClientAuthentication(new ClientParametersAuthentication(
                googleClientSecrets.getWeb().getClientId(),
                googleClientSecrets.getWeb().getClientSecret()))
            .setRedirectUri(REDIRECT_URI)
            .execute();
        Credential credential = flow.createAndStoreCredential(tokenResponse, null);
        return credential;
    } catch (Exception e) {
        throw e;
    }
}

リダイレクト URI のエンドポイントをコントローラに追加します。取得 リクエストの認証コードと state パラメータ。比較 セッションに格納される state 属性に state パラメータ。もし 認証フローに進みます。一致しない場合、 エラーが返されます。

次に、AuthService getAndSaveCredentials メソッドを呼び出して、 パラメータとして指定します。Credentials の取得後 セッションに格納します次に、ダイアログを閉じて、 ユーザーをアドオンのランディングページに 誘導します

/** Handles the redirect URL to grant the application access to the user's
*   account.
*   @param request the current request used to obtain the authorization code
*   and state parameter from.
*   @param session the current session.
*   @param response the current response to pass information to.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the close-pop-up template if authorization is successful, or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/callback"})
public String callback(HttpServletRequest request, HttpSession session,
    HttpServletResponse response, Model model) {
    try {
        String authCode = request.getParameter("code");
        String requestState = request.getParameter("state");
        String sessionState = session.getAttribute("state").toString();
        if (!requestState.equals(sessionState)) {
            response.setStatus(401);
            return onError("Invalid state parameter.", model);
        }
        Credential credentials = authService.getAndSaveCredentials(authCode);
        session.setAttribute("credentials", credentials);
        return "close-pop-up";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

API 呼び出しをテストする

フローが完了すると、Google API の呼び出しを発行できるようになります。

たとえば、ユーザーのプロフィール情報をリクエストします。こちらから ユーザーの情報を取得します。

Python

次のドキュメントをご覧ください: OAuth 2.0 Discovery API これを使用して、入力された UserInfo オブジェクトを取得します。

# Retrieve the credentials from the session data and construct a
# Credentials instance.
credentials = google.oauth2.credentials.Credentials(
    **flask.session["credentials"])

# Construct the OAuth 2.0 v2 discovery API library.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

# Request and store the username in the session.
# This allows it to be used in other methods or in an HTML template.
flask.session["username"] = (
    user_info_service.userinfo().get().execute().get("name"))

Java

次のコマンドを使用して、UserInfo オブジェクトを構築するメソッドをサービスクラスに作成します。 パラメータとして Credentials を指定します。

/** Obtains the Userinfo object by passing in the required credentials.
*   @param credentials retrieved from the authorization flow.
*   @return the Userinfo object for the currently signed-in user.
*   @throws IOException if creating UserInfo service or obtaining the
*   Userinfo object is unsuccessful.
*/
public Userinfo getUserInfo(Credential credentials) throws IOException {
    try {
        Oauth2 userInfoService = new Oauth2.Builder(
            new NetHttpTransport(),
            new GsonFactory(),
            credentials).build();
        Userinfo userinfo = userInfoService.userinfo().get().execute();
        return userinfo;
    } catch (Exception e) {
        throw e;
    }
}

ユーザーのメールアドレスを表示するコントローラに /test エンドポイントを追加します。

/** Returns the test request page with the user's email.
*   @param session the current session.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the test page that displays the current user's email or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/test"})
public String test(HttpSession session, Model model) {
    try {
        Credential credentials = (Credential) session.getAttribute("credentials");
        Userinfo userInfo = authService.getUserInfo(credentials);
        String userInfoEmail = userInfo.getEmail();
        if (userInfoEmail != null) {
            model.addAttribute("userEmail", userInfoEmail);
        } else {
            return onError("Could not get user email.", model);
        }
        return "test";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

認証ストレージの消去

「消去」できます。現在のセッションからユーザーの認証情報を削除します。 これにより、アドオンのランディング ページでルーティングをテストできます。

ユーザーが以前にログアウトしたことを示す表示をおすすめします。 アドオンのランディングページにリダイレクトしますアプリは 承認フローで認証情報を取得できますが、ユーザーは認証情報を アプリを再承認する必要があります。

Python

@app.route("/clear")
def clear_credentials():
    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    return flask.render_template("signed-out.html")

代わりに flask.session.clear() を使用しますが、これは意図していない可能性があります セッションに格納されている他の値がある場合、

Java

コントローラに、/clear エンドポイントを追加します。

/** Clears the credentials in the session and returns the sign-out
*   confirmation page.
*   @param session the current session.
*   @return the sign-out confirmation page.
*/
@GetMapping(value = {"/clear"})
public String clear(HttpSession session) {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            session.removeAttribute("credentials");
        }
        return "sign-out";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

アプリの権限を取り消す

ユーザーは、POST リクエストを送信してアプリの権限を取り消すことができます。 https://oauth2.googleapis.com/revoke。リクエストには、ユーザーの できます。

Python

import requests

@app.route("/revoke")
def revoke():
    if "credentials" not in flask.session:
        return flask.render_template("addon-discovery.html",
                            message="You need to authorize before " +
                            "attempting to revoke credentials.")

    credentials = google.oauth2.credentials.Credentials(
        **flask.session["credentials"])

    revoke = requests.post(
        "https://oauth2.googleapis.com/revoke",
        params={"token": credentials.token},
        headers={"content-type": "application/x-www-form-urlencoded"})

    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    status_code = getattr(revoke, "status_code")
    if status_code == 200:
        return flask.render_template("authorization.html")
    else:
        return flask.render_template(
            "index.html", message="An error occurred during revocation!")

Java

取り消しエンドポイントを呼び出すメソッドをサービスクラスに追加します。

/** Revokes the app's permissions to the user's account.
*   @param credentials retrieved from the authorization flow.
*   @return response entity returned from the HTTP call to obtain response
*   information.
*   @throws RestClientException if the POST request to the revoke endpoint is
*   unsuccessful.
*/
public ResponseEntity<String> revokeCredentials(Credential credentials) throws RestClientException {
    try {
        String accessToken = credentials.getAccessToken();
        String url = "https://oauth2.googleapis.com/revoke?token=" + accessToken;

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        HttpEntity<Object> httpEntity = new HttpEntity<Object>(httpHeaders);
        ResponseEntity<String> responseEntity = new RestTemplate().exchange(
            url,
            HttpMethod.POST,
            httpEntity,
            String.class);
        return responseEntity;
    } catch (RestClientException e) {
        throw e;
    }
}

セッションをクリアするエンドポイント /revoke をコントローラに追加します。 取り消しが取り消された場合は、ユーザーを認証ページにリダイレクトする 成功です。

/** Revokes the app's permissions and returns the authorization page.
*   @param session the current session.
*   @return the authorization page.
*   @throws Exception if revoking access is unsuccessful.
*/
@GetMapping(value = {"/revoke"})
public String revoke(HttpSession session) throws Exception {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            Credential credentials = (Credential) session.getAttribute("credentials");
            ResponseEntity responseEntity = authService.revokeCredentials(credentials);
            Integer httpStatusCode = responseEntity.getStatusCodeValue();

            if (httpStatusCode != 200) {
                return onError("There was an issue revoking access: " +
                    responseEntity.getStatusCode(), model);
            }
            session.removeAttribute("credentials");
        }
        return startAuthFlow(model);
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

アドオンをテストする

Google Classroom にログインします。 教師テストユーザーの 1 人としてログインさせます。[授業] タブに移動し、 新しい課題を作成します。テキスト領域の下にある [アドオン] ボタンをクリックします。 アドオンを選択しますiframe が開き、アドオンが Attachment Setup URI: GWM SDK の App Engine 構成 できます。

これで、次のステップ、繰り返しの処理を開始する準備が整いました。 確認できます