132 lines
4.0 KiB
JavaScript
132 lines
4.0 KiB
JavaScript
import {
|
|
InMemoryWebStorage,
|
|
UserManager,
|
|
WebStorageStateStore,
|
|
} from "oidc-client-ts";
|
|
|
|
import { createPkcePair } from "./pkce.js";
|
|
|
|
const authority = "http://localhost:8080/realms/keycloak-patterns";
|
|
const backendBaseUrl = "http://localhost:8081";
|
|
|
|
const userManager = new UserManager({
|
|
authority,
|
|
client_id: "spa-public",
|
|
redirect_uri: "http://localhost:8088/callback.html",
|
|
post_logout_redirect_uri: "http://localhost:8088/",
|
|
response_type: "code",
|
|
scope: "openid profile email",
|
|
automaticSilentRenew: true,
|
|
monitorSession: false,
|
|
loadUserInfo: false,
|
|
userStore: new WebStorageStateStore({
|
|
store: new InMemoryWebStorage(),
|
|
}),
|
|
stateStore: new WebStorageStateStore({
|
|
store: window.sessionStorage,
|
|
}),
|
|
});
|
|
|
|
const loginButton = document.querySelector("#login");
|
|
const logoutButton = document.querySelector("#logout");
|
|
const apiButton = document.querySelector("#call-api");
|
|
const pkceButton = document.querySelector("#pkce-demo");
|
|
const sessionState = document.querySelector("#session-state");
|
|
const result = document.querySelector("#result");
|
|
|
|
let currentUser = null;
|
|
|
|
function renderJson(value) {
|
|
result.textContent = JSON.stringify(value, null, 2);
|
|
}
|
|
|
|
function tokenMetadata(user) {
|
|
return {
|
|
subject: user.profile.sub,
|
|
username: user.profile.preferred_username,
|
|
expiresAt: new Date(user.expires_at * 1000).toISOString(),
|
|
accessTokenHeldBy: "browser memory",
|
|
refreshTokenHeldBy: user.refresh_token ? "browser memory" : "not issued",
|
|
};
|
|
}
|
|
|
|
function renderSession(user) {
|
|
currentUser = user;
|
|
const authenticated = Boolean(user && !user.expired);
|
|
sessionState.dataset.authenticated = String(authenticated);
|
|
sessionState.textContent = authenticated
|
|
? `${user.profile.preferred_username} 로그인됨 · token은 메모리에만 보관`
|
|
: "로그인되지 않음 · 브라우저 저장소에 token 없음";
|
|
loginButton.disabled = authenticated;
|
|
logoutButton.disabled = !authenticated;
|
|
apiButton.disabled = !authenticated;
|
|
|
|
if (authenticated) {
|
|
renderJson(tokenMetadata(user));
|
|
}
|
|
}
|
|
|
|
async function finishSigninCallback() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const isCallback = window.location.pathname === "/callback.html"
|
|
&& (params.has("code") || params.has("error"));
|
|
|
|
if (!isCallback) {
|
|
return null;
|
|
}
|
|
|
|
const user = await userManager.signinRedirectCallback();
|
|
window.history.replaceState({}, document.title, "/");
|
|
return user;
|
|
}
|
|
|
|
async function callProtectedApi() {
|
|
if (!currentUser || currentUser.expired) {
|
|
throw new Error("로그인이 필요합니다.");
|
|
}
|
|
|
|
const response = await fetch(`${backendBaseUrl}/api/me`, {
|
|
headers: {
|
|
Authorization: `Bearer ${currentUser.access_token}`,
|
|
},
|
|
});
|
|
const body = await response.json();
|
|
renderJson({
|
|
httpStatus: response.status,
|
|
resourceServerResponse: body,
|
|
tokenBoundary: tokenMetadata(currentUser),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`보호 API가 ${response.status}를 반환했습니다.`);
|
|
}
|
|
}
|
|
|
|
loginButton.addEventListener("click", () => userManager.signinRedirect());
|
|
logoutButton.addEventListener("click", () => userManager.signoutRedirect());
|
|
apiButton.addEventListener("click", () => {
|
|
callProtectedApi().catch((error) => renderJson({ error: error.message }));
|
|
});
|
|
pkceButton.addEventListener("click", () => {
|
|
createPkcePair()
|
|
.then(({ verifier, challenge, method }) => renderJson({
|
|
method,
|
|
verifierLength: verifier.length,
|
|
challengeLength: challenge.length,
|
|
note: "실제 로그인에서는 oidc-client-ts가 같은 S256 단계를 수행합니다.",
|
|
}))
|
|
.catch((error) => renderJson({ error: error.message }));
|
|
});
|
|
|
|
userManager.events.addUserLoaded(renderSession);
|
|
userManager.events.addUserUnloaded(() => renderSession(null));
|
|
userManager.events.addAccessTokenExpired(() => renderSession(null));
|
|
|
|
try {
|
|
const callbackUser = await finishSigninCallback();
|
|
renderSession(callbackUser ?? await userManager.getUser());
|
|
} catch (error) {
|
|
renderSession(null);
|
|
renderJson({ error: error.message });
|
|
}
|