Skip to content

Commit

Permalink
webauthn autofill UI spike
Browse files Browse the repository at this point in the history
  • Loading branch information
xifengfang-okta committed Aug 23, 2024
1 parent 030b2cd commit a03d62d
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 3 deletions.
5 changes: 4 additions & 1 deletion src/v2/controllers/FormController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ export default Controller.extend({
}

// Build options to invoke or throw error for invalid action
if (FORMS.LAUNCH_AUTHENTICATOR === actionPath && actionParams) {
//Passkey experience
const userHandle = (actionParams as any)?.credentials?.userHandle;
const isPasskey = FORMS.CHALLENGE_AUTHENTICATOR === actionPath && !_.isEmpty(userHandle);
if ((FORMS.LAUNCH_AUTHENTICATOR === actionPath || isPasskey) && actionParams) {
//https://oktainc.atlassian.net/browse/OKTA-562885 a temp solution to send rememberMe when click the launch OV buttion.
//will redesign to handle FastPass silent probing case where no username and rememberMe opiton at all.
invokeOptions = {
Expand Down
57 changes: 55 additions & 2 deletions src/v2/view-builder/views/IdentifierView.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { loc, createCallout } from '@okta/courage';
import { _, loc, createCallout } from '@okta/courage';
import { FORMS as RemediationForms } from '../../ion/RemediationConstants';
import { BaseForm, BaseView, createIdpButtons, createCustomButtons } from '../internals';
import CryptoUtil from '../../../util/CryptoUtil';
import DeviceFingerprinting from '../utils/DeviceFingerprinting';
import IdentifierFooter from '../components/IdentifierFooter';
import Link from '../components/Link';
Expand All @@ -13,6 +14,7 @@ import { getForgotPasswordLink } from '../utils/LinksUtil';
import CookieUtil from 'util/CookieUtil';
import CustomAccessDeniedErrorMessage from './shared/CustomAccessDeniedErrorMessage';
import Util from 'util/Util';
import { getMessageFromBrowserError } from '../../ion/i18nTransformer';

const CUSTOM_ACCESS_DENIED_KEY = 'security.access_denied_custom_message';

Expand Down Expand Up @@ -115,6 +117,7 @@ const Body = BaseForm.extend({
// When a user enters invalid credentials, /introspect returns an error,
// along with a user object containing the identifier entered by the user.
this.$el.find('.identifier-container').remove();
this.getWebauthnAutofillUICredentialsAndSave();
},

/**
Expand Down Expand Up @@ -144,7 +147,7 @@ const Body = BaseForm.extend({
// because we want to allow the user to choose from previously used identifiers.
newSchema = {
...newSchema,
autoComplete: Util.getAutocompleteValue(this.options.settings, 'username')
autoComplete: Util.getAutocompleteValue(this.options.settings, 'username') + this.options.appState.get('webauthnAutofillUIChallenge')?.challengeData ? ' webauthn' : ''
};
} else if (schema.name === 'credentials.passcode') {
newSchema = {
Expand Down Expand Up @@ -245,6 +248,56 @@ const Body = BaseForm.extend({
if (cookieUsername) {
this.model.set('identifier', cookieUsername);
}
},

remove() {
BaseForm.prototype.remove.apply(this, arguments);
if (this.webauthnAbortController) {
this.webauthnAbortController.abort();
this.webauthnAbortController = null;
}
},

getWebauthnAutofillUICredentialsAndSave() {
const challengeData = this.options.appState.get('webauthnAutofillUIChallenge')?.challengeData;
if (!challengeData) return;
const options = _.extend({}, challengeData, {
challenge: CryptoUtil.strToBin(challengeData.challenge),
});

if (typeof AbortController !== 'undefined') {
this.webauthnAbortController = new AbortController();
}

navigator.credentials.get({
mediation: 'conditional',
publicKey: options,
signal: this.webauthnAbortController && this.webauthnAbortController.signal
}).then((assertion) => {
const userHandle = CryptoUtil.binToStr(assertion.response.userHandle ?? '');
if (_.isEmpty(userHandle)) {
this.model.trigger('error', this.model, {responseJSON: {errorSummary: 'Invalid Passkey'}});
return;
}
const credentials = {
clientData: CryptoUtil.binToStr(assertion.response.clientDataJSON),
authenticatorData: CryptoUtil.binToStr(assertion.response.authenticatorData),
signatureData: CryptoUtil.binToStr(assertion.response.signature),
userHandle
};

//TODO, is there a better way for this?
this.options.appState.trigger('invokeAction', RemediationForms.CHALLENGE_AUTHENTICATOR, {'credentials': credentials});
}, (error) => {
// Do not display if it is abort error triggered by code when switching.
// this.webauthnAbortController would be null if abort was triggered by code.
if (this.webauthnAbortController) {
this.model.trigger('error', this.model, {responseJSON: {errorSummary: getMessageFromBrowserError(error)}});
}
}).finally(() => {
// unset webauthnAbortController on successful authentication or error
this.webauthnAbortController = null;
});
}
});

Expand Down

0 comments on commit a03d62d

Please sign in to comment.