💾

Amplify Authenticationのトークンってどこに保存されているの?

2023/07/22に公開

Amplify LibrariesのAuthenticationはフロントエンド側のコーティングだけで簡単にユーザー認証機能を作成できます。
内部的にはAmazon Cognitoを用いていますが、アクセストークンをどのように保持しているのかが気になったので、実装を見てみました。

結論

デフォルトではlocalStorageに保存している。

書かないこと

Amplify Librariesの使い方

AWS Amplify

AmplifyはAWSの他サービスと連携してフルスタックのWeb・モバイルアププリケーションを作成するサービス群です。
現在はこれらのサービスで構成されています。

  • AWS嬢へのバックエンド作成をサポートするAmplify CLI
  • フロントエンドからの組み込みを行うためのAmplify Libraries
  • Webアプリのデプロイやホスティングを行うAmplify Hosting
  • GUIでアプリケーション開発ができるAmplify Studio
  • React, Vue, Flutterなどに機能を埋め込めるAmplify UI Components

Amplify FrameworkとかAmplify Consoleって名称はいつなくなったんだ

Amplify Libraries

ライブラリはaws-amplifyというnpm packageで配布されています。

Repository

SignInのサンプルコードは
ドキュメントにこのように記載されています。

import { Auth } from "aws-amplify";

type SignInParameters = {
  username: string;
  password: string;
};

export async function signIn({ username, password }: SignInParameters) {
  try {
    const user = await Auth.signIn(username, password);
  } catch (error) {
    console.log("error signing in", error);
  }
}

パスワードを用いた認証の場合はAuth.signInWithPasswordで実際の認証処理を行なっています。

	private signInWithPassword(
		authDetails: AuthenticationDetails
	): Promise<CognitoUser | any> {
		if (this.pendingSignIn) {
			throw new Error('Pending sign-in attempt already in progress');
		}

		const user = this.createCognitoUser(authDetails.getUsername());

		this.pendingSignIn = new Promise((resolve, reject) => {
			user.authenticateUser(
				authDetails,
				this.authCallbacks(
					user,
					value => {
						this.pendingSignIn = null;
						resolve(value);
					},
					error => {
						this.pendingSignIn = null;
						reject(error);
					}
				)
			);
		});

		return this.pendingSignIn;
	}

ここで認証結果を元にCognitoUserのインスタンスを生成しています。

export default class CognitoUser {
  /**
   * Constructs a new CognitoUser object
   * @param {object} data Creation options
   * @param {string} data.Username The user's username.
   * @param {CognitoUserPool} data.Pool Pool containing the user.
   * @param {object} data.Storage Optional storage object.
   */
  constructor(data) {
    if (data == null || data.Username == null || data.Pool == null) {
      throw new Error("Username and Pool information are required.");
    }

    this.username = data.Username || "";
    this.pool = data.Pool;
    this.Session = null;

    this.client = data.Pool.client;

    this.signInUserSession = null;
    this.authenticationFlowType = "USER_SRP_AUTH";

    this.storage = data.Storage || new StorageHelper().getStorage();

    this.keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`;
    this.userDataKey = `${this.keyPrefix}.${this.username}.userData`;
  }
  // 中略
}

ここで、オプションとして指定されていない場合はthis.storageStorageHelper.getStorage()を設定しています。
StorageHelper.getStorageで取得されるのはstorageWindowであり、constuctorではlocalStorageが設定されています。

export default class StorageHelper {
	/**
	 * This is used to get a storage object
	 * @returns {object} the storage
	 */
	constructor() {
		try {
			this.storageWindow = window.localStorage;
			this.storageWindow.setItem('aws.cognito.test-ls', 1);
			this.storageWindow.removeItem('aws.cognito.test-ls');
		} catch (exception) {
			this.storageWindow = MemoryStorage;
		}
	/**
	 * This is used to return the storage
	 * @returns {object} the storage
	 */
	getStorage() {
		return this.storageWindow;
	}
}

CognitoUser.cacheToken()this.storageに各tokenを保存しています。

	cacheTokens() {
		const keyPrefix = `CognitoIdentityServiceProvider.${this.pool.getClientId()}`;
		const idTokenKey = `${keyPrefix}.${this.username}.idToken`;
		const accessTokenKey = `${keyPrefix}.${this.username}.accessToken`;
		const refreshTokenKey = `${keyPrefix}.${this.username}.refreshToken`;
		const clockDriftKey = `${keyPrefix}.${this.username}.clockDrift`;
		const lastUserKey = `${keyPrefix}.LastAuthUser`;

		this.storage.setItem(
			idTokenKey,
			this.signInUserSession.getIdToken().getJwtToken()
		);
		this.storage.setItem(
			accessTokenKey,
			this.signInUserSession.getAccessToken().getJwtToken()
		);
		this.storage.setItem(
			refreshTokenKey,
			this.signInUserSession.getRefreshToken().getToken()
		);
		this.storage.setItem(
			clockDriftKey,
			`${this.signInUserSession.getClockDrift()}`
		);
		this.storage.setItem(lastUserKey, this.username);
	}

所感

amplify authでcognitoを初期設定で用いる時はどのtokenもlocalstorageに保管しています。(オプションとして他の場所を指定することは可能です。)
token認証は、ステートレスなのでスケールアウトしやすいというメリットを持ちますが、一度tokenを発行すると期限が切れるまで無効化しにくいという特徴を持ちます。
一般的にはaccessTokenの期限を短くし、refreshTokenの期限を相対的に長くすることで、漏洩時のリスクとユーザーの利便性を保つことがなされているかと思います。
しかし、クライアント側にrefreshTokenを保存する場合はこの戦略を用いることができないので、どういったプラクティスを用いられているのかが気になりました。

NCDCエンジニアブログ

Discussion