Compare commits
20 Commits
fixing-san
...
zamir/VAN-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1aabc079a | ||
|
|
bcece267bc | ||
|
|
1575034ed3 | ||
|
|
922f1e2ac4 | ||
|
|
b03fe545ba | ||
|
|
47fce8aa7d | ||
|
|
5fd6754025 | ||
|
|
428372d198 | ||
|
|
4f83726387 | ||
|
|
40cb949d4e | ||
|
|
daa54be1c8 | ||
|
|
b126635981 | ||
|
|
94e1207d37 | ||
|
|
9ec6e1cf05 | ||
|
|
eddce35a37 | ||
|
|
38cbd4d0d8 | ||
|
|
b962b3f4ca | ||
|
|
ad9b40d9e9 | ||
|
|
7990fe7483 | ||
|
|
def1415f14 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 16]
|
||||
node: [16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
155
docs/decisions/0003-2FA-for-course-staff-accounts.rst
Normal file
155
docs/decisions/0003-2FA-for-course-staff-accounts.rst
Normal file
@@ -0,0 +1,155 @@
|
||||
3. 2FA for Course Team Accounts
|
||||
-----------------------------------------
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
Proposed
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
Course team accounts have a great deal of access to and control over sensitive information as well as the ability to run arbitrary Javascript on the LMS. An attacker who compromises an educator account could do a large amount of damage. To protect our learners (and our reputation) we should require 2FA or other enhanced account security for all accounts with these elevated permissions.
|
||||
|
||||
An attacker should find it difficult to gain access to the privileged aspects of an educator's account (even if in possession of their password).
|
||||
|
||||
|
||||
Proposed Solutions
|
||||
------------------
|
||||
|
||||
Option 1: Enable Two Factor Authentication (2FA)
|
||||
|
||||
Option 2: Ask users for their password before specific restricted actions on site
|
||||
|
||||
Option 3: Reset password after a specific time period
|
||||
|
||||
|
||||
Decision
|
||||
--------
|
||||
|
||||
We have decided to go with Option 1. We will enable **Two Factor Authentication** (2FA) for user accounts that belong to course teams (in some cases these can be staff accounts as well).
|
||||
|
||||
This change can be accommodated within our current login flow. A high level flow of this process is as follow:
|
||||
|
||||
- A user belonging to a course team will authenticate themselves like they currently do, either by providing login credentials or by SSO.
|
||||
- Once they have authenticated successfully they will be authenticated by a 2FA method
|
||||
- After successful verification of 2FA code, assign a session to user.
|
||||
|
||||
Why choose 2FA?
|
||||
***************
|
||||
|
||||
- It is a conventional way to secure user accounts.
|
||||
- We can protect staff users at the first step they take on our site i.e authentication.
|
||||
- Development wise it will not require extensive effort as compared to the second option.
|
||||
|
||||
|
||||
Two Factor Authentication approaches
|
||||
************************************
|
||||
|
||||
- Email Token
|
||||
- Sms Token
|
||||
- Push Based 2FA
|
||||
- TOTP
|
||||
- U2F Security Keys
|
||||
|
||||
|
||||
Comparison
|
||||
**********
|
||||
|
||||
.. list-table:: **Comparison of 2FA methods**
|
||||
:widths: 10 15 15 15
|
||||
:header-rows: 1
|
||||
:stub-columns: 1
|
||||
|
||||
* - Types
|
||||
- Intro
|
||||
- Pros
|
||||
- Cons
|
||||
* - Email Token
|
||||
- Send a token to the registered email address and verify the token, during login.
|
||||
- - Works fine if the email is secured
|
||||
- Extra device not needed
|
||||
- We already have user email so we will not be requiring any personal information from the user.
|
||||
- - Vulnerable if password is reused. (most users keep same password)
|
||||
- Vulnerable if device is compromised (malware attack)
|
||||
- Not secured as other 2FA methods.
|
||||
- Email delivery concern.
|
||||
* - SMS Token
|
||||
- Send a token to the registered mobile number and verify the token, during login.
|
||||
- - No need to install extra app
|
||||
- Works fine if mobile network is not ported
|
||||
- - Vulnerable to targeted attacks like social engineering and number porting etc.
|
||||
- Requires a cell phone.
|
||||
- Privacy concern (people not comfortable giving phone number)
|
||||
- Cannot use if phone is dead or unable to reach signals
|
||||
- We will be getting a mobile phone number ( a new info) so we will be needing compliance approval.
|
||||
* - Push Based
|
||||
- Send a push notification on your trusted device, the prompt will ask for accept and denial.
|
||||
- - Secured
|
||||
- Convenient
|
||||
- Time saving
|
||||
- - Cannot use single app for multiple sites
|
||||
- Requires signal and connection
|
||||
- Cannot use if phone is dead
|
||||
- Requires custom integrations from providers.
|
||||
* - U2F
|
||||
- Using small USB, NFC or Bluetooth Low Energy (BTLE) devices often called “security keys.” To set it up on a site, you register your U2F device. And then tap on the device when logging in.
|
||||
- - Secured
|
||||
- Can use same U2F device for multiple sites
|
||||
- - Needs a device
|
||||
- Cost
|
||||
- Support (only chrome supports U2F currently)
|
||||
- NFC only available on android mobiles
|
||||
- Vulnerable to physical theft of device.
|
||||
* - TOTP
|
||||
- Use an authenticator app on another phone to generate time-based codes locally based on secret keys.
|
||||
- - Secured
|
||||
- Generate codes locally
|
||||
- Time based code (valid for some seconds)
|
||||
- Standardized
|
||||
- Does Not require signal or connection therefore not vulnerable to social engineering attacks like number porting.
|
||||
- Some authenticator apps do not require a mobile device either.
|
||||
- Does not require any personal information from user.
|
||||
- - Cannot use if phone is dead
|
||||
- Not convenient (need to type code within a time frame)
|
||||
- Vulnerable if the mobile is compromised and TOTP for the website has been set up on the same mobile with the app.
|
||||
|
||||
|
||||
Preferred Method
|
||||
----------------
|
||||
|
||||
We have **TOTP** (Time-based on time password) as preferred two-factor authentication method.
|
||||
|
||||
**TOTP**
|
||||
TOTP stands for Time-based One-Time Passwords and is a common form of two factor authentication (2FA). Unique numeric passwords are generated with a standardized algorithm that uses the current time as an input. The time-based passwords are available offline and provide user friendly, increased account security when used as a second factor. (ref).
|
||||
|
||||
|
||||
Issues with previous approach
|
||||
------------------------------
|
||||
|
||||
Previously our approach was to either reduced session expiration time or go with 2FA and Password reset after some time simultaneously but there were some problems with this approach:
|
||||
|
||||
- Reducing session expiration to 2 weeks from 4 weeks was not making a big difference and if we reduce it to 1 day then it was an issue for course teams since they are normally making changes that might span over a few days and they might lose those edit if they don't save them as draft.
|
||||
|
||||
- Resetting passwords often makes people more likely to pick simple passwords. Also we should avoid doing password resets except in cases of known compromise.
|
||||
|
||||
Based on these reasoning, we decided to look into other kinds of 2FA.
|
||||
|
||||
|
||||
Consequences
|
||||
************
|
||||
|
||||
Adds an additional step for some users which can negatively affect the user experience.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
Corresponding Security Ticket:
|
||||
https://2u-internal.atlassian.net/browse/SEG-96
|
||||
|
||||
Detailed analysis of all security enhancements options provided above can be found here:
|
||||
https://openedx.atlassian.net/l/c/3HKHjZY4
|
||||
|
||||
Analysis of other kinds of 2FA:
|
||||
https://docs.google.com/document/d/1oNtUNbu71DBvznIVbxiOJbVMToTZqkCoiT-5VMIjqWg/edit?usp=sharing
|
||||
147
package-lock.json
generated
147
package-lock.json
generated
@@ -51,7 +51,7 @@
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "9.1.2",
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"babel-plugin-formatjs": "10.3.18",
|
||||
"codecov": "3.8.2",
|
||||
@@ -2005,9 +2005,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-build": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.2.tgz",
|
||||
"integrity": "sha512-qC7ReTYGEiAfd1WMMEKR2JnO6/kvTh80COOqA4CslV8l6kchMP7pw9dVPDj+/njWFhK/sn9hoi+Ro4wlcylwyg==",
|
||||
"version": "9.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.4.tgz",
|
||||
"integrity": "sha512-Lw4EqZ8iMnIDLA/xlAEByPzzoyU9faStktYgnRGhPM9DPyQ504NQ0I/qv7vX6qWJ2wII458TNAgGzu9zr2hHWQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.16.0",
|
||||
@@ -2051,8 +2051,8 @@
|
||||
"react-dev-utils": "12.0.0",
|
||||
"react-refresh": "0.10.0",
|
||||
"resolve-url-loader": "5.0.0-beta.1",
|
||||
"sass": "1.26.11",
|
||||
"sass-loader": "10.0.5",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "2.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
@@ -14692,6 +14692,12 @@
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -24212,34 +24218,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.26.11",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
|
||||
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz",
|
||||
"integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==",
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||
"integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"klona": "^2.0.4",
|
||||
"loader-utils": "^2.0.0",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.0.0",
|
||||
"semver": "^7.3.2"
|
||||
"neo-async": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
"node": ">= 12.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -24247,9 +24252,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"fibers": ">= 3.1.0",
|
||||
"node-sass": "^4.0.0 || ^5.0.0",
|
||||
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"sass": "^1.3.0",
|
||||
"webpack": "^4.36.0 || ^5.0.0"
|
||||
"sass-embedded": "*",
|
||||
"webpack": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"fibers": {
|
||||
@@ -24260,42 +24266,12 @@
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader/node_modules/semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
||||
@@ -29482,9 +29458,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@edx/frontend-build": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.2.tgz",
|
||||
"integrity": "sha512-qC7ReTYGEiAfd1WMMEKR2JnO6/kvTh80COOqA4CslV8l6kchMP7pw9dVPDj+/njWFhK/sn9hoi+Ro4wlcylwyg==",
|
||||
"version": "9.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.4.tgz",
|
||||
"integrity": "sha512-Lw4EqZ8iMnIDLA/xlAEByPzzoyU9faStktYgnRGhPM9DPyQ504NQ0I/qv7vX6qWJ2wII458TNAgGzu9zr2hHWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/cli": "7.16.0",
|
||||
@@ -29528,8 +29504,8 @@
|
||||
"react-dev-utils": "12.0.0",
|
||||
"react-refresh": "0.10.0",
|
||||
"resolve-url-loader": "5.0.0-beta.1",
|
||||
"sass": "1.26.11",
|
||||
"sass-loader": "10.0.5",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "2.0.0",
|
||||
"url-loader": "4.1.1",
|
||||
@@ -39416,6 +39392,12 @@
|
||||
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
|
||||
"dev": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -46630,51 +46612,24 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.26.11",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
|
||||
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"sass-loader": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz",
|
||||
"integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==",
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||
"integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"klona": "^2.0.4",
|
||||
"loader-utils": "^2.0.0",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.0.0",
|
||||
"semver": "^7.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
}
|
||||
"neo-async": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"saxes": {
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "9.1.2",
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"babel-plugin-formatjs": "10.3.18",
|
||||
"codecov": "3.8.2",
|
||||
|
||||
@@ -37,12 +37,12 @@ $accent-a-light: #c9f2f5;
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.stateful-button-variation1-width {
|
||||
width: 16.4rem;
|
||||
.register-stateful-button-width {
|
||||
min-width: 14.4rem;
|
||||
}
|
||||
|
||||
.login-button-width {
|
||||
width: 6rem;
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.tpa-skeleton {
|
||||
@@ -866,7 +866,7 @@ select.form-control {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.suggested-username-close-button {
|
||||
right: 0;
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
}
|
||||
.suggested-username-with-error {
|
||||
@@ -881,3 +881,7 @@ select.form-control {
|
||||
overflow-x: auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pgn__form-control-decorator-trailing {
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
@@ -20,51 +20,53 @@ const LargeLeftLayout = (props) => {
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<div className="min-vh-100 pr-0 mt-lg-n2 d-flex align-items-center">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<svg className={classNames(
|
||||
'large-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<span className="text-light-300 dicount-heading">
|
||||
<span className="lead mr-3">
|
||||
<SideDiscountBanner />
|
||||
<div className="min-vh-100 d-flex justify-content-left align-items-center">
|
||||
<div className="d-flex pr-0 mt-lg-n2">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<svg className={classNames(
|
||||
'large-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<span className="text-light-300 dicount-heading">
|
||||
<span className="lead mr-3">
|
||||
<SideDiscountBanner />
|
||||
</span>
|
||||
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ const FormGroup = (props) => {
|
||||
as={props.as}
|
||||
readOnly={props.readOnly}
|
||||
type={props.type}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
className="form-field"
|
||||
autoComplete={props.autoComplete}
|
||||
name={props.name}
|
||||
|
||||
@@ -63,6 +63,7 @@ const PasswordField = (props) => {
|
||||
type={isPasswordHidden ? 'password' : 'text'}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={props.handleChange}
|
||||
|
||||
@@ -99,9 +99,9 @@ const ForgotPasswordPage = (props) => {
|
||||
</Helmet>
|
||||
<Form className="mw-xs">
|
||||
<ForgotPasswordAlert email={props.email} emailError={errors.email} status={status} />
|
||||
<h4>
|
||||
<h2 className="h4">
|
||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
||||
</h4>
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
||||
</p>
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "El nombre que te identificará en tus cursos.",
|
||||
"help.text.username.2": "Esto no puede modificarse posteriormente.",
|
||||
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
|
||||
"create.account.button": "Crear una cuenta",
|
||||
"create.account.for.free.button": "Crea una cuenta gratis",
|
||||
"create.an.account.btn.pending.state": "Cargando",
|
||||
"registration.other.options.heading": "O regístrese con:",
|
||||
|
||||
@@ -112,12 +112,12 @@
|
||||
"login.form.invalid.error.message": "Veuillez remplir les champs ci-dessous.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "réinitialiser votre mot de passe",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "cliquez ici pour le réinitialiser.",
|
||||
"password.security.nudge.title": "Password security",
|
||||
"password.security.block.title": "Password change required",
|
||||
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"password.security.nudge.title": "Sécurité du mot de passe",
|
||||
"password.security.block.title": "Changement de mot de passe requis",
|
||||
"password.security.nudge.body": "Notre système a détecté que votre mot de passe est vulnérable. Nous vous recommandons de le modifier afin que votre compte reste sécurisé.",
|
||||
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
|
||||
"password.security.close.button": "Fermer",
|
||||
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
|
||||
"register.page.title": "S'inscrire | {siteName}",
|
||||
"registration.fullname.label": "Nom complet",
|
||||
"registration.email.label": "Email",
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "Le nom qui vous identifiera dans vos cours.",
|
||||
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
|
||||
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
|
||||
"create.account.button": "Créer un compte",
|
||||
"create.account.for.free.button": "Créer un compte gratuitement",
|
||||
"create.an.account.btn.pending.state": "Chargement en cours",
|
||||
"registration.other.options.heading": "Ou inscrivez-vous avec :",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.button": "Create an account",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -24,6 +25,7 @@ import ChangePasswordPrompt from './ChangePasswordPrompt';
|
||||
const LoginFailureMessage = (props) => {
|
||||
const { intl } = props;
|
||||
const { context, errorCode, value } = props.loginError;
|
||||
const authService = getAuthService();
|
||||
let errorList;
|
||||
let link;
|
||||
let resetLink = (
|
||||
@@ -135,6 +137,10 @@ const LoginFailureMessage = (props) => {
|
||||
}
|
||||
break;
|
||||
case NUDGE_PASSWORD_CHANGE:
|
||||
// Need to clear the CSRF token here to fetch a new one because token is already rotated after successful login.
|
||||
if (authService) {
|
||||
authService.getCsrfTokenService().clearCsrfTokenCache();
|
||||
}
|
||||
return (
|
||||
<ChangePasswordPrompt
|
||||
redirectUrl={props.loginError.redirectUrl}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
import {
|
||||
@@ -17,6 +18,9 @@ import {
|
||||
REQUIRE_PASSWORD_CHANGE,
|
||||
} from '../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
auth.getAuthService = jest.fn();
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
|
||||
describe('LoginFailureMessage', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import renderer from 'react-test-renderer';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { loginRequest, loginRequestFailure, loginRequestReset } from '../data/actions';
|
||||
@@ -18,9 +19,11 @@ import LoginPage from '../LoginPage';
|
||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
auth.getAuthService = jest.fn();
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
const IntlLoginPage = injectIntl(LoginPage);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
@@ -76,8 +75,6 @@ class RegistrationPage extends React.Component {
|
||||
optimizelyExperimentName: '',
|
||||
readOnly: true,
|
||||
validatePassword: false,
|
||||
// TODO: Remove after VAN-876 experimentation is complete.
|
||||
registerRenameExpVariation: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,13 +87,6 @@ class RegistrationPage extends React.Component {
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (payload.register_for_free === 'true') {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'van-876-authn-registration-page',
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.save_for_later === 'true') {
|
||||
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
|
||||
}
|
||||
@@ -174,15 +164,11 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
|
||||
getExperiments = () => {
|
||||
const { experimentName, renameRegisterExperiment } = window;
|
||||
const { experimentName } = window;
|
||||
|
||||
if (experimentName) {
|
||||
this.setState({ optimizelyExperimentName: experimentName });
|
||||
}
|
||||
|
||||
if (renameRegisterExperiment) {
|
||||
this.setState({ registerRenameExpVariation: renameRegisterExperiment });
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
@@ -523,7 +509,6 @@ class RegistrationPage extends React.Component {
|
||||
setSurveyCookie('register');
|
||||
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
|
||||
setCookie('authn-returning-user');
|
||||
const payload = { ...this.queryParams };
|
||||
|
||||
// Fire optimizely events
|
||||
window.optimizely = window.optimizely || [];
|
||||
@@ -534,13 +519,6 @@ class RegistrationPage extends React.Component {
|
||||
value: this.state.totalRegistrationTime,
|
||||
},
|
||||
});
|
||||
|
||||
if (payload.register_for_free === 'true') {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'van-876-authn-register-for-free-conversion',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -673,16 +651,10 @@ class RegistrationPage extends React.Component {
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className={classNames(
|
||||
'mt-4 mb-4',
|
||||
{ 'stateful-button-variation1-width': this.state.registerRenameExpVariation === 'variation1' },
|
||||
{ 'stateful-button-width': this.state.registerRenameExpVariation !== 'variation1' },
|
||||
)}
|
||||
className="register-stateful-button-width mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: this.state.registerRenameExpVariation === 'variation1' ? (
|
||||
intl.formatMessage(messages['create.account.for.free.button'])
|
||||
) : intl.formatMessage(messages['create.account.button']),
|
||||
default: intl.formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={this.handleSubmit}
|
||||
|
||||
@@ -59,11 +59,6 @@ const messages = defineMessages({
|
||||
description: 'Help text for email field on registration page',
|
||||
},
|
||||
// Form buttons
|
||||
'create.account.button': {
|
||||
id: 'create.account.button',
|
||||
defaultMessage: 'Create an account',
|
||||
description: 'Button label that appears on register page',
|
||||
},
|
||||
'create.account.for.free.button': {
|
||||
id: 'create.account.for.free.button',
|
||||
defaultMessage: 'Create an account for free',
|
||||
|
||||
@@ -404,7 +404,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
it('should match default button state', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account');
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free');
|
||||
});
|
||||
|
||||
it('should match pending button state', () => {
|
||||
|
||||
Reference in New Issue
Block a user