JavaScript
AWS
AdventCalendar
cognito
AppSync

AWS AppSync をシンプルなJavaScriptで試してみる


はじめに

こんにちは、Serverless Advent Calendar 2018 の 19日目 です。

今まで見ることしかしていなかったAdvent Calendarですが、

大好きなServerlessについて、初めて記事を書くことを決意しました。


私と Serverless Advent Calendar 2018

・ケチかつ飽き性なので、従量課金のServerlessに興味をもつ

・モバイルアプリを作ろうとしたが、認証/ソケット/Push通知あたりで手こずる

・DynamoDBの設計がイマイチでAPIの数も増える一方…(この辺りでやる気なくなってきた)

DevDayTokyo2018のServerlessDayに参加

・その中のセッションでAWS AppSyncの素晴らしさ を感じ、 DynamoDBの設計指針も固まった

・この熱が冷める前にAdventCalendarに参加!


私の経験

・10年目のシステムエンジニア

・業務ではAWSは利用していない

・APIGateway+Lambda+DynamoDBは業務外で少し経験あり(SAM)

・AWS AppSync初(Cognito初、GraphQL初)


さっそくチュートリアル

以下をゴールとし、理解を深めるために「なるべく」手作りする

・Cognitoでユーザ認証(メール認証、Google連携)

・GraphQL経由でのデータアクセス、更新(認証ユーザでのデータアクセス制御)

・GraphQLのサブスクリプションによる通知


順序

・Cognito UserPoolの作成(AppSyncの設定に必要)

・Appsync でAPIの作成

・WebサイトからGraphQLでのデータアクセス、サブスクリプションを試す


Cognito UserPoolの作成

マネジメントコンソールから「ユーザープールを作成」で作成開始

image.png

適当な名称をつけて、「デフォルトを確認する」で作る

image.png

「属性」を修正する

ユーザのサインインは「メールアドレス」とする(一般的、かつ、Google連携との相性を考慮)

emailは必須属性とする

image.png

「アプリクライアント」を追加する

image.png

※Webから繋ごうとしているので、クライアントシークレットは生成しない

image.png

「確認」からプールを作成する

image.png

以下をメモしておく

「プールID」 ⇨ リージョン_ランダム文字列:us-west-2_xxxxxxxxx

「アプリクライアントID」 ⇨ ランダム文字列:xxxxxxxxxxxxxxxxxxxxxxxxxx


メール認証でユーザを作成する

認証を行うために、Cognitoで用意しているフォームを使う

「アプリの統合」⇨「アプリクライアントの設定」

image.png



image.png

※コールバックURLは適当だが、認証用URLのパラメータで完全一致させる必要があるので注意

認証用のドメインを設定する

image.png

認証用ページに行ってみる

https://設定したドメイン.auth.リージョン.amazoncognito.com/login?response_type=code&client_id=クライアントID&redirect_uri=http://リダイレクトURL/

Ex. https://test.auth.us-west-2.amazoncognito.com/login?response_type=code&client_id=xxxxxxxxxxxxxxxxxxxxxxxxxx&redirect_uri=https://aws.amazon.com/jp/

サインアップする

入力したメールアドレスに送信された認証コードを入れる

リダイレクトURLに遷移する

image.png

ユーザープールの「ユーザーとグループ」に追加されている

image.png


Google連携でユーザを作成する

GCPアカウントを作り、プロジェクトを作成し、「認証情報」に行く

image.png

初めての場合は「OAuth同意画面」からドメインを登録しておく

「認証情報」に戻り、OAuthクライアントIDを作成する



image.png

承認済みのリダイレクトURIには、以下を設定する

https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/idpresponse

作成するとクライアントIDとシークレットが表示されるので、メモしておく

AWSのマネジメントコンソールに戻り、ユーザープールの「IDプロパイダー」から、Googleを選択する

image.png

image.png

・GoogleアプリID:GCPで取得したクライアントID

・アプリシークレット:GCPで取得したシークレット

・承認スコープ:"profile email openid"

「属性マッピング」の Googleタブでemailを設定し、Googleからメールアドレスを取得できるようにする

image.png

「アプリクライアントの設定」の有効なIDプロパイダにGoogleが追加されているので、チェックを入れる

image.png

認証用URLに遷移すると、Googleが新たに追加されている

image.png


Appsync の設定

マネジメントコンソールから「Create API」で作っていく

image.png

「Create with wizard」を選択してStart

image.png

デフォルトで作成する(モデルは後から変えられる)

image.png

AppSyncが作成されるので「Setting」で設定を確認し、メモしておく

API URL

API ID

AppSyncのAuthorization typeをCognitoに変更し、作成したユーザプールを指定する

image.png

「Queries」からクエリを実行しようとすると、UnauthorizedExceptionとなる

"Login With User Pools"からログインすると、クエリを実行できるようになる


Webサイトからcognitoでログイン&GraphQLでのデータ取得/登録を試す

GraphQLを呼び出すには、jwt(アクセストークン)が必要になる

jwtは、認証用ページで認証をした後にリダイレクトURLに遷移した際のクエリパラメータ「code」を使うことで、cognitoのエンドポイントから取得することができる

↓ ログイン処理までを以下の順で説明する

- 外部javascriptの読み込み

- 独自関数群

- ログイン処理


index.html

<html>

<head>
<meta charset="UTF-8">
<!-- 外部javascriptの読み込み -->
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script type="module" src="src/aws-exports.js"></script>
<script src="src/aws-sdk.js"></script>
<script src="src/config.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>

<!-- 独自関数群 -->
<script>
var _resolve = function(x){ return $.Deferred().resolve(x).promise(); };
var _reject = function(x){ return $.Deferred().reject(x).promise(); };
var _getStorageItem = function(x){
var _item = JSON.parse(localStorage.getItem(x));
return _item ? _item : {};
};

_param={};
location.search.slice(1).split("&").forEach(v=>{
key = v.split("=")[0];
_param[key] = v.replace(key+"=","");
});
var _cognito = {
storage : {
setTokens : _tokens => {
localStorage.setItem("tokens", JSON.stringify(_tokens));
_cognito.storage.jwt = _tokens.access_token;
return _resolve(_tokens);
},
jwt : _getStorageItem("tokens").access_token
},
api : {
getTokens : _code => {
return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
},
getUser : () => {
return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
},
graphql : (_query) => {
return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
},
},
};

//ログイン処理
typeof _cognito.storage.jwt === "undefined" && !_param.code ? location.href = opt.aws_cognito_loginEndpoint : ""; //token, codeなし
(_cognito.storage.jwt
? _resolve(_cognito.storage.jwt)
: _cognito.api.getTokens(_param.code) //token取得
.then(x=>_cognito.storage.setTokens(x)) //storageに保存
.then(x=>location.href=opt.aws_cognito_redirecturl)
)
.then(() =>
_cognito.api.graphql({
query: `
{
getMyInfo {
id
email
}
}
`
})
)
.then(x=>{
return x.data.getMyInfo
? _resolve(console.log(x.data.getMyInfo))
: _cognito.api.getUser() //ユーザ情報取得
.then(x=>_cognito.api.graphql({ //ユーザ作成
operationName: "myInfo",
query: `
mutation myInfo($input: CreateMyInfoInput!){
createMyInfo(input: $input){
email
}
}`
,
variables: {
"input": {
"email": x.email
}
}
}));
})
.fail(x=>_cognito.storage.setTokens({}).then(x=>location.href = opt.aws_cognito_loginEndpoint));
</script>
</head>
<body>
</body>
</html>



外部javascriptの読み込み

CDNから取得したjQuery

amplifyコマンドで作成した aws-exports.js

aws-sdk.js

・定数定義などをした config.js(これだけイチから作成したjsファイル)

Paho Javascript Clientからダウンロードした mqttws31.min.js

  <!-- 外部javascriptの読み込み -->

<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script type="module" src="src/aws-exports.js"></script>
<script src="src/aws-sdk.js"></script>
<script src="src/config.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>


独自関数群

・Promise用の _resolve, _reject関数

・ローカルストレージアクセス用の _getStorageItem関数

・Getパラメータの取得結果を格納する _paramオブジェクト

var _resolve = function(x){ return $.Deferred().resolve(x).promise(); };

var _reject = function(x){ return $.Deferred().reject(x).promise(); };
var _getStorageItem = function(x){
var _item = JSON.parse(localStorage.getItem(x));
return _item ? _item : {};
};

_param={};
location.search.slice(1).split("&").forEach(v=>{
key = v.split("=")[0];
_param[key] = v.replace(key+"=","");
});

・Cognito/GraphQL周りのアクセス用の _cognitoオブジェクト

関数名
説明

_cognito.storage.setTokens
cognitoから取得したアクセストークンをローカルストレージに保存する関数

_cognito.storage.jwt
アクセストークンを格納する変数

_cognito.api.getTokens
cognitoからアクセストークンを取得する関数

_cognito.api.getUser
cognitoからログイン中のユーザ情報を取得する関数

_cognito.api.graphql
AppsyncのGraphQL APIを呼び出す関数

var _cognito = {

storage : {
setTokens : _tokens => {
localStorage.setItem("tokens", JSON.stringify(_tokens));
_cognito.storage.jwt = _tokens.access_token;
return _resolve(_tokens);
},
jwt : _getStorageItem("tokens").access_token
},
api : {
getTokens : _code => {
return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
},
getUser : () => {
return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
},
graphql : (_query) => {
return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
},
},
};


ログイン処理

・jwtがストレージに保存されていない、かつ、パラメータにcodeが無い場合は、認証用ページに遷移させる

//ログイン処理

typeof _cognito.storage.jwt === "undefined" && !_param.code ? location.href = opt.aws_cognito_loginEndpoint : ""; //token, codeなし

1.jwtがローカルストレージにある場合は2に進み、jwtが無い場合はcognitoから取得してローカルストレージに保存

2.jwtを取得したら、GraphQL(DynamoDB)から自身の情報を取得する

3.自身の情報が取得できたらコンソールに出力して終了する

4.自身の情報が取得できなかった場合は、DynamoDBにデータを作成する(cognitoからemailの情報を取得し、GraphQLで実行する)

(_cognito.storage.jwt

? _resolve(_cognito.storage.jwt)
: _cognito.api.getTokens(_param.code) //1.cognitoからtoken取得
.then(x=>_cognito.storage.setTokens(x)) //1.storageに保存
.then(x=>location.href=opt.aws_cognito_redirecturl)
)
.then(() =>
_cognito.api.graphql({ //2.自身の情報を取得する
query: `
{
getMyInfo {
id
email
}
}
`
})
)
.then(x=>{
return x.data.getMyInfo
? _resolve(console.log(x.data.getMyInfo)) //3. コンソールに出力して終了
: _cognito.api.getUser() //4. cognitoからユーザ情報取得
.then(x=>_cognito.api.graphql({ //4. GraphQLでユーザ作成
operationName: "myInfo",
query: `
mutation myInfo($input: CreateMyInfoInput!){
createMyInfo(input: $input){
email
}
}`
,
variables: {
"input": {
"email": x.email //4. cognitoから取得したユーザ情報のemailを利用する
}
}
}));
})
.fail(x=>_cognito.storage.setTokens({}).then(x=>location.href = opt.aws_cognito_loginEndpoint)); //エラー時はtokenを全て消してリダイレクトする(最初からやり直し)

getMyInfoのResolverは以下の通り

IDにcognitoのsubを使う

#request mapping template

{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": { "S" : "${context.identity.sub}" }
}
}
#response mapping template
$util.toJson($ctx.result)

createMyInfoのResolverは以下の通り

IDにcognitoのsubを使う

attributeValuesでinputのemailが設定される

#request mapping template

{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.identity.sub),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id",
},
},
}
#response mapping template
$util.toJson($ctx.result)

これで、ログインまでを実装することができた!


APIの呼び出しについてもう少し詳しく解説


_cognito.api.getTokens

cognitoからアクセストークンを取得する関数

・urlは「https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/token」を設定する

・client_idはユーザープールで設定した「アプリクライアントID」を設定する

・redirect_uriはアプリクライアントの設定の「コールバックURL」と一致させる

・codeは認証用ページからリダイレクトされた時に付与されるGetパラメータcodeが設定される

getTokens : _code => {

return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
}


_cognito.api.getUser

cognitoからログイン中のユーザ情報を取得する関数

・urlは「https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/userInfo」を設定する

・Authorizationヘッダに、"Bearer " + jwt を設定する

getUser : () => {

return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
}


_cognito.api.graphql

AppsyncのGraphQL APIを呼び出す関数

・urlはAppsyncの「Settings」にあるAPI URLを設定する

・Authorizationヘッダに、jwt を設定する

graphql : (_query) => {

return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
}


WebサイトからAppsyncのSubscriptionを試す

以下の順序でSubscriptionを確認する

1.GraphQLのsubscriptionを実行する

2.戻り値でMQTT接続情報が返って来るため、Paho.MQTT.Clientを作成し、接続する

3.接続後、subscribeをしてメッセージを受け付ける

4.メッセージを受け取ると、コンソールにログを出力する

var onConnect = function(x){ //3.接続後、subscribeをしてメッセージを受け付ける関数

console.log("onConnect!");
console.log(x);
client.subscribe(sub.topics);
};
var onMessageArrived = function(x){ //4.メッセージを受け取ると、コンソールにログを出力する関数
console.log("onMessageArrived!");
console.log(x.payloadString);
};
var onConnectionLost = function(x){
console.log("onConnectionLost!");
console.log(x);
};
//subscription開始
_cognito.api.graphql({ //1.GraphQLのsubscriptionを実行する
query: `
subscription NewPostSub {
onCreatePost {
id
postDate
message
}
}
`

})
.then(x=>{ //2.戻り値でMQTT接続情報が返って来るため、Paho.MQTT.Clientを作成し、接続する
sub = {
wssUrl: x.extensions.subscription.mqttConnections[0].url,
client: x.extensions.subscription.mqttConnections[0].client,
topics: x.extensions.subscription.mqttConnections[0].topics[0],
}
client = new Paho.MQTT.Client(sub.wssUrl, sub.client);
var connectOptions = {
onSuccess: onConnect, //3.接続後、subscribeをしてメッセージを受け付ける関数の登録
useSSL: true,
timeout: 3,
mqttVersion: 4,
onFailure: function(x) {
console.log("connect failed!");
console.log(x);
}
};
client.onMessageArrived = onMessageArrived; //4.メッセージを受け取ると、コンソールにログを出力する関数の登録
client.onConnectionLost = onConnectionLost;
client.connect(connectOptions);
})
.fail(x=>console.log(x));

上記を実行後、onCreatePostの発火を促すcreatePostを実行すると、メッセージが出力される!

AppsyncのQueryでmutationを実行

image.png

メッセージが通知される

image.png


終わりに

ご覧いただきありがとうございました。

日本語の情報がかなり少なく大変でしたが、シンプルなjavascriptでCognitoへのログイン、

Appsyncを利用したGraphQLの実行(Query/Mutation/Subscription)ができました。

次はルームを作って特定ユーザのみSubscriptionできるようにする仕組みや、オフライン時の動作などを確認したいと思います。

至らぬ点などありましたら、コメントいただけるとありがたいです。