mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-11-04 00:28:54 +00:00 
			
		
		
		
	
		
			
	
	
		
			195 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			195 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								/*
							 | 
						||
| 
								 | 
							
								 * Copyright 2002-2024 the original author or authors.
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * 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
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 *      https://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.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								"use strict";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import base64url from "./base64url.js";
							 | 
						||
| 
								 | 
							
								import http from "./http.js";
							 | 
						||
| 
								 | 
							
								import abortController from "./abort-controller.js";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function isConditionalMediationAvailable() {
							 | 
						||
| 
								 | 
							
								  return !!(
							 | 
						||
| 
								 | 
							
								    window.PublicKeyCredential &&
							 | 
						||
| 
								 | 
							
								    window.PublicKeyCredential.isConditionalMediationAvailable &&
							 | 
						||
| 
								 | 
							
								    (await window.PublicKeyCredential.isConditionalMediationAvailable())
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function authenticate(headers, contextPath, useConditionalMediation) {
							 | 
						||
| 
								 | 
							
								  let options;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const optionsResponse = await http.post(`${contextPath}/webauthn/authenticate/options`, headers);
							 | 
						||
| 
								 | 
							
								    if (!optionsResponse.ok) {
							 | 
						||
| 
								 | 
							
								      throw new Error(`HTTP ${optionsResponse.status}`);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    options = await optionsResponse.json();
							 | 
						||
| 
								 | 
							
								  } catch (err) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Authentication failed. Could not fetch authentication options: ${err.message}`, { cause: err });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
							 | 
						||
| 
								 | 
							
								  const decodedOptions = {
							 | 
						||
| 
								 | 
							
								    ...options,
							 | 
						||
| 
								 | 
							
								    challenge: base64url.decode(options.challenge),
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Invoke the WebAuthn get() method.
							 | 
						||
| 
								 | 
							
								  const credentialOptions = {
							 | 
						||
| 
								 | 
							
								    publicKey: decodedOptions,
							 | 
						||
| 
								 | 
							
								    signal: abortController.newSignal(),
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  if (useConditionalMediation) {
							 | 
						||
| 
								 | 
							
								    // Request a conditional UI
							 | 
						||
| 
								 | 
							
								    credentialOptions.mediation = "conditional";
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let cred;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    cred = await navigator.credentials.get(credentialOptions);
							 | 
						||
| 
								 | 
							
								  } catch (err) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const { response, type: credType } = cred;
							 | 
						||
| 
								 | 
							
								  let userHandle;
							 | 
						||
| 
								 | 
							
								  if (response.userHandle) {
							 | 
						||
| 
								 | 
							
								    userHandle = base64url.encode(response.userHandle);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  const body = {
							 | 
						||
| 
								 | 
							
								    id: cred.id,
							 | 
						||
| 
								 | 
							
								    rawId: base64url.encode(cred.rawId),
							 | 
						||
| 
								 | 
							
								    response: {
							 | 
						||
| 
								 | 
							
								      authenticatorData: base64url.encode(response.authenticatorData),
							 | 
						||
| 
								 | 
							
								      clientDataJSON: base64url.encode(response.clientDataJSON),
							 | 
						||
| 
								 | 
							
								      signature: base64url.encode(response.signature),
							 | 
						||
| 
								 | 
							
								      userHandle,
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    credType,
							 | 
						||
| 
								 | 
							
								    clientExtensionResults: cred.getClientExtensionResults(),
							 | 
						||
| 
								 | 
							
								    authenticatorAttachment: cred.authenticatorAttachment,
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let authenticationResponse;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body);
							 | 
						||
| 
								 | 
							
								    if (!authenticationCallResponse.ok) {
							 | 
						||
| 
								 | 
							
								      throw new Error(`HTTP ${authenticationCallResponse.status}`);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    authenticationResponse = await authenticationCallResponse.json();
							 | 
						||
| 
								 | 
							
								    //   if (authenticationResponse && authenticationResponse.authenticated) {
							 | 
						||
| 
								 | 
							
								  } catch (err) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, {
							 | 
						||
| 
								 | 
							
								      cause: err,
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) {
							 | 
						||
| 
								 | 
							
								    throw new Error(
							 | 
						||
| 
								 | 
							
								      `Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`,
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return authenticationResponse.redirectUrl;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								async function register(headers, contextPath, label) {
							 | 
						||
| 
								 | 
							
								  if (!label) {
							 | 
						||
| 
								 | 
							
								    throw new Error("Error: Passkey Label is required");
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let options;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers);
							 | 
						||
| 
								 | 
							
								    if (!optionsResponse.ok) {
							 | 
						||
| 
								 | 
							
								      throw new Error(`Server responded with HTTP ${optionsResponse.status}`);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    options = await optionsResponse.json();
							 | 
						||
| 
								 | 
							
								  } catch (e) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
							 | 
						||
| 
								 | 
							
								  const decodedExcludeCredentials = !options.excludeCredentials
							 | 
						||
| 
								 | 
							
								    ? []
							 | 
						||
| 
								 | 
							
								    : options.excludeCredentials.map((cred) => ({
							 | 
						||
| 
								 | 
							
								        ...cred,
							 | 
						||
| 
								 | 
							
								        id: base64url.decode(cred.id),
							 | 
						||
| 
								 | 
							
								      }));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const decodedOptions = {
							 | 
						||
| 
								 | 
							
								    ...options,
							 | 
						||
| 
								 | 
							
								    user: {
							 | 
						||
| 
								 | 
							
								      ...options.user,
							 | 
						||
| 
								 | 
							
								      id: base64url.decode(options.user.id),
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    challenge: base64url.decode(options.challenge),
							 | 
						||
| 
								 | 
							
								    excludeCredentials: decodedExcludeCredentials,
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let credentialsContainer;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    credentialsContainer = await navigator.credentials.create({
							 | 
						||
| 
								 | 
							
								      publicKey: decodedOptions,
							 | 
						||
| 
								 | 
							
								      signal: abortController.newSignal(),
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  } catch (e) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // FIXME: Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
							 | 
						||
| 
								 | 
							
								  const { response } = credentialsContainer;
							 | 
						||
| 
								 | 
							
								  const credential = {
							 | 
						||
| 
								 | 
							
								    id: credentialsContainer.id,
							 | 
						||
| 
								 | 
							
								    rawId: base64url.encode(credentialsContainer.rawId),
							 | 
						||
| 
								 | 
							
								    response: {
							 | 
						||
| 
								 | 
							
								      attestationObject: base64url.encode(response.attestationObject),
							 | 
						||
| 
								 | 
							
								      clientDataJSON: base64url.encode(response.clientDataJSON),
							 | 
						||
| 
								 | 
							
								      transports: response.getTransports ? response.getTransports() : [],
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								    type: credentialsContainer.type,
							 | 
						||
| 
								 | 
							
								    clientExtensionResults: credentialsContainer.getClientExtensionResults(),
							 | 
						||
| 
								 | 
							
								    authenticatorAttachment: credentialsContainer.authenticatorAttachment,
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const registrationRequest = {
							 | 
						||
| 
								 | 
							
								    publicKey: {
							 | 
						||
| 
								 | 
							
								      credential: credential,
							 | 
						||
| 
								 | 
							
								      label: label,
							 | 
						||
| 
								 | 
							
								    },
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let verificationJSON;
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest);
							 | 
						||
| 
								 | 
							
								    if (!verificationResp.ok) {
							 | 
						||
| 
								 | 
							
								      throw new Error(`HTTP ${verificationResp.status}`);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    verificationJSON = await verificationResp.json();
							 | 
						||
| 
								 | 
							
								  } catch (e) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (!(verificationJSON && verificationJSON.success)) {
							 | 
						||
| 
								 | 
							
								    throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export default {
							 | 
						||
| 
								 | 
							
								  authenticate,
							 | 
						||
| 
								 | 
							
								  register,
							 | 
						||
| 
								 | 
							
								  isConditionalMediationAvailable,
							 | 
						||
| 
								 | 
							
								};
							 |