login.tailscale.com
Open in
urlscan Pro
2a05:d014:386:202:2697:66bf:c24a:a674
Public Scan
Submitted URL: https://tailscale.com/admin
Effective URL: https://login.tailscale.com/login?next_url=%2Fadmin
Submission: On September 26 via manual from US — Scanned from DE
Effective URL: https://login.tailscale.com/login?next_url=%2Fadmin
Submission: On September 26 via manual from US — Scanned from DE
Form analysis
6 forms found in the DOMPOST
<form method="POST">
<input type="email" autofocus="" required="" id="user" name="user" placeholder="Enter your email..." value="">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<button name="submitbtn" type="submit">Sign in</button>
</form>
POST
<form method="POST">
<input type="hidden" name="provider" value="google">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<button name="submitbtn" type="submit" class="button-logo button-sso">
<svg width="19" height="18" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 8.18371C17 7.63989 16.9551 7.09314 16.8591 6.55814H9.16046V9.63879H13.5691C13.3862 10.6324 12.7983 11.5113 11.9376 12.0698V14.0687H14.5678C16.1123 12.6754 17 10.6177 17 8.18371Z" fill="#4285F4"></path>
<path d="M9.16042 16C11.3617 16 13.2182 15.2916 14.5707 14.0687L11.9406 12.0698C11.2088 12.5578 10.2641 12.8341 9.16342 12.8341C7.03409 12.8341 5.22865 11.4261 4.58085 9.53299H1.8667V11.5936C3.25227 14.2951 6.07438 16 9.16042 16V16Z"
fill="#34A853"></path>
<path d="M4.57786 9.53298C4.23596 8.53941 4.23596 7.46353 4.57786 6.46996V4.40933H1.8667C0.709065 6.66985 0.709065 9.33309 1.8667 11.5936L4.57786 9.53298V9.53298Z" fill="#FBBC04"></path>
<path
d="M9.16042 3.16589C10.3241 3.14825 11.4487 3.57743 12.2914 4.36523L14.6217 2.0812C13.1462 0.72312 11.1878 -0.0235267 9.16042 -1.02057e-05C6.07438 -1.02057e-05 3.25227 1.70493 1.8667 4.40932L4.57785 6.46995C5.22265 4.57394 7.03109 3.16589 9.16042 3.16589V3.16589Z"
fill="#EA4335"></path>
</svg> Sign in with Google </button>
</form>
POST
<form method="POST">
<input type="hidden" name="provider" value="microsoft">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<button name="submitbtn" type="submit" class="button-logo button-sso">
<svg width="17" height="17" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H7.57886V7.57886H0V0Z" fill="#F25022"></path>
<path d="M0 8.42114H7.57886V16H0V8.42114Z" fill="#00A4EF"></path>
<path d="M8.42114 0H16V7.57886H8.42114V0Z" fill="#7FBA00"></path>
<path d="M8.42114 8.42114H16V16H8.42114V8.42114Z" fill="#FFB900"></path>
</svg> Sign in with Microsoft </button>
</form>
POST
<form method="POST" id="githubForm">
<input type="hidden" name="provider" value="github">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<button name="submitbtn" type="submit" class="button-logo button-sso">
<svg width="19" height="19" viewBox="0 0 16 16" version="1.1">
<path fill-rule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
</path>
</svg> Sign in with GitHub </button>
</form>
POST
<form method="POST" id="appleForm">
<input type="hidden" name="provider" value="apple">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<button name="submitbtn" type="submit" class="button-logo button-sso">
<svg width="16px" height="16px" viewBox="0 0 56 56">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="matrix(2.7742681,0,0,2.7742681,-49.798152,-43.154083)">
<rect id="Rectangle" x="6" y="6" width="44" height="44"></rect>
<path
d="m 28.222656,20.384615 c 0.832031,0 1.875,-0.579784 2.496094,-1.352829 0.5625,-0.700572 0.972656,-1.678957 0.972656,-2.657342 0,-0.132867 -0.01172,-0.265735 -0.03516,-0.374444 -0.925781,0.03624 -2.039063,0.640178 -2.707031,1.44946 -0.527344,0.61602 -1.007813,1.582326 -1.007813,2.57279 0,0.144946 0.02344,0.289892 0.03516,0.338208 0.05859,0.01208 0.152344,0.02416 0.246094,0.02416 z M 25.292969,35 c 1.136718,0 1.640625,-0.785124 3.058593,-0.785124 1.441407,0 1.757813,0.760966 3.023438,0.760966 1.242187,0 2.074219,-1.183725 2.859375,-2.343293 0.878906,-1.328671 1.242187,-2.633185 1.265625,-2.693579 -0.08203,-0.02416 -2.460938,-1.026701 -2.460938,-3.841068 0,-2.439924 1.875,-3.539097 1.980469,-3.623649 -1.242187,-1.835982 -3.128906,-1.884298 -3.644531,-1.884298 -1.394531,0 -2.53125,0.869676 -3.246094,0.869676 -0.773437,0 -1.792969,-0.82136 -3,-0.82136 -2.296875,0 -4.628906,1.95677 -4.628906,5.652892 0,2.294978 0.867187,4.722823 1.933594,6.293071 C 23.347656,33.912905 24.144531,35 25.292969,35 Z"
fill="#000000" fill-rule="nonzero"></path>
</g>
</svg> Sign in with Apple </button>
</form>
POST
<form method="POST" id="passkeyForm">
<input type="hidden" name="provider" value="passkey">
<input type="hidden" name="next_url" value="/admin">
<input type="hidden" name="gorilla.csrf.Token" value="POFR+bdVXEl9kDR5WfmpvXBYXRuE8ELTNWlYfS9lr+GBORioMNQ6zur7L+aO3lY9uy3sglQYjvqxqS6v39NmNw==">
<input type="hidden" name="passkey_credentials">
<button name="passkey_sign_in" type="submit" class="button-logo button-sso">
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489 475" width="16" height="16">
<style>
.a {
fill: #010101
}
.b {
fill: #fcfcfc
}
</style>
<path
d="m326 348c0 37.2-0.1 73.8 0.1 110.5 0 3.6-0.8 4.6-4.6 4.6q-157.9-0.2-315.9 0c-3.4 0-4.8-0.4-4.7-4.5 0.7-20.8-1.3-41.7 1-62.4 5-43 25.1-77.7 59.7-103.7 26.6-20 56.8-29.7 90.3-29.5 25.9 0.1 51.9-0.1 77.9 0 19.1 0.1 37.2 4.5 54.8 11.8 2 0.9 3.1 2.1 4 4 8.3 17.2 19.7 32 34.6 43.9 2 1.6 2.9 3.2 2.9 5.8-0.2 6.3-0.1 12.7-0.1 19.5z">
</path>
<path class="a" d="m300.9 104.4c1.7 48.7-17.9 85.7-60.6 108.4-67.8 36-150.6-6-163.1-81.8-10.3-62.5 34.5-122.4 97.2-129.9 60.6-7.2 115.4 33.8 125.4 93.9 0.5 2.9 0.7 5.9 1.1 9.4z"></path>
<path class="a"
d="m348.4 155.3c34.5-24.2 77.3-23.1 108.5 2.7 48.9 40.4 39.7 118-17.3 146-2.1 1-4.1 2-7.1 3.4 9.4 9.2 18.4 18.3 27.6 27 3.1 2.8 3 4.4 0 7.2-10.5 10.2-20.7 20.8-31.2 31-2.2 2.2-2.5 3.3-0.1 5.7 10.7 10.4 21.2 21.2 31.9 31.7 2.6 2.5 2.3 3.8-0.1 6.1-18.8 18.7-37.6 37.4-56.2 56.3-2.8 2.8-4.3 2.5-6.9-0.1q-15.2-15.6-30.7-30.8c-2.5-2.4-3.4-4.9-3.4-8.3 0.1-41.3 0.1-82.7 0.1-124 0-3-0.5-5-3.6-6.7-56.8-31-62.8-106.6-11.5-147.2z">
</path>
<path class="b" d="m386.5 220.6c-15.5-13.3-13.1-34.9 4.5-42.8 10.4-4.7 22.9-1.4 29.9 7.8 6.8 8.9 6.6 21.7-0.5 30.5-7.3 8.9-19.4 11.7-29.7 6.9-1.4-0.6-2.6-1.4-4.2-2.4z"></path>
</svg> Sign in with a passkey </button>
<input type="hidden" name="passkey_challenge" value="Pkq249VsYSGzCQF/ov2pT/s25IgbPEby">
<input type="hidden" name="passkey_user_id" value="">
<script>
(async function init() {
const usernameMinLength = 3;
const usernameMaxLength = 63;
const scriptTag = document.currentScript;
const $name = (name) => scriptTag.closest("form").querySelector(`[name='${name}']`);
const signInButton = $name("passkey_sign_in");
const signUpButton = $name("passkey_sign_up");
const button = signInButton || signUpButton;
const usernameInput = $name("passkey_username");
const userId = $name("passkey_user_id")?.value;
const challenge = $name("passkey_challenge")?.value;
const credentialsInput = $name("passkey_credentials");
const csrfToken = $name("gorilla.csrf.Token")?.getAttribute("value");
if (supported()) {
let el = document.getElementById("passkey-not-supported");
el?.remove();
} else {
let el = document.getElementById("passkey-not-supported");
el.style.display = "block";
el = document.getElementById("passkey-supported");
el.remove();
}
let invalid = false;
if (!challenge) {
invalid = true;
showError("Invalid form data from the server.");
console.error("No passkey challenge found in the form.");
}
if (signInButton) {
signInButton.disabled = invalid;
signInButton.addEventListener("click", async function(event) {
event.preventDefault();
try {
const credentials = await signIn();
credentialsInput.value = serializeCredentials(credentials);
event.target.form.submit();
} catch (e) {
showError("Passkey sign in failed, please try again.");
console.error(e);
}
});
}
if (signUpButton) {
signUpButton.disabled = invalid;
signUpButton.addEventListener("click", async function(event) {
event.preventDefault();
try {
if (!usernameInput.checkValidity()) {
usernameInput.reportValidity();
return;
}
const username = getUsername();
const available = await fetchCheckUsername(username);
if (!available) {
throw new Error("Username is not available");
return;
}
const credentials = await signUp();
credentialsInput.value = serializeCredentials(credentials);
event.target.form.submit();
} catch (e) {
showError("Passkey sign up failed, please try again.");
console.error(e);
}
});
}
if (usernameInput) {
const checkDebouncer = debounce(500);
const validationDebouncer = debounce(100);
usernameInput.setAttribute("minlength", usernameMinLength);
usernameInput.setAttribute("maxlength", usernameMaxLength);
usernameInput.setAttribute("required", "required");
usernameInput.setAttribute("pattern", "^[a-z][a-z0-9-]{1,61}[a-z0-9]$");
usernameInput.setAttribute("title", "At least 3 characters, start with a letter, end with a letter or number, and contain only lowercase letters, numbers and hyphens.");
let lastValue;
usernameInput.addEventListener("input", function(e) {
const username = getUsername();
(() => {
if (username === lastValue) {
return;
}
if (username.length === 0) {
return;
}
if (username.length < usernameMinLength) {
return;
}
checkDebouncer(() => checkUsername(username));
})();
lastValue = username;
validationDebouncer(() => {
usernameInput.setCustomValidity("");
usernameInput.removeAttribute("aria-invalid");
setTimeout(() => {
usernameInput.blur();
usernameInput.focus();
}, 0);
});
});
}
function supported() {
return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
}
function debounce(delay) {
let timeout;
return function(callback) {
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
}
function cleanUsername(username) {
if (!username) {
return "";
}
username = username.trim();
username = username.substring(0, usernameMaxLength);
username = username.toLowerCase();
if (!username[0].match(/[a-z]/)) {
username = username.substring(1);
}
username = username.replace(/[^a-z0-9-]/g, "");
return username;
}
function getUsername() {
let username = usernameInput.value;
username = cleanUsername(username);
usernameInput.value = username;
return username;
}
async function checkUsername() {
if (!usernameInput.checkValidity()) {
return;
}
await (async () => {
const username = getUsername();
if (!username) {
return;
}
try {
const available = await fetchCheckUsername(username);
if (available) {
usernameInput.removeAttribute("aria-invalid");
} else {
usernameInput.setAttribute("aria-invalid", true);
}
usernameInput.setCustomValidity(available ? "" : "Username is not available");
} catch (e) {
usernameInput.setCustomValidity(e);
usernameInput.setAttribute("aria-invalid", true);
}
})();
usernameInput.reportValidity();
}
async function fetchCheckUsername(username) {
if (!csrfToken) {
throw new Error("Missing CSRF token. Please refresh the page.");
}
const res = await fetch("/admin/api/passkey/check-username", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({
username
}),
});
let data, error;
try {
const j = await res.clone().json();
data = j.data;
error = j.error;
} catch (e) {
throw new Error(await res.text());
}
if (!res.ok) {
let errorMsg = error || `Error checking username availability: ${res.status}`;
throw new Error(errorMsg);
}
return !!data?.available;
}
function showError(message) {
let msgSpan = document.querySelector("div#error-box p.error-message");
if (!msgSpan) {
const tmpl = document.querySelector("template#error-box-tmpl").content;
const clone = document.importNode(tmpl, true);
const logoEl = document.querySelector("div.logo");
if (logoEl) {
logoEl.after(clone);
} else {
button?.parentElement.before(clone);
}
msgSpan = document.querySelector("div#error-box p.error-message");
}
msgSpan.textContent = message;
}
function removeError() {
document.querySelector("div#error-box")?.remove();
}
async function signIn() {
const challengeBuf = Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer;
return await navigator.credentials.get({
publicKey: {
challenge: challengeBuf,
rpId: window.location.host,
timeout: 60_000,
userVerification: "required",
},
});
}
async function signUp() {
const username = getUsername();
if (!username) {
showError("No username provided.");
console.error("No username provided.");
return;
}
if (!userId) {
invalid = true;
showError("Invalid form data from the server.");
console.error("No user ID found in the form.");
return;
}
const challengeBuf = Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer;
return await navigator.credentials.create({
publicKey: {
challenge: challengeBuf,
rp: {
name: "tailscale.com",
id: window.location.host,
},
user: {
id: new TextEncoder().encode(userId),
name: username,
displayName: username,
},
pubKeyCredParams: [{
type: "public-key",
alg: -7,
}, {
type: "public-key",
alg: -35,
}, {
type: "public-key",
alg: -36,
}, {
type: "public-key",
alg: -257,
}, {
type: "public-key",
alg: -258,
}, {
type: "public-key",
alg: -259,
}, {
type: "public-key",
alg: -37,
}, {
type: "public-key",
alg: -38,
}, {
type: "public-key",
alg: -39,
}, {
type: "public-key",
alg: -8,
}, ],
authenticatorSelection: {
requireResidentKey: true,
residentKey: "required",
userVerification: "required",
},
timeout: 60_000,
attestation: "none",
},
});
}
function base64URL(v) {
let base64Std = btoa(String.fromCharCode(...new Uint8Array(v)));
return base64Std.replaceAll("+", "-").replaceAll("/", "_");
}
function serializeCredentials(credentials) {
const {
id,
rawId,
response,
type
} = credentials;
const {
attestationObject,
authenticatorData,
signature,
userHandle,
clientDataJSON,
} = response;
return JSON.stringify({
id,
type,
rawId: base64URL(rawId),
response: {
attestationObject: base64URL(attestationObject),
clientDataJSON: base64URL(clientDataJSON),
authenticatorData: base64URL(authenticatorData),
signature: base64URL(signature),
userHandle: base64URL(userHandle),
},
});
}
})();
</script>
<template id="error-box-tmpl">
<div id="error-box" class="error-box error text-sm">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<h3 class="font-medium">Error</h3>
</div>
<p aria-live="polite" class="error-message"></p>
</div>
</template>
<style>
div.username {
position: relative;
display: flex;
padding: 0;
}
div.username input {
padding: 8px;
width: 66%;
border-style: none;
}
div.username input:focus:not(.focus-visible) {
outline: none;
}
div.username span {
pointer-events: none;
user-select: none;
display: inline-block;
text-align: center;
flex-grow: 1;
padding: 8px;
color: #706e6d;
border-left: 1px solid rgba(0, 0, 0, 0.05);
}
</style>
</form>
Text Content
Sign in or Sign in with Google Sign in with Microsoft Sign in with GitHub Sign in with Apple Sign in with a passkey ERROR First time? Learn more at tailscale.com. By clicking the buttons above, you acknowledge that you have read, understood, and agree to Tailscale’s Terms of Service and Privacy Policy.