374 lines
15 KiB
HTML
374 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<title>Azure Static Web Apps Emulator - Authentication Portal</title>
|
|
<link rel="shortcut icon" href="https://appservice.azureedge.net/images/static-apps/v3/favicon.svg" type="image/x-icon" />
|
|
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/4.1.1/css/bootstrap.min.css" crossorigin="anonymous" />
|
|
<style>
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
}
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.container-height {
|
|
flex: 1 0 auto;
|
|
}
|
|
footer {
|
|
flex-shrink: 0;
|
|
width: 100%;
|
|
background-color: #f5f5f5;
|
|
left: -1px;
|
|
padding: 10px;
|
|
display: flex;
|
|
font-size: 0.8em;
|
|
justify-content: space-between;
|
|
}
|
|
footer nav {
|
|
display: flex;
|
|
}
|
|
footer ul {
|
|
list-style: none;
|
|
display: flex;
|
|
}
|
|
footer li {
|
|
margin: 0 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-light">
|
|
<div class="navbar-brand">
|
|
<div class="container pl-4 ml-5">
|
|
<img src=https://appservice.azureedge.net/images/static-apps/v3/microsoft_azure_logo.svg width="270" height="108" alt />
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div class="container-fluid container-height mr-1">
|
|
<div class="row">
|
|
<div class="row col-xs-12 col-sm-12 d-block d-lg-none d-xl-none d-md-block d-sm-block d-xs-block">
|
|
<div class="text-center"><img src=https://appservice.azureedge.net/images/static-apps/v3/staticapps.svg /></div>
|
|
</div>
|
|
<div
|
|
class="
|
|
extra-pl-small-scr
|
|
offset-xl-1 offset-lg-1 offset-md-2 offset-sm-2 offset-xs-4
|
|
col-xl-5 col-lg-5 col-md-10 col-sm-11 col-xs-11
|
|
div-vertical-center
|
|
"
|
|
>
|
|
<div class="container-fluid">
|
|
<h1 class="pb-4">Azure Static Web Apps Auth</h1>
|
|
<form action="" name="auth">
|
|
<div class="form-group row">
|
|
<label for="identityProvider" class="col-4 col-form-label">Provider</label>
|
|
<div class="col-8">
|
|
<input
|
|
disabled
|
|
id="identityProvider"
|
|
name="identityProvider"
|
|
list="providers-list"
|
|
placeholder="Choose an auth provider"
|
|
type="text"
|
|
required="required"
|
|
class="form-control"
|
|
/>
|
|
<small id="identityProviderHelpBlock" class="form-text text-muted">Name of the identity provider</small>
|
|
</div>
|
|
<datalist id="providers-list">
|
|
<option value="aad"></option>
|
|
<option value="google"></option>
|
|
<option value="github"></option>
|
|
<option value="twitter"></option>
|
|
<option value="facebook"></option>
|
|
<option value="openid"></option>
|
|
</datalist>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label for="userId" class="col-4 col-form-label">User ID</label>
|
|
<div class="col-8">
|
|
<input
|
|
id="userId"
|
|
name="userId"
|
|
autocomplete="off"
|
|
placeholder="Choose a user ID"
|
|
type="text"
|
|
aria-describedby="userIdHelpBlock"
|
|
required="required"
|
|
class="form-control"
|
|
/>
|
|
<small id="userIdHelpBlock" class="form-text text-muted">An Azure Static Web Apps-specific unique ID for the user</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label for="userDetails" class="col-4 col-form-label">Username</label>
|
|
<div class="col-8">
|
|
<input
|
|
id="userDetails"
|
|
name="userDetails"
|
|
autocomplete="off"
|
|
placeholder="Choose a username"
|
|
type="text"
|
|
aria-describedby="userDetailsHelpBlock"
|
|
required="required"
|
|
class="form-control"
|
|
/>
|
|
<small id="userDetailsHelpBlock" class="form-text text-muted">Username or email address of the user</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label for="userRoles" class="col-4 col-form-label">User's roles</label>
|
|
<div class="col-8">
|
|
<textarea id="userRoles" name="userRoles" cols="40" rows="3" class="form-control" aria-describedby="userRolesHelpBlock"></textarea>
|
|
<small id="userRolesHelpBlock" class="form-text text-muted">Roles used during authorization. One role per line.</small>
|
|
<small id="userRolesHelpBlock" class="form-text text-muted"
|
|
>Note: roles "authenticated" and "anonymous" will be added automatically if not provided.</small
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label for="claims" class="col-4 col-form-label">User's claims</label>
|
|
<div class="col-8">
|
|
<textarea
|
|
id="claims"
|
|
name="claims"
|
|
cols="40"
|
|
rows="3"
|
|
class="form-control"
|
|
aria-describedby="userClaimsHelpBlock"
|
|
placeholder="[{'typ': 'name','val': 'Azure Static Web Apps'}]"
|
|
></textarea>
|
|
<small id="userClaimsHelpBlockError" class="d-none form-text text-muted alert alert-danger">Invalid JSON value!</small>
|
|
<small id="userClaimsHelpBlock" class="form-text text-muted"
|
|
>Claims from the identity provider. JSON array of claims. See
|
|
<a href="https://docs.microsoft.com/azure/static-web-apps/user-information">documentation</a> for example claims.</small
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="col-4"></div>
|
|
<div class="col-8 text-right">
|
|
<button name="submit" id="submit" type="submit" class="btn btn-primary">Login</button>
|
|
<button name="clear" id="clear" type="button" class="btn btn-outline-danger">Clear</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-5 col-lg-5 col-md-12 d-none d-lg-block">
|
|
<div class="text-center">
|
|
<img src="https://appservice.azureedge.net/images/static-apps/v3/staticapps.svg" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<footer>
|
|
<div>Azure Static Web Apps emulator (<a rel="noopener noreferrer" target="_blank" href="https://github.com/Azure/static-web-apps-cli/commit/8d0b7aa">main+sha.8d0b7aa</a>)</div>
|
|
<nav>
|
|
<ul>
|
|
<li><a href="https://github.com/azure/static-web-apps-cli/blob/master/CONTRIBUTING.md">Contribute</a></li>
|
|
<li><a href="https://docs.microsoft.com/azure/static-web-apps/">Documentation</a></li>
|
|
<li><a href="https://privacy.microsoft.com/privacystatement">Privacy & Cookies</a></li>
|
|
<li><a href="https://www.microsoft.com/legal/intellectualproperty/copyright">Terms of Use</a></li>
|
|
</ul>
|
|
</nav>
|
|
<div class="copyright">© Microsoft 2023</div>
|
|
</footer>
|
|
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.2.1.min.js" crossorigin="anonymous"></script>
|
|
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/4.1.1/bootstrap.min.js" crossorigin="anonymous"></script>
|
|
<script>
|
|
const defaultRoles = ["anonymous", "authenticated"];
|
|
const defaultClaims = "[]"; // this should be a valid JSON string
|
|
|
|
function saveCookie(formElement) {
|
|
const data = localStorage[hashStorageKey(formElement)];
|
|
document.cookie = `StaticWebAppsAuthCookie=${btoa(data)}; path=/`;
|
|
}
|
|
function redirectToApp() {
|
|
const defaultRedirectPath = "/";
|
|
try {
|
|
const metaSearch = document.querySelector(`meta[name="swa:originalSearch"]`)?.content || "";
|
|
const redirectPath = metaSearch || window.location.search || defaultRedirectPath;
|
|
const urlSearch = (metaSearch || location.search).replace("?", "");
|
|
const urlQuery = urlSearch && Object.fromEntries(new Map(urlSearch.split("&").map((query) => query.split("="))));
|
|
const postLoginRedirectUri = urlQuery ? urlQuery["post_login_redirect_uri"] : redirectPath;
|
|
|
|
window.location.href = decodeURIComponent(postLoginRedirectUri) || defaultRedirectPath;
|
|
} catch (error) {
|
|
console.warn(error);
|
|
window.location.href = defaultRedirectPath;
|
|
}
|
|
}
|
|
function parseIdentityProviderFromUrl() {
|
|
const pathname = document.querySelector(`meta[name="swa:originalPath"]`)?.content || window.location.pathname;
|
|
if (pathname.includes("/.auth/login")) {
|
|
return pathname.split("/").pop();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
function syncIdentityProviderFromUrl(formElement) {
|
|
// @TODO handle unknown providers
|
|
const identityProviderName = parseIdentityProviderFromUrl();
|
|
const providerElement = $("#identityProvider");
|
|
if (providerElement.val() === "") {
|
|
providerElement.val(identityProviderName);
|
|
providerElement.saveToLocalStorage();
|
|
} else {
|
|
// custom routes: let the user enter their provider
|
|
providerElement.removeAttr("disabled");
|
|
}
|
|
}
|
|
function hashStorageKey(element) {
|
|
const formElement = element.closest("form");
|
|
const providerName = parseIdentityProviderFromUrl();
|
|
const formName = formElement.attr("name");
|
|
return `${formName}@${providerName}`;
|
|
}
|
|
function generateUserId() {
|
|
return [...Array(32)].map(() => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
}
|
|
$.fn.saveToLocalStorage = function saveToLocalStorage(options) {
|
|
const settings = $.extend({}, options);
|
|
|
|
if (typeof Storage == "undefined") {
|
|
console.warn("localStorage is not available. Auth data will not be cached!");
|
|
return false;
|
|
}
|
|
|
|
return this.each(function () {
|
|
const element = $(this);
|
|
const storageKey = hashStorageKey(element);
|
|
|
|
if (!localStorage[storageKey]) {
|
|
localStorage[storageKey] = "{}";
|
|
}
|
|
|
|
const inputName = element.attr("name");
|
|
const inputValue = element.val();
|
|
let data = JSON.parse(localStorage[storageKey]);
|
|
if (inputName === "userRoles") {
|
|
// ensure default roles are included
|
|
data[inputName] = [...new Set([...inputValue.trim().split("\n"), ...defaultRoles])];
|
|
} else if (inputName === "claims") {
|
|
const claimsErrorBlock = $("#userClaimsHelpBlockError");
|
|
const formSubmitBtn = $("#submit");
|
|
try {
|
|
claimsErrorBlock.addClass("d-none");
|
|
formSubmitBtn.prop("disabled", false);
|
|
data[inputName] = JSON.parse(inputValue);
|
|
} catch (error) {
|
|
claimsErrorBlock.removeClass("d-none");
|
|
formSubmitBtn.prop("disabled", true);
|
|
data[inputName] = JSON.parse(defaultClaims);
|
|
}
|
|
} else {
|
|
data[inputName] = inputValue;
|
|
}
|
|
localStorage[storageKey] = JSON.stringify(data);
|
|
});
|
|
};
|
|
$.fn.syncFromLocalStorage = function syncFromLocalStorage(options) {
|
|
const settings = $.extend({}, options);
|
|
|
|
return this.each(function () {
|
|
const element = $(this);
|
|
const storageKey = hashStorageKey(element);
|
|
let data = {};
|
|
|
|
if (typeof Storage == "undefined") {
|
|
console.warn("localStorage is not available. Auth data will not be cached!");
|
|
return false;
|
|
}
|
|
|
|
if (localStorage[storageKey]) {
|
|
data = JSON.parse(localStorage[storageKey]);
|
|
if (!data) {
|
|
// initialize localStorage to empty object
|
|
localStorage[storageKey] = JSON.stringify({});
|
|
data = JSON.parse(localStorage[storageKey]);
|
|
}
|
|
}
|
|
|
|
element.find("input, textarea").each(function () {
|
|
const input = $(this);
|
|
const inputName = input.attr("name");
|
|
if (inputName === "userRoles") {
|
|
const userRoles = data[inputName];
|
|
if (Array.isArray(userRoles)) {
|
|
input.val(userRoles.join("\n"));
|
|
} else if (input.val() === "") {
|
|
// set default values and cache entry
|
|
input.val(settings.defaultRoles.join("\n"));
|
|
input.saveToLocalStorage();
|
|
}
|
|
} else if (inputName === "identityProvider") {
|
|
// don't restore provider name from storage.
|
|
// this value will be taken from the URL
|
|
} else if (inputName === "userId") {
|
|
const userId = data[inputName] || generateUserId();
|
|
input.val(userId);
|
|
input.saveToLocalStorage();
|
|
} else if (inputName === "claims") {
|
|
const userClaims = data[inputName];
|
|
if (Array.isArray(userClaims)) {
|
|
input.val(JSON.stringify(userClaims, null, 2));
|
|
} else if (!userClaims || input.val() === "") {
|
|
// set default values and cache entry
|
|
input.val(settings.defaultClaims);
|
|
input.saveToLocalStorage();
|
|
}
|
|
} else if (input.attr("type") !== "submit") {
|
|
const value = data[inputName];
|
|
input.val(value);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
</script>
|
|
<script>
|
|
$(function () {
|
|
// setup default values
|
|
const form = $("form[name='auth']");
|
|
|
|
// trigger on first call
|
|
form.syncFromLocalStorage({
|
|
defaultRoles,
|
|
defaultClaims,
|
|
});
|
|
|
|
// get default provider name from URL
|
|
syncIdentityProviderFromUrl(form);
|
|
|
|
// delegate storing auth info to each input
|
|
form.on("keyup", "input, textarea", (event) => $(event.currentTarget).saveToLocalStorage());
|
|
|
|
// clear form
|
|
$("#clear").click(() => {
|
|
form.find("input[type=text]:not(#identityProvider)").val("");
|
|
form.find("textarea#userRoles").val(defaultRoles.join("\n"));
|
|
form.find("textarea#claims").val(defaultClaims);
|
|
form.find("#userId").val(generateUserId());
|
|
form.find("input, textarea").each((idx, element) => $(element).saveToLocalStorage());
|
|
});
|
|
|
|
// store cookie info as base64
|
|
form.submit(() => {
|
|
event.preventDefault();
|
|
saveCookie(form);
|
|
redirectToApp();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|