Open13
firebaseAuth x Service Worker ユーザー認証
firebae公式
公式のコードを使って全容(service-worker)
sw.js
/**
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Service worker for Firebase Auth test app application. The
* service worker caches all content and only serves cached content in offline
* mode.
*/
import firebase from 'firebase/app';
import 'firebase/auth';
import * as config from './config.js';
// Initialize the Firebase app in the web worker.
firebase.initializeApp(config);
const CACHE_NAME = 'cache-v1';
const urlsToCache = [
'/',
'/manifest.json',
'/config.js',
'/script.js',
'/common.js',
'/style.css'
];
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('user signed in', user.uid);
} else {
console.log('user signed out');
}
});
/**
* Returns a promise that resolves with an ID token if available.
* @return {!Promise<?string>} The promise that resolves with an ID token if
* available. Otherwise, the promise resolves with null.
*/
const getIdToken = () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
unsubscribe();
if (user) {
user.getIdToken().then((idToken) => {
resolve(idToken);
}, (error) => {
resolve(null);
});
} else {
resolve(null);
}
});
}).catch((error) => {
console.log(error);
});
};
/**
* @param {string} url The URL whose origin is to be returned.
* @return {string} The origin corresponding to given URL.
*/
const getOriginFromUrl = (url) => {
// https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
const pathArray = url.split('/');
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + '//' + host;
};
self.addEventListener('install', (event) => {
// Perform install steps.
event.waitUntil(caches.open(CACHE_NAME).then((cache) => {
// Add all URLs of resources we want to cache.
return cache.addAll(urlsToCache)
.catch((error) => {
// Suppress error as some of the files may not be available for the
// current page.
});
}));
});
// As this is a test app, let's only return cached data when offline.
self.addEventListener('fetch', (event) => {
const fetchEvent = event;
// Get underlying body if available. Works for text and json bodies.
const getBodyContent = (req) => {
return Promise.resolve().then(() => {
if (req.method !== 'GET') {
if (req.headers.get('Content-Type').indexOf('json') !== -1) {
return req.json()
.then((json) => {
return JSON.stringify(json);
});
} else {
return req.text();
}
}
}).catch((error) => {
// Ignore error.
});
};
const requestProcessor = (idToken) => {
let req = event.request;
let processRequestPromise = Promise.resolve();
// For same origin https requests, append idToken to header.
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == 'https:' ||
self.location.hostname == 'localhost') &&
idToken) {
// Clone headers as request headers are immutable.
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
// Add ID token to header. We can't add to Authentication header as it
// will break HTTP basic authentication.
headers.append('Authorization', 'Bearer ' + idToken);
processRequestPromise = getBodyContent(req).then((body) => {
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: 'same-origin',
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
// This will fail for CORS requests. We just continue with the
// fetch caching logic below and do not pass the ID token.
}
});
}
return processRequestPromise.then(() => {
return fetch(req);
})
.then((response) => {
// Check if we received a valid response.
// If not, just funnel the error response.
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// If response is valid, clone it and save it to the cache.
const responseToCache = response.clone();
// Save response to cache only for GET requests.
// Cache Storage API does not support using a Request object whose method is
// not 'GET'.
if (req.method === 'GET') {
caches.open(CACHE_NAME).then((cache) => {
cache.put(fetchEvent.request, responseToCache);
});
}
// After caching, return response.
return response;
})
.catch((error) => {
// For fetch errors, attempt to retrieve the resource from cache.
return caches.match(fetchEvent.request.clone());
})
.catch((error) => {
// If error getting resource from cache, do nothing.
console.log(error);
});
};
// Try to fetch the resource first after checking for the ID token.
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});
self.addEventListener('activate', (event) => {
// Update this list with all caches that need to remain cached.
const cacheWhitelist = ['cache-v1'];
event.waitUntil(caches.keys().then((cacheNames) => {
return Promise.all(cacheNames.map((cacheName) => {
// Check if cache is not whitelisted above.
if (cacheWhitelist.indexOf(cacheName) === -1) {
// If not whitelisted, delete it.
return caches.delete(cacheName);
}
// Allow active service worker to set itself as the controller for all clients
// within its scope. Otherwise, pages won't be able to use it until the next
// load. This makes it possible for the login page to immediately use this.
})).then(() => clients.claim());
}));
});
Next.jsではどう導入すればよいのか。。
Nuxt.jsでは色々あるよ
TypeScriptに置き換えた
service_worker.ts
import '../@types/service_worker';
import 'firebase/auth';
const CACHE_NAME = 'cache-v1';
import { onAuthStateChanged, getIdToken } from 'firebase/auth';
import { firebaseAuth } from 'src/libs/firebase/index';
// idTokenを取得
function getIdTokenPromise(): Promise<unknown> {
try {
return new Promise((resolve, reject) => {
const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => {
unsubscribe();
if (user) {
getIdToken(user).then((idToken) => {
resolve(idToken);
}, (error) => {
resolve(null);
});
} else {
resolve(null);
}
});
});
} catch (error_1) {
console.log(error_1);
}
}
// URLからルートのURLを取得する処理
function getOriginFromUrl(url: String): string {
const pathArray = url.split('/');
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + '//' + host;
}
// Service Workderのライフサイクルでfetchしたときの処理
self.addEventListener('fetch', (event: any) => {
const fetchEvent: FetchEvent = event;
// Get underlying body if available. Works for text and json bodies.
function getBodyContent(req: Request): Promise<string | void | undefined> {
return Promise.resolve().then(() => {
if (req.method !== 'GET') {
if (req.headers.get('Content-Type').indexOf('json') !== -1) {
return req.json()
.then((json: any) => {
return JSON.stringify(json);
});
} else {
return req.text();
}
}
}).catch((error) => {
console.log(error);
});
}
// リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
function requestProcessor(idToken: any): Promise<void | Response> {
let req: Request = event.request;
let processRequestPromise = Promise.resolve();
// URLを取得して、httpsもしくはlocalhostかなどをチェック
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == 'https:' ||
self.location.hostname == 'localhost') &&
idToken) {
// ヘッダ情報をクローンする
const headers = new Headers();
req.headers.forEach((val: string, key: string) => {
headers.append(key, val);
});
// クローンしたヘッダにFirebase AuthのIdTokenを追加
headers.append('Authorization', 'Bearer ' + idToken);
processRequestPromise = getBodyContent(req).then((body: any) => {
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: 'same-origin',
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body,
});
} catch (e) {
console.log(e);
// This will fail for CORS requests. We just continue with the
// fetch caching logic below and do not pass the ID token.
}
});
}
return processRequestPromise.then(() => {
return fetch(req);
}).then((response: Response) => {
// レスポンスが正しくない場合はそのまま返却
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// request を複製する(ストリームは再利用できないので)
const responseToCache = response.clone();
// Save response to cache only for GET requests.
// Cache Storage API does not support using a Request object whose method is
// not 'GET'.
if (req.method === 'GET') {
caches.open(CACHE_NAME).then((cache) => {
// cache に登録する
cache.put(fetchEvent.request, responseToCache);
});
}
// After caching, return response.
return response;
})
.catch((error) => {
// For fetch errors, attempt to retrieve the resource from cache.
return caches.match(fetchEvent.request.clone());
})
.catch((error) => {
// If error getting resource from cache, do nothing.
console.log(error);
});
}
// 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
event.respondWith(getIdTokenPromise().then(requestProcessor, requestProcessor));
});
// Service Workderのライフサイクルでactivateしたときの処理
self.addEventListener('activate', (event: any) => {
const extendebleEvent: ExtendableEvent = event;
// Update this list with all caches that need to remain cached.
const cacheWhitelist = ['cache-v1'];
extendebleEvent.waitUntil(caches.keys().then((cacheNames) => {
return Promise.all(cacheNames.map((cacheName) => {
// キャッシュが登録されてるか確認
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})).then(() => clients.claim());
}));
});
@types/service_worcker.d.ts
/**
* Copyright (c) 2016, Tiernan Cridland
*
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
* granted, provided that the above copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
* Typings for Service Worker
* @author Tiernan Cridland
* @email tiernanc@gmail.com
* @license: ISC
*/
interface Navigator {
serviceWorker: ServiceWorkerContainer;
}
interface ExtendableEvent extends Event {
waitUntil(fn: Promise<any>): void;
}
interface ServiceWorker extends Worker {
scriptURL: string;
state: ServiceWorkerState;
}
interface ServiceWorkerContainer {
controller?: ServiceWorker;
oncontrollerchange?: (event?: Event) => any;
onerror?: (event?: Event) => any;
onmessage?: (event?: Event) => any;
ready: Promise<ServiceWorkerRegistration>;
getRegistration(scope?: string): Promise<ServiceWorkerRegistration>;
getRegistrations(): Promise<Array<ServiceWorkerRegistration>>;
register(url: string, options?: ServiceWorkerRegistrationOptions): Promise<ServiceWorkerRegistration>;
}
interface ServiceWorkerNotificationOptions {
tag?: string;
}
interface ServiceWorkerRegistration {
active?: ServiceWorker;
installing?: ServiceWorker;
onupdatefound?: (event?: Event) => any;
pushManager: PushManager;
scope: string;
waiting?: ServiceWorker;
getNotifications(options?: ServiceWorkerNotificationOptions): Promise<Array<Notification>>;
update(): void;
unregister(): Promise<boolean>;
}
interface ServiceWorkerRegistrationOptions {
scope?: string;
}
type ServiceWorkerState = "installing" | "installed" | "activating" | "activated" | "redundant";
// CacheStorage API
interface Cache {
add(request: Request): Promise<void>;
addAll(requestArray: Array<Request>): Promise<void>;
'delete'(request: Request, options?: CacheStorageOptions): Promise<boolean>;
keys(request?: Request, options?: CacheStorageOptions): Promise<Array<string>>;
match(request: Request, options?: CacheStorageOptions): Promise<Response>;
matchAll(request: Request, options?: CacheStorageOptions): Promise<Array<Response>>;
put(request: Request|string, response: Response): Promise<void>;
}
interface CacheStorage {
'delete'(cacheName: string): Promise<boolean>;
has(cacheName: string): Promise<boolean>;
keys(): Promise<Array<string>>;
match(request: Request, options?: CacheStorageOptions): Promise<Response>;
open(cacheName: string): Promise<Cache>;
}
interface CacheStorageOptions {
cacheName?: string;
ignoreMethod?: boolean;
ignoreSearch?: boolean;
ignoreVary?: boolean;
}
// Client API
interface Client {
frameType: ClientFrameType;
id: string;
url: string;
}
interface Clients {
claim(): Promise<any>;
get(id: string): Promise<Client>;
matchAll(options?: ClientMatchOptions): Promise<Array<Client>>;
openWindow(url: string): Promise<WindowClient>;
}
interface ClientMatchOptions {
includeUncontrolled?: boolean;
type?: ClientMatchTypes;
}
interface WindowClient {
focused: boolean;
visibilityState: WindowClientState;
focus(): Promise<WindowClient>;
navigate(url: string): Promise<WindowClient>;
}
type ClientFrameType = "auxiliary" | "top-level" | "nested" | "none";
type ClientMatchTypes = "window" | "worker" | "sharedworker" | "all";
type WindowClientState = "hidden" | "visible" | "prerender" | "unloaded";
// Fetch API
interface Body {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
text(): Promise<string>;
}
interface FetchEvent extends Event {
request: Request;
respondWith(response: Promise<Response>|Response): Promise<Response>;
}
interface InstallEvent extends ExtendableEvent {
activeWorker: ServiceWorker
}
interface ActivateEvent extends ExtendableEvent {
}
interface Headers {
new(init?: any): Headers;
append(name: string, value: string): void;
'delete'(name: string): void;
entries(): Array<Array<string>>;
get(name: string): string;
getAll(name: string): Array<string>;
has(name: string): boolean;
keys(): Array<string>;
set(name: string, value: string): void;
values(): Array<string>;
}
interface Request extends Body {
new(url: string, init?: {
method?: string,
url?: string,
referrer?: string,
mode?: 'cors'|'no-cors'|'same-origin'|'navigate',
credentials?: 'omit'|'same-origin'|'include',
redirect?: 'follow'|'error'|'manual',
integrity?: string,
cache?: 'default'|'no-store'|'reload'|'no-cache'|'force-cache'
headers?: Headers
}): Request;
cache: RequestCache;
credentials: RequestCredentials;
headers: Headers;
integrity: string;
method: string;
mode: RequestMode;
referrer: string;
referrerPolicy: ReferrerPolicy;
redirect: RequestRedirect;
url: string;
clone(): Request;
}
interface Response extends Body {
new(url: string): Response;
new(body: Blob|BufferSource|FormData|String, init: {
status?: number,
statusText?: string,
headers?: (Headers|{ [k: string]: string })
}): Response;
headers: Headers;
ok: boolean;
redirected: boolean;
status: number;
statusText: string;
type: ResponseType;
url: string;
useFinalURL: boolean;
clone(): Response;
error(): Response;
redirect(): Response;
}
type ReferrerPolicy = "" | "no-referrer" | "no-referrer-when-downgrade" | "origin-only" | "origin-when-cross-origin" |
"unsafe-url";
type RequestCache = "default" | "no-store" | "reload" | "no-cache" | "force-cache";
type RequestCredentials = "omit" | "same-origin" | "include";
type RequestMode = "cors" | "no-cors" | "same-origin" | "navigate";
type RequestRedirect = "follow" | "error" | "manual";
type ResponseType = "basic" | "cores" | "error" | "opaque";
// Notification API
interface Notification {
body: string;
data: any;
icon: string;
lang: string;
requireInteraction: boolean;
silent: boolean;
tag: string;
timestamp: number;
title: string;
close(): void;
requestPermission(): Promise<string>;
}
interface NotificationEvent {
action: string;
notification: Notification;
}
// Push API
interface PushEvent extends ExtendableEvent {
data: PushMessageData;
}
interface PushManager {
getSubscription(): Promise<PushSubscription>;
permissionState(): Promise<string>;
subscribe(): Promise<PushSubscription>;
}
interface PushMessageData {
arrayBuffer(): ArrayBuffer;
blob(): Blob;
json(): any;
text(): string;
}
interface PushSubscription {
endpoint: string;
getKey(method: string): ArrayBuffer;
toJSON(): string;
unsubscribe(): Promise<boolean>;
}
// Sync API
interface SyncEvent extends Event {
lastChance: boolean;
tag: string;
}
// ServiceWorkerGlobalScope
declare var Headers: Headers;
declare var Response: Response;
declare var Request: Request;
declare var caches: CacheStorage;
declare var clients: Clients;
declare var onactivate: (event?: ExtendableEvent) => any;
declare var onfetch: (event?: FetchEvent) => any;
declare var oninstall: (event?: ExtendableEvent) => any;
declare var onmessage: (event: MessageEvent) => any;
declare var onnotificationclick: (event?: NotificationEvent) => any;
declare var onnotificationclose: (event?: NotificationEvent) => any;
declare var onpush: (event?: PushEvent) => any;
declare var onpushsubscriptionchange: () => any;
declare var onsync: (event?: SyncEvent) => any;
declare var registration: ServiceWorkerRegistration;
declare function fetch(request: Request|string): Promise<Response>;
declare function skipWaiting(): void;
Next.jsでService Workerを使う
- public /service_worker.jsに作る
tsではできないらしい。。
tokenが取れない
ReferenceError: onAuthStateChanged is not defined
onAuthStateChangedがないよとおこられる
next-pwaを導入でエラーは出なくなったけど、headerにauthが入らない
issueがたってた
参考
-
worker/index.ts
を作って、custom service workerを書く。 -
next.config.js
を下記のように変更
const withPWA = require("next-pwa");
const runtimeCaching= require("next-pwa/cache");
module.exports = withPWA({
pwa: {
dest: 'public',
runtimeCaching
}
})
一応これでbuildは通ったけど、、
publicファイルにfirebase-service-worker.js
ファイルを作る
public/firebase-service-worker.js
importScripts('https://www.gstatic.com/firebasejs/9.1.3/firebase-app-compat.js')
importScripts('https://www.gstatic.com/firebasejs/9.1.3/firebase-auth-compat.js')
importScripts('/swenv.js')
const CACHE_NAME = 'cache-v1'
// Initialize the Firebase app in the service worker script.
firebase.initializeApp(swEnv)
const auth = firebase.auth()
// idTokenを取得
const getIdTokenPromise = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged((user) => {
unsubscribe()
if (user) {
user.getIdToken().then(
(idToken) => {
resolve(idToken)
},
(e) => {
resolve(null)
}
)
} else {
resolve(null)
}
})
})
}
// URLからルートのURLを取得する処理
const getOriginFromUrl = (url) => {
const pathArray = url.split('/')
const protocol = pathArray[0]
const host = pathArray[2]
return protocol + '//' + host
}
// Service Workderのライフサイクルでfetchしたときの処理
self.addEventListener('fetch', (event) => {
const fetchEvent = event
// Get underlying body if available. Works for text and json bodies.
function getBodyContent(req) {
return Promise.resolve()
.then(() => {
if (req.method !== 'GET') {
if (req.headers.get('Content-Type').indexOf('json') !== -1) {
return req.json().then((json) => {
return JSON.stringify(json)
})
} else {
return req.text()
}
}
})
.catch((error) => {
console.log(error)
})
}
// リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
function requestProcessor(idToken) {
let req = event.request
let processRequestPromise = Promise.resolve()
// URLを取得して、httpsもしくはlocalhostかなどをチェック
console.log(idToken);
if (
self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == 'https:' ||
self.location.hostname == 'localhost') &&
idToken
) {
// ヘッダ情報をクローンする
const headers = new Headers()
req.headers.forEach((val, key) => {
headers.append(key, val)
})
// クローンしたヘッダにFirebase AuthのIdTokenを追加
headers.append('Authorization', 'Bearer ' + idToken)
processRequestPromise = getBodyContent(req).then((body) => {
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: 'same-origin',
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body,
})
} catch (e) {
console.log(e)
// This will fail for CORS requests. We just continue with the
// fetch caching logic below and do not pass the ID token.
}
})
}
return processRequestPromise
.then(() => {
return fetch(req)
})
.then((response) => {
// レスポンスが正しくない場合はそのまま返却
if (!response || response.status !== 200 || response.type !== 'basic') {
return response
}
// request を複製する(ストリームは再利用できないので)
const responseToCache = response.clone()
// Save response to cache only for GET requests.
// Cache Storage API does not support using a Request object whose method is
// not 'GET'.
if (req.method === 'GET') {
caches.open(CACHE_NAME).then((cache) => {
// cache に登録する
cache.put(fetchEvent.request, responseToCache)
})
}
// After caching, return response.
return response
})
.catch((error) => {
// For fetch errors, attempt to retrieve the resource from cache.
return caches.match(fetchEvent.request.clone())
})
.catch((error) => {
// If error getting resource from cache, do nothing.
console.log(error)
})
}
// 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
event.respondWith(
getIdTokenPromise().then(requestProcessor, requestProcessor)
)
})
// Service Workderのライフサイクルでactivateしたときの処理
self.addEventListener('activate', (event) => {
const extendebleEvent = event
// Update this list with all caches that need to remain cached.
const cacheWhitelist = ['cache-v1']
extendebleEvent.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// キャッシュが登録されてるか確認
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName)
}
})
).then(() => clients.claim())
})
)
})
環境変数の設定はこの記事を参考にした
next-pwaを使わず、一応これでも呼び出しはできているみたい
Next.js x FirebaseAuth x Service Workerの実装
誰かできた人いたらコメントとかで教えて下さい。。