Skip to content

Commit

Permalink
Samples: password recovery OKTA-382115 (#693)
Browse files Browse the repository at this point in the history
OKTA-388545
<<<Jenkins Check-In of Tested SHA: 6842f2a for eng_productivity_ci_bot_okta@okta.com>>>
Artifact: okta-auth-js
Files changed count: 252
  • Loading branch information
shuowu authored and eng-prod-CI-bot-okta committed Apr 20, 2021
1 parent a29968d commit 58b6aec
Show file tree
Hide file tree
Showing 46 changed files with 934 additions and 279 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Expand Up @@ -123,6 +123,7 @@
"semi": 2,
"strict": 0,
"wrap-iife": [2, "any"],
"no-throw-literal": 2,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/camelcase": 0,
Expand Down
8 changes: 6 additions & 2 deletions lib/OktaAuth.ts
Expand Up @@ -99,8 +99,10 @@ import {
authenticate,
cancel,
register,
interact,
recoverPassword,
handleInteractionCodeRedirect,
} from './idx';
import { startAuthTransaction } from './idx/startAuthTransaction';

const Emitter = require('tiny-emitter');

Expand Down Expand Up @@ -241,7 +243,9 @@ class OktaAuth implements SigninAPI, SignoutAPI {
authenticate: authenticate.bind(null, this),
register: register.bind(null, this),
cancel: cancel.bind(null, this),
interact: interact.bind(null, this),
recoverPassword: recoverPassword.bind(null, this),
handleInteractionCodeRedirect: handleInteractionCodeRedirect.bind(null, this),
startAuthTransaction: startAuthTransaction.bind(null, this),
};

// Fingerprint API
Expand Down
3 changes: 2 additions & 1 deletion lib/crypto/base64.ts
Expand Up @@ -10,6 +10,7 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { AuthSdkError } from '../errors';
import { atob, btoa } from './webcrypto';

// converts a string to base64 (url/filename safe variant)
Expand Down Expand Up @@ -40,7 +41,7 @@ export function base64UrlToString(b64u) {
b64 += '=';
break;
default:
throw 'Not a valid Base64Url';
throw new AuthSdkError('Not a valid Base64Url');
}
var utf8 = atob(b64);
try {
Expand Down
10 changes: 8 additions & 2 deletions lib/idx/authenticate.ts
Expand Up @@ -2,16 +2,22 @@ import { AuthTransaction } from '../tx';
import {
OktaAuth,
AuthenticationOptions,
RemediatorFlow
RemediationFlow
} from '../types';
import { run } from './run';
import { Identify, EnrollOrChallengeAuthenticator } from './remediators';

const flow: RemediationFlow = {
'identify': Identify,
'challenge-authenticator': EnrollOrChallengeAuthenticator,
};

export async function authenticate(
authClient: OktaAuth, options: AuthenticationOptions
): Promise<AuthTransaction> {
return run(authClient, {
...options,
flow: RemediatorFlow.Authenticate,
flow,
needInteraction: false
});
}
6 changes: 3 additions & 3 deletions lib/idx/cancel.ts
Expand Up @@ -11,11 +11,11 @@
*/

import { OktaAuth, CancelOptions } from '../types';
import { introspect } from './introspect';
import { interact } from './interact';

export async function cancel (authClient: OktaAuth, options: CancelOptions) {
return introspect(authClient, options)
.then(idxResponse => {
return interact(authClient, options)
.then(({ idxResponse }) => {
return idxResponse.actions.cancel();
})
.finally(() => {
Expand Down
36 changes: 36 additions & 0 deletions lib/idx/handleInteractionCodeRedirect.ts
@@ -0,0 +1,36 @@
import { AuthSdkError } from '../errors';
import { OktaAuth } from '..';
import {IdxTransactionMeta} from '../types';

export async function handleInteractionCodeRedirect(
authClient: OktaAuth,
url: string
): Promise<void> {
const {
codeVerifier,
state: savedState
} = authClient.transactionManager.load() as IdxTransactionMeta;
const {
searchParams
// URL API has been added to the polyfill
// eslint-disable-next-line compat/compat
} = new URL(url);
const state = searchParams.get('state');
const interactionCode = searchParams.get('interaction_code');

// Error handling
const error = searchParams.get('error');
if (error) {
throw new AuthSdkError(error);
}
if (state !== savedState) {
throw new AuthSdkError('State in redirect uri does not match with transaction state');
}
if (!interactionCode) {
throw new AuthSdkError('Unable to parse interaction_code from the url');
}

// Save tokens to storage
const { tokens } = await authClient.token.exchangeCodeForTokens({ interactionCode, codeVerifier });
authClient.tokenManager.setTokens(tokens);
}
3 changes: 2 additions & 1 deletion lib/idx/index.ts
@@ -1,5 +1,6 @@
export * from './authenticate';
export * from './interact';
export * from './introspect';
export * from './cancel';
export * from './register';
export * from './recoverPassword';
export * from './handleInteractionCodeRedirect';
32 changes: 32 additions & 0 deletions lib/idx/recoverPassword.ts
@@ -0,0 +1,32 @@
import { AuthTransaction } from '../tx';
import {
OktaAuth,
PasswordRecoveryOptions,
RemediationFlow,
} from '../types';
import { run } from './run';
import {
Identify,
SelectAuthenticator,
EnrollOrChallengeAuthenticator,
AuthenticatorVerificationData,
} from './remediators';

const flow: RemediationFlow = {
'identify-recovery': Identify,
'select-authenticator-authenticate': SelectAuthenticator,
'challenge-authenticator': EnrollOrChallengeAuthenticator,
'authenticator-verification-data': AuthenticatorVerificationData,
'reset-authenticator': EnrollOrChallengeAuthenticator,
};

export async function recoverPassword(
authClient: OktaAuth, options: PasswordRecoveryOptions
): Promise<AuthTransaction> {
return run(authClient, {
...options,
flow,
needInteraction: true,
actionPath: 'currentAuthenticator-recover',
});
}
17 changes: 15 additions & 2 deletions lib/idx/register.ts
Expand Up @@ -2,16 +2,29 @@ import { AuthTransaction } from '../tx';
import {
OktaAuth,
RegistrationOptions,
RemediatorFlow
RemediationFlow,
} from '../types';
import { run } from './run';
import {
SelectEnrollProfile,
EnrollProfile,
SelectAuthenticator,
EnrollOrChallengeAuthenticator,
} from './remediators';

const flow: RemediationFlow = {
'select-enroll-profile': SelectEnrollProfile,
'enroll-profile': EnrollProfile,
'select-authenticator-enroll': SelectAuthenticator,
'enroll-authenticator': EnrollOrChallengeAuthenticator,
};

export async function register(
authClient: OktaAuth, options: RegistrationOptions
): Promise<AuthTransaction> {
return run(authClient, {
...options,
flow: RemediatorFlow.Registration,
flow,
needInteraction: true
});
}
34 changes: 8 additions & 26 deletions lib/idx/remediate.ts
Expand Up @@ -3,45 +3,26 @@
import {
IdxResponse,
isRawIdxResponse,
RemediationFlow,
RemediationValues,
RemediatorFlow
} from '../types';
import {
createApiError,
isErrorResponse,
getIdxRemediation
} from './util';
import Identify from './remediators/Identify';
import ChallengeAuthenticator from './remediators/ChallengeAuthenticator';
import SelectEnrollProfile from './remediators/SelectEnrollProfile';
import EnrollProfile from './remediators/EnrollProfile';
import SelectAuthenticatorEnroll from './remediators/SelectAuthenticatorEnroll';
import EnrollAuthenticator from './remediators/EnrollAuthenticator';

const REMEDIATORS = {
[RemediatorFlow.Authenticate]: {
'identify': Identify,
'challenge-authenticator': ChallengeAuthenticator,
},
[RemediatorFlow.Registration]: {
'select-enroll-profile': SelectEnrollProfile,
'enroll-profile': EnrollProfile,
'select-authenticator-enroll': SelectAuthenticatorEnroll,
'enroll-authenticator': EnrollAuthenticator,
},
// add more
};

// This function is called recursively until it reaches success or cannot be remediated
export async function remediate(
idxResponse: IdxResponse,
flow: RemediatorFlow,
flow: RemediationFlow,
values: RemediationValues
) {
const { neededToProceed } = idxResponse;
const idxRemediation = getIdxRemediation(REMEDIATORS[flow], neededToProceed);
// TODO: idxRemediation may be unfound due to policy setting, handle error here
const idxRemediation = getIdxRemediation(flow, neededToProceed);
const name = idxRemediation.name;
const T = REMEDIATORS[flow][name];
const T = flow[name];
if (!T) {
// No remediator is registered. bail!
return idxResponse;
Expand All @@ -50,7 +31,8 @@ export async function remediate(

// Recursive loop breaker
if (!remediator.canRemediate()) {
return idxResponse;
const nextStep = remediator.getNextStep();
return { idxResponse, nextStep };
}

const data = remediator.getData();
Expand All @@ -60,7 +42,7 @@ export async function remediate(
throw createApiError(idxResponse.rawIdxState);
}
if (idxResponse.interactionCode) {
return idxResponse;
return { idxResponse };
}
return remediate(idxResponse, flow, values); // recursive call
} catch (e) {
Expand Down
27 changes: 27 additions & 0 deletions lib/idx/remediators/AuthenticatorVerificationData.ts
@@ -0,0 +1,27 @@
import Base from './Base';

export default class AuthenticatorVerificationData extends Base {
values: any; // TODO: add proper type

map = {
'authenticator': ['authenticator']
};

canRemediate() {
// TODO: check if authenticator exist in values
return this.remediation.value
.some(({ name }) => name === 'authenticator');
}

mapAuthenticator() {
const authenticatorVal = this.remediation.value
.find(({ name }) => name === 'authenticator').form.value;
return {
id: authenticatorVal
.find(({ name }) => name === 'id').value,
enrollmentId: authenticatorVal
.find(({ name }) => name === 'enrollmentId').value,
methodType: 'sms',
};
}
}
6 changes: 5 additions & 1 deletion lib/idx/remediators/Base.ts
Expand Up @@ -27,7 +27,7 @@ export default class Base {
}

// returns an object for the entire remediation form, or just a part
getData(key: string) {
getData(key?: string) {
if (!this.map) {
return {};
}
Expand Down Expand Up @@ -84,6 +84,10 @@ export default class Base {
});
}

getNextStep() {
return { name: this.remediation.name };
}

// Override this method to extract error message from remediation form fields
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
getErrorMessages(errorRemediation: IdxResponse) {
Expand Down
14 changes: 0 additions & 14 deletions lib/idx/remediators/ChallengeAuthenticator.ts

This file was deleted.

@@ -1,17 +1,19 @@
import Base from './Base';
import { RegistrationRemediationValues } from '../types';

export default class EnrollAuthenticator extends Base {
values: RegistrationRemediationValues;
export default class EnrollOrChallengeAuthenticator extends Base {
values: any; // TODO: add proper type

map = {
'credentials': ['credentials', 'password', 'emailVerificationCode']
'credentials': ['credentials', 'password', 'emailVerificationCode', 'verificationCode']
};

canRemediate() {
if (this.values.emailVerificationCode && this.remediation.relatesTo.value.type === 'email') {
return true;
}
if (this.values.verificationCode && this.remediation.relatesTo.value.type === 'phone') {
return true;
}
if (this.values.password && this.remediation.relatesTo.value.type === 'password') {
return true;
}
Expand All @@ -20,7 +22,16 @@ export default class EnrollAuthenticator extends Base {

mapCredentials() {
return {
passcode: this.values.emailVerificationCode || this.values.password
passcode: this.values.emailVerificationCode
|| this.values.verificationCode
|| this.values.password
};
}

getNextStep() {
return {
name: this.remediation.name,
type: this.remediation.relatesTo.value.type,
};
}

Expand Down
@@ -1,7 +1,7 @@
import Base from './Base';
import { RegistrationRemediationValues } from '../types';

export default class SelectAuthenticatorEnroll extends Base {
export default class SelectAuthenticator extends Base {
values: RegistrationRemediationValues;
map = {};

Expand Down
7 changes: 7 additions & 0 deletions lib/idx/remediators/index.ts
@@ -0,0 +1,7 @@
export { default as Base } from './Base';
export { default as EnrollOrChallengeAuthenticator } from './EnrollOrChallengeAuthenticator';
export { default as EnrollProfile } from './EnrollProfile';
export { default as Identify } from './Identify';
export { default as SelectAuthenticator } from './SelectAuthenticator';
export { default as SelectEnrollProfile } from './SelectEnrollProfile';
export { default as AuthenticatorVerificationData } from './AuthenticatorVerificationData';

0 comments on commit 58b6aec

Please sign in to comment.