nginx を用いたリバースプロキシ型の OpenID Connect(OIDC)ユーザ認証を、ローカルPCの開発環境向けに構築したので紹介します。
nginx の njs(NGINX JavaScript) を用いて OIDC 認可コードフローを実装しました。njs は nginx のモジュールです。JavaScript で nginx の機能を拡張できます。公式の nginx docker イメージでも利用できます。
nginx + JavaScript の組み合わせは、本番間環境により近い構成にできる自由度の高さがメリットです。
リバースプロキシ型とは
ユーザー認証をリバースプロキシと認可サーバに任せます。アプリケーションはビジネスロジックの開発に集中できます。
リバースプロキシは、ブラウザからの HTTP リクエストをバックエンドの WEBサーバへ転送しますがその際、HTTPリクエストのセッション Cookie を検証します。
セッション Cookie が存在しない場合、リバースプロキシは、認可サーバへリダイレクトしユーザにログインを促します。ユーザがログインするとリバースプロキシへリダイレクトバックされるので、認可サーバからトークンを取得しそのトークンを用いてユーザ情報を取得するとともにセッション Cookie を作成します。
セッション Cookie が存在する場合、リバースプロキシは、HTTPリクエストヘッダにユーザ情報を付加し WEBサーバへ転送します。
リバースプロキシ型について詳しくは次の記事などを参照ください。
njs で実装した機能
njs を用いると、WEBサーバが HTTP リクエストハンドラを呼び出すのと似た感じで nginx から JavaScript 関数を呼び出すことができます。
ただし njs は、nginx をアプリケーション サーバーにすることが目的ではないとのこと ↓
今回は、njs で次の機能を実装しました。
- OIDC 認可コードフローでトークン取得
- 認可サーバからユーザ情報を取得し JWT に変換後、セッション Cookie に格納
- セッション Cookie の JWT を検証
注意
OIDC の state や nonce といった脆弱性対策は実装していません
動作環境
Dockerコンテナで環境構築します。
- Docker version 23.0
- 認可サーバ: Keycloak 21.0
- リバースプロキシ: nginx 1.23
- WEBサーバ: nginx 1.23
実行方法
次のフォルダ構成で各種の設定ファイルと njs の JavaScript コードを配置し docker compose を実行します。設定ファイルおよびコードの全文は以降に掲載します。
フォルダ構成
.
├── backend/ # WEBサーバ
│ └── nginx.conf
├── docker-compose.yml
├── keycloak/ # 認可サーバ
│ └── import/
│ └── realm-develop.json # Keycloak の Realm エクスポートファイル
└── proxy/ # リバースプロキシ
├── conf.d/
│ └── app.conf
├── nginx.conf
└── njs/ # njs の JavaScript コード
├── jwt.js
└── oidc.js
version: "3.9"
services:
keycloak:
image: quay.io/keycloak/keycloak:21.0
container_name: idp
restart: always
command: start-dev --import-realm
environment:
KEYCLOAK_ADMIN: "admin"
KEYCLOAK_ADMIN_PASSWORD: "admin"
KC_HOSTNAME_URL: "http://localhost:8080"
KC_HOSTNAME_STRICT_BACKCHANNEL: false
ports:
- 0.0.0.0:8080:8080
volumes:
- ./keycloak/import:/opt/keycloak/data/import/:ro
proxy:
container_name: proxy
image: nginx:1.23
restart: always
environment:
OIDC_ISSUER: "http://localhost:8080/realms/develop"
OIDC_AUTH_ENDPOINT: "http://localhost:8080/realms/develop/protocol/openid-connect/auth"
OIDC_LOGOUT_ENDPOINT: "http://localhost:8080/realms/develop/protocol/openid-connect/logout"
OIDC_TOKEN_ENDPOINT: "http://idp:8080/realms/develop/protocol/openid-connect/token"
OIDC_USER_INFO_ENDPOINT: "http://idp:8080/realms/develop/protocol/openid-connect/userinfo"
OIDC_CLIENT_ID: "reverse-proxy"
OIDC_CLIENT_SECRET: "hogehogehoge"
OIDC_SCOPE: "openid" # 空白文字区切り
JWT_GEN_KEY: "mySecretKey"
ports:
- 0.0.0.0:80:80
volumes:
- ./proxy/nginx.conf:/etc/nginx/nginx.conf:ro
- ./proxy/conf.d:/etc/nginx/conf.d/:ro
- ./proxy/njs:/etc/nginx/njs/:ro
backend:
container_name: backend
image: nginx:1.23
volumes:
- ./backend/nginx.conf:/etc/nginx/nginx.conf:ro
コンテナを開始します。
$ docker compose up -d
$ docker compose ps
NAME IMAGE ... SERVICE ... PORTS
backend nginx:1.23 ... backend ... 80/tcp
idp quay.io/keycloak/keycloak:21.0 ... keycloak ... 0.0.0.0:8080->8080/tcp, 8443/tcp
proxy nginx:1.23 ... proxy ... 0.0.0.0:80->80/tcp
コンテナを開始すると Keycloak の初期化処理が走るのでそれが終わるまで1分ほど待ちます。
ブラウザで http://localhost/ を開くと Keycloak のログイン画面が表示されます。初回はユーザ登録(セルフサインアップ)します。ログインするとバックエンドのWEBサーバ nginx のウェルカムページが表示されます。
ブラウザの開発ツールで、セッション Cookie MY_SESSION
が作成されていることを確認。
WEBサーバ nginx(backendコンテナ)のログで、HTTPリクエストヘッダにユーザ情報(JWT)が付加されていることを確認。ログの eyJ0eX ... CnARo
の部分がユーザ情報の JWT です。
$ docker compose logs backend
backend | 192.168.208.2 - - [05/Apr/2023:09:42:58 +0000] "GET / HTTP/1.0" 200 615 "-" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1YjhiNWZjZS1jN2JjLTQ2NmMtOTJlMy0wOTQxYTc0NGJmNzkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJhIGIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJkZXZAZXhhbXBsZS5jb20iLCJnaXZlbl9uYW1lIjoiYSIsImZhbWlseV9uYW1lIjoiYiIsImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIn0.Hhl9aBQznkfQTjjShyR1-5kkxYFvdXtKVAQgXjCnARo"
サイト JSON Web Tokens - jwt.io で JWT の中身を確認できます。漏洩しても問題が無いデータでお試しください。
ブラウザで http://localhost/logout を叩くとログアウトできます。
リバースプロキシ nginx の設定
proxy/nginx.conf
に次の設定を追加します。
- njs モジュールをロード
-
conf.d/app.conf
をインクルード
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
load_module modules/ngx_http_js_module.so; # njs モジュールをロード
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/app.conf; # conf.d/app.conf をインクルード
}
proxy/conf.d/app.conf
に次を設定します。
- Docker resolver
- インポートするJSモジュールのパス追加
- インポートするJSモジュール
- nginx 変数に JS関数の戻り値を設定
-
server
セクションで URLパスと JSモジュール関数を紐づけ
resolver 127.0.0.11; # Docker resolver
js_path "/etc/nginx/njs/"; # インポートするJSモジュールのパス追加
js_import oidc from oidc.js; # インポートするJSモジュール
js_set $user_info oidc.user_info; # nginx 変数に JS関数の戻り値を設定
server {
listen 80;
location / {
auth_request /auth/validate; # /auth/validate でログイン有無を確認
error_page 401 = @login; # 未ログインなら @login へ
expires -1;
proxy_set_header X-Amzn-Oidc-Data $user_info; # ヘッダーにユーザ情報を付加
proxy_pass http://backend; # WEBサーバへ転送
}
location /auth/validate {
internal;
js_content oidc.validate; # JSモジュール oidc の関数 validate を呼び出し
}
location @login {
js_content oidc.login; # JSモジュール oidc の関数 login を呼び出し
}
location /auth/postlogin {
js_content oidc.postlogin; # JSモジュール oidc の関数 postlogin を呼び出し
}
location /logout {
js_content oidc.logout; # JSモジュール oidc の関数 logout を呼び出し
}
location /auth/postlogout {
js_content oidc.postlogout; # JSモジュール oidc の関数 postlogout を呼び出し
}
location @bye {
add_header 'Content-Type' 'text/html';
return 200 "See you again.\n";
}
}
ロケーション /
にマッチすると /auth/validate
にログイン有無を確認しにいきます。/auth/validate
は、JS関数 oidc.validate()
を呼び出します。oidc.validate()
は、セッション Cookie を調べログイン済みなら HTTPステータス 200 を返し、未ログインなら 401 を返します。ステータス 200 ならヘッダーにユーザ情報を付加しリクエストをバックエンドへ転送します。401 なら @login
へ内部リダイレクトし JS関数 oidc.login()
を呼び出しログインシーケンスを開始します。ログインシーケンスは OIDC 認可コードフローに基づきます。
@
で始まるロケーションは、nginx の名前付きロケーションです。URIパスの書き換えを行わない内部リダイレクトに利用します。
リバースプロキシ nginx の JS関数
ロケーション /auth/validate
に紐づく JS関数 oidc.validate()
は、セッション Cookie に格納された JWT の署名を検証します。検証に成功すれば HTTPステータス 200 を、失敗すれば 401 を返します。
- JS関数
oidc.validate()
// ログイン済みか否かをチェック
async function validate(r) {
let secret_key = process.env.JWT_GEN_KEY;
let session_data = r.variables.cookie_MY_SESSION;
let valid = await jwt.verify(session_data, secret_key);
if( valid ) {
r.return(200);
} else {
r.return(401);
}
}
ここで関数 validate(r)
の引数 r
は、HTTPリクエストを表すオブジェクトです。process
は、プロセスに関する情報を提供するグローバル オブジェクトです。
リファレンスはこちら↓
process.env.JWT_GEN_KEY
は、環境変数 JWT_GEN_KEY
の値を取得します。
リファレンスの注釈に次のようにありますが、動作確認した環境では env ディレクティブを使用せずとも環境変数を参照できました。理由は不明。
デフォルトでは、nginx は親プロセスから継承されたすべての環境変数を削除しますが、TZ 変数は除きます。継承された変数の一部を保持するには、 env ディレクティブを使用します。
r.variables.cookie_MY_SESSION
は nginx の組み込み変数です。Cookie MY_SESSION
の値を参照します。このように JSコード内で nginx の組み込み変数を参照することもできます。
ロケーション @login
に紐づくJS関数 oidc.login()
は、認可サーバ(Keycloak)の認可エンドポイントへリダイレクトします。認可サーバは、ログイン画面を表示します。
- JS関数
oidc.login()
const postlogin_uri = "http://localhost:80/auth/postlogin";
// OIDC認可エンドポイントへリダイレクト
function login(r) {
let referer = r.variables.uri;
let params = qs.stringify({
response_type : "code",
scope : process.env.OIDC_SCOPE,
client_id : process.env.OIDC_CLIENT_ID,
redirect_uri : postlogin_uri + "?" + qs.stringify({p: referer}),
});
let url = process.env.OIDC_AUTH_ENDPOINT + "?" + params;
r.return(302, url);
}
ユーザがログインすると、認可エンドポイントへのリクエストパラメータに指定した redirect_uri
(= /auth/postlogin)へリダイレクトバックされ、JS関数 oidc.postlogin()
が呼び出されます。
JS関数 oidc.postlogin()
は、認可サーバのトークンエンドポイントから IDトークンを取得し検証します。そのトークンを用いてユーザ情報を取得し JWT へ変換します。JWT をセッション Cookie に設定し、呼び出し元ページへリダイレクトします。
- JS関数
oidc.postlogin()
// ログイン後の処理
async function postlogin(r) {
try {
let referer = r.args.p;
// OIDCトークン取得
let redirect_uri = postlogin_uri + "?" + qs.stringify({p: referer});
let tokens = await get_token(r.args.code, redirect_uri);
// IDトークン検証
validate_id_token(tokens.id_token, process.env.OIDC_ISSUER, process.env.OIDC_CLIENT_ID);
// OIDCユーザ情報取得
let claims = await get_userinfo(tokens.access_token);
// ユーザ情報を JWT に変換
let secret_key = process.env.JWT_GEN_KEY;
let session_data = await jwt.encode(claims, secret_key);
r.headersOut["Set-Cookie"] = [
"MY_SESSION=" + session_data + "; Path=/",
];
r.return(302, referer);
} catch (e) {
r.error(e.message);
r.return(403); // Forbidden
}
}
注意
IDトークンの検証は、一部のクレームのみを対象とし、署名は検証していません。
// OIDC IDトークン検証
function validate_id_token(token, issuer, audience) {
let payload = jwt.decode(token).payload;
if( payload.iss != issuer ) throw new Error("invalid token");
if( payload.aud != audience ) throw new Error("invalid token");
if( payload.exp < Math.floor(Date.now()/1000) ) throw new Error("invalid token");
}
セッション Cookie に格納する JWT は暗号化していません。
暗号化したい場合は njs のサンプルコードに暗号化の例があります。
- njs サンプルコード
ログアウトの際は、認可サーバ Keycloak のセッションも併せて終了する必要があります。
ロケーション /logout
に紐づく JS関数 oidc.logout()
は、Keyclok のログアウトエンドポイントへリダイレクトします。Keycloak がログアウトされるとロケーション/postlogout
へリダイレクトバックされるので JS関数 oidc.postlogout()
でセッション Cookie を削除します。
- JS関数
oidc.logout()
// ログアウトエンドポイントへリダイレクト
function logout(r) {
let params = qs.stringify({
client_id : process.env.OIDC_CLIENT_ID,
post_logout_redirect_uri : postlogout_uri,
});
let url = process.env.OIDC_LOGOUT_ENDPOINT + "?" + params;
r.return(302, url);
}
- JS関数
oidc.postlogout()
// ログアウト後の処理. セッション Cookie 削除
function postlogout(r) {
r.headersOut['Set-Cookie'] = ["MY_SESSION=; Path=/; Max-Age=-1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"];
r.internalRedirect("@bye");
}
バックエンド nginx の設定
バックエンドのWEBサーバやアプリケーションサーバは、リバースプロキシが HTTPリクエストヘッダに付加したユーザ情報(JWT)を見て、それが誰からのリクエストなのかを識別し、リソースのアクセス制御を行うことができます。
ここではそこまで実装しません。HTTPリクエストヘッダに付加されたユーザ情報(JWT)をログ出力するまでにとどめます。
backend/nginx.conf
に次を設定します。
-
log_format
にHTTPリクエストヘッダの出力を追加
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ユーザ情報(ヘッダ X-Amzn-Oidc-Data)をログへ出力
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_x_amzn_oidc_data"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
ここでは、ユーザ情報(JWT)をヘッダ X-Amzn-Oidc-Data に格納しているので log_format
に '"$http_x_amzn_oidc_data"'
を追加します。
ログ出力例
$ docker compose logs backend
backend | 192.168.208.2 - - [05/Apr/2023:09:42:58 +0000] "GET / HTTP/1.0" 200 615 "-" "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1YjhiNWZjZS1jN2JjLTQ2NmMtOTJlMy0wOTQxYTc0NGJmNzkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJhIGIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJkZXZAZXhhbXBsZS5jb20iLCJnaXZlbl9uYW1lIjoiYSIsImZhbWlseV9uYW1lIjoiYiIsImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIn0.Hhl9aBQznkfQTjjShyR1-5kkxYFvdXtKVAQgXjCnARo"
このヘッダ名にした理由が気になる方は、巻末の参考記事「Application Load Balancer を使用してユーザーを認証する」をご覧ください。
Keycloak の Realm エクスポートファイルの準備
リバースプロキシが認可サーバからトークンを取得する際にクライアント ID とクライアントシークレットが必要ですが、そのためには Keycloak の Realm にアプリケーションクライアントを登録する必要があります。
コンテナを開始/終了する度にアプリケーションクライアントを登録しなおすのは面倒なので、事前定義した Realm をインポートするようにします。
注意
クライアントシークレットの安全な取り扱いについては考慮しません。
手順
- Realm 作成
- アプリケーションクライアント登録
- セルフサインアップ有効化
- Realm エクスポート
- クライアントシークレット設定
- Realm エクスポートファイル配置
Realm エクスポートファイル keycloak/import/realm-develop.json
が存在しない状態で Docker Compose を実行しコンテナを開始します。
$ docker compose up -d
Keycloak 初期化処理が終わるまで1分ほど待ちます。ブラウザから http://localhost:8080/ を開くと Keycloak のウェルカムページが表示されるので [Administration Console] を選択し admin/admin でログインします。
Realm 作成
Realm "develop" を作成します。
画面左上の master と表示されているプルダウンから Create Realm を選択。
Realm name に "develop" と入力し Create する。
アプリケーションクライアント登録
作成したRealm "develop" にアプリケーションクライアント "reverse-proxy" を登録します。
サイドメニューから Clients を選択し Creat Client をクリック。
必須入力項目
Field | Value |
---|---|
Client ID | reverse-proxy |
Client authentication | ON |
Valid redirect URIs | * |
Valid post logout redirect URIs | * |
注意
上記の redirect URIs の設定値は全てを許容する脆弱な設定です。
セルフサインアップ有効化
ユーザーのセルフサインアップを許可します。
サイドメニューから Realm settings を選択。
タブ Login を開き User registration を ON。
Realm エクスポート
作成した Realm "develop" をファイルにエクスポートします。
画面右上の Action プルダウンから Partial Export を選択。
Include clients を ON にし Export を実行。
クライアントシークレット設定
エクスポートされたファイルにクライアントシークレットは出力されないので、手動でクライアントシークレットを書き込みます。
保存した Realm エクスポートファイルをエディタで開く。
clientId "reverse-proxy" を検索し secret に "hogehogehoge" と入力し保存。
Realm エクスポートファイル配置
Realm エクスポートファイルを keycloak/import/realm-develop.json
に配置します。
Docker Compose を再起動すると Realm エクスポートファイルがインポートされます。
$ docker compose down -v
$ docker compose up -d
コンテナ keycloan のログにインポートした旨のメッセージが表示されます。
$ docker compose logs keycloak
... Full importing from file /opt/keycloak/bin/../data/import/realm-develop.json
... Realm 'develop' imported
Realm のインポート/エクスポートについては、Keycloak のドキュメントを参照ください ↓
njs コード全文
proxy/njs/jwt.js
// JWTデコード
function decode(jot) {
var parts = jot.split('.').slice(0,2)
.map(v=>Buffer.from(v, 'base64url').toString())
.map(JSON.parse);
return { headers:parts[0], payload: parts[1] };
}
// JWTエンコード
async function encode(claims, key) {
let header = { typ: "JWT", alg: "HS256" };
let s = [header, claims]
.map(JSON.stringify)
.map(v=>Buffer.from(v).toString('base64url'))
.join('.');
let wc_key = await crypto.subtle.importKey(
'raw', key, {name: 'HMAC', hash: 'SHA-256'}, false, ['sign']);
let sign = await crypto.subtle.sign({name: 'HMAC'}, wc_key, s);
return s + '.' + Buffer.from(sign).toString('base64url');
}
// JWT署名検証
async function verify(jot, key) {
if( !jot ) return false;
let parts = jot.split('.');
let data = parts.slice(0,2).join('.');
let sign = Buffer.from(parts[2], 'base64url');
let wc_key = await crypto.subtle.importKey(
'raw', key, {name: 'HMAC', hash: 'SHA-256'}, false, ['verify']);
return crypto.subtle.verify({name: 'HMAC'}, wc_key, sign, data);
}
export default {decode, encode, verify}
proxy/njs/oidc.js
import qs from "querystring";
import jwt from "jwt.js";
const postlogin_uri = "http://localhost:80/auth/postlogin";
const postlogout_uri = "http://localhost:80/auth/postlogout";
// OIDC IDトークン検証
function validate_id_token(token, issuer, audience) {
let payload = jwt.decode(token).payload;
if( payload.iss != issuer ) throw new Error("invalid token");
if( payload.aud != audience ) throw new Error("invalid token");
if( payload.exp < Math.floor(Date.now()/1000) ) throw new Error("invalid token");
}
// OIDCトークンエンドポイントからトークン取得
async function get_token(code, redirect_uri) {
let reply = await ngx.fetch(process.env.OIDC_TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: qs.stringify({
grant_type : "authorization_code",
client_id : process.env.OIDC_CLIENT_ID,
client_secret : process.env.OIDC_CLIENT_SECRET,
redirect_uri : redirect_uri,
code : code,
}),
});
return await reply.json();
}
// OIDCユーザ情報取得
async function get_userinfo(access_token) {
let reply = await ngx.fetch(process.env.OIDC_USER_INFO_ENDPOINT, {
method: "GET",
headers: { "Authorization": "Bearer " + access_token },
});
return await reply.json();
}
// ログイン済みか否かをチェック
async function validate(r) {
let secret_key = process.env.JWT_GEN_KEY;
let session_data = r.variables.cookie_MY_SESSION;
let valid = await jwt.verify(session_data, secret_key);
if( valid ) {
r.return(200);
} else {
r.return(401);
}
}
// OIDC認可エンドポイントへリダイレクト
function login(r) {
let referer = r.variables.uri;
let params = qs.stringify({
response_type : "code",
scope : process.env.OIDC_SCOPE,
client_id : process.env.OIDC_CLIENT_ID,
redirect_uri : postlogin_uri + "?" + qs.stringify({p: referer}),
});
let url = process.env.OIDC_AUTH_ENDPOINT + "?" + params;
r.return(302, url);
}
// ログイン後の処理
async function postlogin(r) {
try {
let referer = r.args.p;
// OIDCトークン取得
let redirect_uri = postlogin_uri + "?" + qs.stringify({p: referer});
let tokens = await get_token(r.args.code, redirect_uri);
// IDトークン検証
validate_id_token(tokens.id_token, process.env.OIDC_ISSUER, process.env.OIDC_CLIENT_ID);
// OIDCユーザ情報取得
let claims = await get_userinfo(tokens.access_token);
// ユーザ情報を JWT に変換
let secret_key = process.env.JWT_GEN_KEY;
let session_data = await jwt.encode(claims, secret_key);
r.headersOut["Set-Cookie"] = [
"MY_SESSION=" + session_data + "; Path=/",
];
r.return(302, referer);
} catch (e) {
r.error(e.message);
r.return(403); // Forbidden
}
}
// ログアウトエンドポイントへリダイレクト
function logout(r) {
let params = qs.stringify({
client_id : process.env.OIDC_CLIENT_ID,
post_logout_redirect_uri : postlogout_uri,
});
let url = process.env.OIDC_LOGOUT_ENDPOINT + "?" + params;
r.return(302, url);
}
// ログアウト後の処理. セッション Cookie 削除
function postlogout(r) {
r.headersOut['Set-Cookie'] = ["MY_SESSION=; Path=/; Max-Age=-1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"];
r.internalRedirect("@bye");
}
// バックエンドへ送るユーザ情報
function user_info(r) {
return r.variables.cookie_MY_SESSION;
}
export default {validate, login, logout, postlogin, postlogout, user_info}
参考にした記事