Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix redirect for IDV/IDP flow for OV enrollment on Android #3782

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .testcaferc.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const env = {
};

const chromeName = env.CHROME_HEADLESS ? 'chrome:headless' : 'chrome';
const chromeOptions = env.OKTA_SIW_MOBILE ? ':emulation:device=iphone X' : '';
const chromeOptions = env.OKTA_SIW_MOBILE ? ':emulation:device=pixel' : '';
const chromeFlags = env.CHROME_HEADLESS ? '' : ' --disable-search-engine-choice-screen';
const chromeFullName = `${chromeName}${chromeOptions}${chromeFlags}`;

Expand All @@ -85,6 +85,8 @@ const config = {
],
src: env.OKTA_SIW_MOBILE ? [
'test/testcafe/spec/EnrollAuthenticatorOktaVerify_spec.js',
'test/testcafe/spec/AuthenticatorIdvView_spec.js',
'test/testcafe/spec/AuthenticatorIdPView_spec.js',
] : env.OKTA_SIW_EN_LEAKS ? [
'test/testcafe/spec-en-leaks/*_spec.js',
] : [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"stateHandle": "02TptqPN4BOLIwMAGUVLPlZVJEnONAq7xkg19dy6Gk",
"version": "1.0.0",
"expiresAt": "2021-01-19T15:10:35.000Z",
"intent": "LOGIN",
"remediation": {
"type": "array",
"value": [
{
"name": "redirect-idp",
"type": "OIDC",
"idp": {
"id": "0oa69chx4bZyx8O7l0g4",
"name": "IDP Authenticator"
},
"href": "http://localhost:3000/sso/idps/0oa69chx4bZyx8O7l0g4?stateToken=02TptqPN4BOLIwMAGUVLPlZVJEnONAq7xkg19dy6Gk",
"method": "GET",
"relatesTo" : [ "$.currentAuthenticatorEnrollment" ]
}
]
},
"currentAuthenticatorEnrollment": {
"type": "object",
"value": {
"key": "external_idp",
"type": "federated",
"id": "aut4mhtS1b84AR0iQ0g4",
"displayName": "IDP Authenticator",
"methods": [
{ "type": "idp" }
]
}
},
"currentAuthenticator": {
"type": "object",
"value": {
"key": "external_idp",
"type": "federated",
"id": "aut4mhtS1b84AR0iQ0g4",
"displayName": "IDP Authenticator",
"methods": [
{ "type": "idp" }
]
}
},
"authenticators": {
"type": "array",
"value": [
{
"key": "external_idp",
"type": "federated",
"id": "aut4mhtS1b84AR0iQ0g4",
"displayName": "IDP Authenticator",
"methods": [
{ "type": "idp" }
]
}
]
},
"authenticatorEnrollments": {
"type": "array",
"value": [
{
"profile": { "provider": "Custom OIDC Provider" },
"type": "federated",
"key": "external_idp",
"id": "aut4mhtS1b84AR0iQ0g4",
"displayName": "IDP Authenticator",
"methods": [
{ "type": "idp" }
]
}
]
},
"user": {
"type": "object",
"value": { "id": "00u2m55pu8UZyeMMl0g4", "identifier": "[email protected]" }
},
"cancel": {
"rel": [ "create-form" ],
"name": "cancel",
"href": "http://localhost:3000/idp/idx/cancel",
"method": "POST",
"value": [
{
"name": "stateHandle",
"required": true,
"value": "02TptqPN4BOLIwMAGUVLPlZVJEnONAq7xkg19dy6Gk",
"visible": false,
"mutable": false
}
],
"accepts": "application/ion+json; okta-version=1.0.0"
},
"app": {
"type": "object",
"value": {
"name": "Okta_Authenticator",
"label": "Okta Authenticator",
"id": "BDSC3453323dsdfS"
}
},
"authentication": {
"type": "object",
"value": {
"protocol": "OAUTH2.0",
"issuer": {
"name": "Test App",
"uri": "http://localhost:3000"
},
"request": {
"max_age": -1,
"scope": "openid profile email okta.authenticators.read okta.authenticators.manage.self",
"display": "page",
"response_type": "code",
"redirect_uri": "https://login.okta.com/oauth/callback",
"state": "i41VVuProw96htTUmvRP9A",
"code_challenge_method": "S256",
"nonce": "ASDF4343SDFS3-GhS8SQCw",
"code_challenge": "abcd_8asd8asdf8as98fasdf_-_9sadif9rasd9fasdf-cc",
"response_mode": "query"
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"version": "1.0.0",
"stateHandle": "02.id.94zt8uHWhX3pnDTehDQyqBGcoUhlDrEDvrwOIUYe",
"expiresAt": "2024-08-01T17:57:28.000Z",
"intent": "CREDENTIAL_ENROLLMENT",
"remediation": {
"type": "array",
"value": [
{
"name": "redirect-idverify",
"type": "ID_PROOFING",
"href": "http://localhost:3000/idp/identity-verification?stateTokenExternalId=bzJOSnhodWVNZjZuVEsrUj",
"method": "GET",
"idp": {
"id": "IDV_PERSONA",
"name": "Persona"
}
}
]
},
"user": {
"type": "object",
"value": {
"id": "00ujkgu115wtBLr0Z0g4",
"identifier": "[email protected]",
"profile": {
"firstName": "admin",
"lastName": "admin",
"timeZone": "America/Los_Angeles",
"locale": "en_US",
"email": "a***[email protected]"
}
}
},
"cancel": {
"rel": [
"create-form"
],
"name": "cancel",
"href": "https://idp.okta1.com/idp/idx/cancel",
"method": "POST",
"produces": "application/ion+json; okta-version=1.0.0",
"value": [
{
"name": "stateHandle",
"required": true,
"value": "02.id.94zt8uHWhX3pnDTehDQyqBGcoUhlDrEDvrwOIUYe",
"visible": false,
"mutable": false
}
],
"accepts": "application/json; okta-version=1.0.0"
},
"app": {
"type": "object",
"value": {
"name": "Okta_Authenticator",
"label": "Okta Authenticator",
"id": "BDSC3453323dsdfS"
}
},
"authentication": {
"type": "object",
"value": {
"protocol": "OAUTH2.0",
"issuer": {
"name": "Test App",
"uri": "http://localhost:3000"
},
"request": {
"max_age": -1,
"scope": "openid profile email okta.authenticators.read okta.authenticators.manage.self",
"display": "page",
"response_type": "code",
"redirect_uri": "https://login.okta.com/oauth/callback",
"state": "i41VVuProw96htTUmvRP9A",
"code_challenge_method": "S256",
"nonce": "ASDF4343SDFS3-GhS8SQCw",
"code_challenge": "abcd_8asd8asdf8as98fasdf_-_9sadif9rasd9fasdf-cc",
"response_mode": "query"
}
}
}
}
13 changes: 7 additions & 6 deletions src/v2/controllers/FormController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,14 @@ export default Controller.extend({

// Use full page redirection if necessary
if (model.get('useRedirect')) {
// Clear when navigates away from SIW page, e.g. success, IdP Authenticator.
// Because SIW sort of finished its current /transaction/
sessionStorageHelper.removeStateHandle();
if (model.get('useRedirectButton')) {
// OKTA-635926: do not redirect without user gesture for ov enrollment on android
// We use a user gesture to complete the redirect in AutoRedirectView
} else {
// Clear when navigates away from SIW page, e.g. success, IdP Authenticator.
// Because SIW sort of finished its current /transaction/
sessionStorageHelper.removeStateHandle();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes


// OKTA-635926: do not redirect without user gesture for ov enrollment on android
// if Util.isAndroidOVEnrollment() returns true we use a user gesture to complete the redirect in AutoRedirectView
if (!Util.isAndroidOVEnrollment(this.options.appState.get('authentication'))) {
const currentViewState = this.options.appState.getCurrentViewState();
// OKTA-702402: redirect only if/when the page is visible
Util.executeOnVisiblePage(() => {
Expand Down
2 changes: 2 additions & 0 deletions src/v2/view-builder/internals/BaseModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ const create = function(remediation = {}, optionUiSchemaConfig = {}) {
formName: 'string',
// use full page redirect instead of AJAX
useRedirect: 'boolean',
// use a special button to perform redirect on click instead of doing this automatically
useRedirectButton: 'boolean',
};
createPropsAndLocals(
remediation,
Expand Down
17 changes: 15 additions & 2 deletions src/v2/view-builder/views/AutoRedirectView.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Body = BaseForm.extend({
const user = this.options.appState.get('user');

// OKTA-635926: add user gesture for ov enrollment on android
if (Util.isAndroidOVEnrollment(this.options.appState.get('authentication'))) {
if (this.useRedirectButton()) {
titleString = loc('oie.success.text.signingIn.with.appName.android.ov.enrollment', 'login');
return titleString;
}
Expand Down Expand Up @@ -71,12 +71,25 @@ const Body = BaseForm.extend({
BaseForm.prototype.initialize.apply(this, arguments);
this.redirectView = this.settings.get('interstitialBeforeLoginRedirect');
this.model.set('useRedirect', true);
if (this.useRedirectButton()) {
this.model.set('useRedirectButton', true);
}
this.trigger('save', this.model);
},

useRedirectButton() {
const idx = this.options.appState.get('idx');
const isAndroidOVEnrollment = Util.isAndroidOVEnrollment(this.options.appState.get('authentication'));
// Do not show "Open Okta Verify" button for "redirect-idp" remediation
// converted to "success-redirect" with `convertRedirectIdPToSuccessRedirectIffOneIdp()`
// (Which can happen if there is a IdP route configured for a user)
const isSuccessRedirect = idx?.rawIdxState?.success?.href;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this fix it will show unnecessary page with "Open Okta Verify" button before redirect to IDP (not IDP authenticator).
In my example I've set up Github IdP and routing rule for my user:

screen-20250204-204028.mp4

return isAndroidOVEnrollment && isSuccessRedirect;
},

render() {
BaseForm.prototype.render.apply(this, arguments);
if (Util.isAndroidOVEnrollment(this.options.appState.get('authentication'))) {
if (this.useRedirectButton()) {
const currentViewState = this.options.appState.getCurrentViewState();
this.add(createButton({
className: 'ul-button button button-wide button-primary hide-underline',
Expand Down
19 changes: 19 additions & 0 deletions test/testcafe/spec/AuthenticatorIdPView_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import xhrVerifyIdPAuthenticatorCustomLogo from '../../../playground/mocks/data/
import xhrVerifyIdPAuthenticatorSingleRemediation from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-single-remediation.json';
import xhrVerifyIdPAuthenticatorError from '../../../playground/mocks/data/idp/idx/error-authenticator-verification-idp.json';
import xhrVerifyIdpAuthentiatorErrorCustomLogo from '../../../playground/mocks/data/idp/idx/error-authenticator-verification-idp-custom-logo.json';
import xhrVerifyIdPAuthenticatorForOvEnrollmentAndroid from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-for-ov-enrollment-on-android.json';

const logger = RequestLogger(/introspect/,
{
Expand Down Expand Up @@ -75,6 +76,10 @@ const verifyErrorCustomLogoMock = RequestMock()
.onRequestTo('http://localhost:3000/idp/idx/introspect')
.respond(xhrVerifyIdpAuthentiatorErrorCustomLogo);

const verifyIdPAuthenticatorForOvEnrollmentAndroid = RequestMock()
.onRequestTo('http://localhost:3000/idp/idx/introspect')
.respond(xhrVerifyIdPAuthenticatorForOvEnrollmentAndroid);

async function setup(t, {isVerify, expectAutoRedirect} = {}, widgetOptions = undefined) {
const options = widgetOptions ? { render: false } : {};
const pageObject = new IdPAuthenticatorPageObject(t);
Expand Down Expand Up @@ -310,3 +315,17 @@ test
await t.expect(pageObject.getBeaconSelector()).contains('custom-app-logo');
}
});

test
.requestHooks(logger, verifyIdPAuthenticatorForOvEnrollmentAndroid)('verify with IdP authenticator for OV enrollment on Android', async t => {
const pageObject = await setup(t, {isVerify: true});
await checkA11y(t);

await t.expect(pageObject.getFormTitle()).eql('Verify with IDP Authenticator');
await t.expect(pageObject.getPageSubtitle()).eql('You will be redirected to verify with IDP Authenticator');
await pageObject.submit('Verify');

const pageUrl = await pageObject.getPageUrl();
await t.expect(pageUrl)
.eql('http://localhost:3000/sso/idps/0oa69chx4bZyx8O7l0g4?stateToken=02TptqPN4BOLIwMAGUVLPlZVJEnONAq7xkg19dy6Gk');
});
19 changes: 19 additions & 0 deletions test/testcafe/spec/AuthenticatorIdvView_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IdPAuthenticatorPageObject from '../framework/page-objects/IdPAuthenticat
import PersonaIdvResponse from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-with-persona.json';
import ClearIdvResponse from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-with-clear.json';
import IncodeIdvResponse from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-with-incode.json';
import PersonaIdvForOvEnrollmentAndroidResponse from '../../../playground/mocks/data/idp/idx/authenticator-verification-idp-with-persona-for-ov-enrollment-on-android.json';

const logger = RequestLogger(/introspect/,
{
Expand Down Expand Up @@ -36,6 +37,14 @@ const incodeIdvMock = RequestMock()
.onRequestTo('http://localhost:3000/idp/identity-verification?stateTokenExternalId=bzJOSnhodWVNZjZuVEsrUj')
.respond('<html><h1>An external IdP login page for testcafe testing</h1></html>');

const personaIdvForOvEnrollmentAndroidMock = RequestMock()
.onRequestTo('http://localhost:3000/idp/idx/introspect')
.respond(PersonaIdvForOvEnrollmentAndroidResponse)
.onRequestTo('http://localhost:3000/idp/idx/credential/enroll')
.respond(PersonaIdvForOvEnrollmentAndroidResponse)
.onRequestTo('http://localhost:3000/idp/identity-verification?stateTokenExternalId=bzJOSnhodWVNZjZuVEsrUj')
.respond('<html><h1>An external IdP login page for testcafe testing</h1></html>');


async function setup(t, widgetOptions = undefined) {
const options = widgetOptions ? { render: false } : {};
Expand Down Expand Up @@ -123,3 +132,13 @@ test
await t.expect(pageUrl)
.eql('http://localhost:3000/idp/identity-verification?stateTokenExternalId=bzJOSnhodWVNZjZuVEsrUj');
});

test
.requestHooks(logger, personaIdvForOvEnrollmentAndroidMock)('should redirect after verification with Persona for OV enrollment on Android', async t => {
const pageObject = await setup(t);
await t.expect(pageObject.getFormTitle()).eql('Verify your identity with Persona');
await pageObject.submit('Continue');
const pageUrl = await pageObject.getPageUrl();
await t.expect(pageUrl)
.eql('http://localhost:3000/idp/identity-verification?stateTokenExternalId=bzJOSnhodWVNZjZuVEsrUj');
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('v2/view-builder/internals/BaseModel', function() {
{
formName: 'string',
useRedirect: 'boolean',
useRedirectButton: 'boolean',
...local
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ describe('v2/view-builder/views/AutoRedirectView', function() {
const appState = new AppState({}, {});
appState.set('user', user);
appState.set('app', app);
appState.set('idx', {
context: SuccessWithAppUser,
rawIdxState: SuccessWithAppUser,
});

testContext.view = new AutoRedirectView({
appState,
Expand Down