Level 0: What does “login using Android PIN” mean?
It does not mean that the web page asks the user to type the Android PIN into an HTML input box. That would be unsafe and Android does not allow it.
The correct meaning is this:
Simple classroom example
Imagine a school gate. The class teacher does not ask for your house key. The gatekeeper checks your identity and tells the teacher, “Yes, this is the right student.” Similarly, the website does not get your Android PIN. Android checks it and tells the browser whether the authentication succeeded.
Level 1: Why plain HTML cannot do this
HTML can create forms, buttons, inputs, and layouts. JavaScript can respond to clicks and run browser APIs. But the Android PIN belongs to the operating system security layer. A normal website must not be allowed to read it.
Create an input field and ask the user to enter the Android PIN.
This is not real Android PIN login. It is only a normal password field.
Use WebAuthn and passkeys.
Android verifies the user locally and the website receives cryptographic proof.
Bad example
<input type="password" placeholder="Enter Android PIN">
This is not Android PIN login. This only creates a password box. The browser has no permission to compare this value with the real Android screen lock.
Level 2: WebAuthn, passkeys, and platform authenticators
WebAuthn means Web Authentication. It allows websites to use public-key credentials for secure login. In normal passwords, the same secret is typed by the user and stored or verified by the server. In WebAuthn, the private key stays with the authenticator, and the server stores a public key.
| Term | Meaning |
|---|---|
| WebAuthn | Web Authentication API used for passwordless and strong login. |
| Passkey | A user-friendly credential that can be unlocked using screen lock or biometrics. |
| Platform authenticator | The built-in authenticator on the device, such as Android device security. |
| Relying Party | The website or app that wants to authenticate the user. |
| Challenge | A random value generated during registration or login to prevent replay attacks. |
| Public key | Stored by the server and used to verify signatures. |
| Private key | Kept safely by the authenticator. The website never receives it. |
The registration flow
- User clicks “Register Android PIN Login”.
- Website creates a random challenge.
- Browser calls
navigator.credentials.create(). - Android asks for PIN, pattern, password, fingerprint, or face unlock.
- Authenticator creates a key pair.
- Website stores the credential ID and public key.
The login flow
- User clicks “Login Using Android PIN”.
- Website creates a fresh challenge.
- Browser calls
navigator.credentials.get(). - Android verifies the user locally.
- Authenticator signs the challenge using the private key.
- Server verifies the signature using the stored public key.
Level 3: Working browser-side demo
This is a learning demo. It stores only a simplified credential reference in localStorage.
A production website must use a server to generate challenges and verify responses.
Level 3A: JavaScript used in the demo
The important browser objects are PublicKeyCredential,
navigator.credentials.create(), and
navigator.credentials.get().
const statusBox = document.getElementById("status");
const usernameInput = document.getElementById("username");
function setStatus(message, type = "") {
statusBox.innerHTML = type
? `<span class="${type}">${message}</span>`
: message;
}
function bufferToBase64Url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function base64UrlToBuffer(base64url) {
const base64 = base64url
.replace(/-/g, "+")
.replace(/_/g, "/");
const padded = base64.padEnd(
base64.length + (4 - base64.length % 4) % 4,
"="
);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function randomChallenge(length = 32) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array;
}
async function checkSupport() {
if (!window.PublicKeyCredential) {
throw new Error("WebAuthn is not supported in this browser.");
}
const available =
await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
if (!available) {
throw new Error(
"No user-verifying platform authenticator was found. Set a PIN, pattern, password, or biometric lock first."
);
}
}
async function registerPasskey() {
try {
await checkSupport();
const username = usernameInput.value.trim();
if (!username) {
setStatus("Please enter a username.", "bad");
return;
}
const userId = new TextEncoder().encode(username);
const publicKeyOptions = {
challenge: randomChallenge(),
rp: {
name: "Learn With Champak Demo"
},
user: {
id: userId,
name: username,
displayName: username
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 }
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
residentKey: "preferred"
},
timeout: 60000,
attestation: "none"
};
setStatus("Android will now ask for screen lock confirmation...");
const credential = await navigator.credentials.create({
publicKey: publicKeyOptions
});
const savedCredential = {
id: credential.id,
rawId: bufferToBase64Url(credential.rawId),
type: credential.type,
username
};
localStorage.setItem("androidPinCredential", JSON.stringify(savedCredential));
setStatus("Android PIN login registered successfully.", "ok");
} catch (error) {
setStatus(error.message || "Registration failed.", "bad");
}
}
async function loginWithPasskey() {
try {
await checkSupport();
const saved = localStorage.getItem("androidPinCredential");
if (!saved) {
setStatus("No saved login found. Register first.", "bad");
return;
}
const credentialData = JSON.parse(saved);
const publicKeyOptions = {
challenge: randomChallenge(),
allowCredentials: [
{
id: base64UrlToBuffer(credentialData.rawId),
type: "public-key",
transports: ["internal"]
}
],
userVerification: "required",
timeout: 60000
};
setStatus("Android will now ask for PIN, pattern, password, fingerprint, or face unlock...");
const assertion = await navigator.credentials.get({
publicKey: publicKeyOptions
});
if (assertion) {
setStatus(`Login successful. Welcome, ${credentialData.username}.`, "ok");
} else {
setStatus("Login failed.", "bad");
}
} catch (error) {
setStatus(error.message || "Login failed.", "bad");
}
}
Level 4: What changes in a real website?
The browser demo is useful for learning, but real authentication needs a backend. The server must create the challenge, store the public key, and verify the signed response.
| Demo page | Real website |
|---|---|
| Challenge generated in browser | Challenge generated on server |
| Credential reference saved in localStorage | Credential ID and public key saved in database |
| Login accepted after browser returns assertion | Server verifies signature, origin, challenge, counter, and user |
| Good for classroom demo | Required for production |
Server-side registration endpoints
POST /webauthn/register/options
Server returns:
{
"challenge": "...",
"rp": "...",
"user": "...",
"pubKeyCredParams": [...]
}
POST /webauthn/register/verify
Browser sends credential response.
Server verifies it and stores the public key.
Server-side login endpoints
POST /webauthn/login/options
Server returns a fresh challenge.
POST /webauthn/login/verify
Browser sends assertion.
Server verifies the signature using the stored public key.
Level 5: Production checklist
Passkeys and WebAuthn require secure browser conditions.
Never trust only browser-side success for real login.
Every registration and login attempt should use a new random challenge.
Verify that the response came from your real domain.
Users may lose a phone, reset Android, or change devices.
During transition, keep email login, password login, or one-time code login.
Common mistakes
- Asking users to type their Android PIN into your own form.
- Saving authentication only in localStorage for a real website.
- Using the same challenge again and again.
- Testing from
file://and thinking the code is broken. - Expecting Android to reveal the PIN to JavaScript.
Level 6: How to explain this to students
Use this simple sentence:
Analogy
Your phone is like a locked cupboard. The website asks, “Can the real owner unlock this cupboard?” Android checks the owner locally. If the owner succeeds, Android gives the website a signed proof. The website never sees the cupboard key.
Practice Quiz
Mini Project
Build a small login page with three buttons:
- Check WebAuthn support.
- Register passkey.
- Login with passkey.
Then improve it by adding a Node.js backend with four routes:
POST /register/options
POST /register/verify
POST /login/options
POST /login/verify
The browser should only start the authentication process. The backend should make the final login decision.