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

Add support for one-time passwords (two-factor authentication) #2655

Merged
merged 30 commits into from Jun 4, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b73d706
Add support for one-time passwords (two-factor authentication)
brianhelba Mar 9, 2018
2d06238
Fix a typo
brianhelba May 9, 2018
8a261a9
Add debugging comments
brianhelba May 9, 2018
8cacee5
Create and import new stylesheet for 2-factor auth
jtomeck May 11, 2018
a309f18
Change 2FA info text to be more descriptive
jtomeck May 11, 2018
8a14c5e
Improve structure for more control over styles
jtomeck May 11, 2018
d6b973d
Perform basic style clean-up.
jtomeck May 11, 2018
47dd4ff
Add styling to the OTP widget
jtomeck May 11, 2018
d9586f8
Fix stylus indentation
jtomeck May 11, 2018
f7b3328
Support initialization of other OTP types
brianhelba May 21, 2018
83033d1
Improve error handling when verifying OTP
brianhelba May 28, 2018
630d857
Add OTP testing
brianhelba May 28, 2018
eca0e7d
Add Girder-OTP to default CORS headers
brianhelba May 29, 2018
eb02d4e
Support OTP from SFTP and add additional testing
brianhelba May 29, 2018
ce8070e
Send filtered "otp" field with user
brianhelba May 29, 2018
df801d0
Add a new cache region for rate limiting OTP tokens
brianhelba May 28, 2018
51f3001
Improve client
brianhelba May 29, 2018
4cade90
Merge remote-tracking branch 'origin/master' into otp
brianhelba May 30, 2018
b0576f0
Do not reveal whether a user exists when logging in
brianhelba May 30, 2018
4518e82
Add a test for TOTP URI info
brianhelba May 31, 2018
f5757c1
Add basic client tests for OTP
brianhelba Jun 1, 2018
ded8274
Rename User.hasOtpEnabled
brianhelba Jun 1, 2018
241a110
Rename OTP client entities to be more consistent
brianhelba Jun 1, 2018
cfdb9b1
Add a CHANGELOG entry for OTP
brianhelba Jun 1, 2018
60d8fdd
Use the site hostname for the OTP issuer if the brand name isn't set
brianhelba Jun 1, 2018
e3e9ce7
Add client error messages when OTP enabling fails
brianhelba Jun 1, 2018
ce16aa0
Remove a comment
brianhelba Jun 1, 2018
24c5989
Add styling to disable page
jtomeck Jun 4, 2018
882df3c
Merge remote-tracking branch 'origin/master' into otp
brianhelba Jun 4, 2018
6dc22f2
Ensure test incorrect OTP tokens always have 6 digits
brianhelba Jun 4, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 10 additions & 7 deletions clients/web/src/auth.js
Expand Up @@ -78,19 +78,22 @@ function fetchCurrentUser() {
* @param cors If the girder server is on a different origin, set this
* to "true" to save the auth cookie on the current domain. Alternatively,
* you may set the global option "girder.corsAuth = true".
* @param otpToken An optional one-time password to include with the login.
*/
function login(username, password, cors) {
function login(username, password, cors = corsAuth, otpToken = null) {
var auth = 'Basic ' + window.btoa(username + ':' + password);
if (cors === undefined) {
cors = corsAuth;
}

const headers = {
'Girder-Authorization': auth
};
if (_.isString(otpToken)) {
// Use _.isString to send header with empty string
headers['Girder-OTP'] = otpToken;
}
return restRequest({
method: 'GET',
url: '/user/authentication',
headers: {
'Girder-Authorization': auth
},
headers: headers,
error: null
}).then(function (response) {
response.user.token = response.authToken;
Expand Down
36 changes: 36 additions & 0 deletions clients/web/src/models/UserModel.js
Expand Up @@ -120,6 +120,42 @@ var UserModel = Model.extend({
}).fail((err) => {
this.trigger('g:error', err);
});
},

initializeOtp: function () {
return restRequest({
url: `user/${this.id}/otp`,
method: 'POST',
error: null
})
.then((resp) => {
return resp.totpUri;
});
},

finializeOtp: function (otpToken) {
return restRequest({
url: `user/${this.id}/otp`,
method: 'PUT',
headers: {
'Girder-OTP': otpToken
},
error: null
})
.done(() => {
this.set('otp', true);
});
},

removeOtp: function () {
return restRequest({
url: `user/${this.id}/otp`,
method: 'DELETE',
error: null
})
.done(() => {
this.set('otp', false);
});
}
}, {
fromTemporaryToken: function (userId, token) {
Expand Down
4 changes: 3 additions & 1 deletion clients/web/src/package.json
Expand Up @@ -20,10 +20,12 @@
"jquery": "~3.2.1",
"jsoneditor": "~5.9.3",
"moment": "~2.20.1",
"qrcode": "~1.2.0",
"remarkable": "~1.7.1",
"sprintf-js": "~1.1.1",
"swagger-ui": "~2.2.10",
"underscore": "~1.8.3"
"underscore": "~1.8.3",
"url-otpauth": "~2.0.0"
},
"main": "./index.js",
"scripts": {
Expand Down
143 changes: 143 additions & 0 deletions clients/web/src/stylesheets/widgets/userOtpManagementWidget.styl
@@ -0,0 +1,143 @@
#g-account-tab-otp
// Included parent ID to avoid using !important for style overrides
.g-user-settings-container
background none
border none
.g-account-otp-info-text
border-bottom 1px solid #cccccc
margin-top 10px
padding-bottom 35px
.g-account-otp-container
padding-top 35px
h2
margin 0 20px 10px 0
display inline-block
h3
color #333
font-size 20px
font-weight bold
margin 0 0 5px
#g-user-otp-initialize
margin-top -12px
.g-account-otp-steps
background-color #fafafa
border 1px solid #e3e3e3
border-radius 4px 4px 0 0
margin-bottom 0
margin-top 10px
overflow visible
padding-right 40px
position relative
&:before
background-color #337ab7
border 1px solid #2e6da4
border-radius 4px 4px 0 0
content ""
display block
height 6px
position absolute
left -1px
right -1px
top -1px
.g-account-otp-step
border-bottom 1px solid #e3e3e3
color #bbb
font-size 20px
font-weight 600
padding 35px 0
// clearfix
&:after
clear both
content ""
display table
&:first-of-type
padding-top 30px
&:last-of-type
border-bottom none
div
color #333
font-size 14px
font-weight normal
padding-top 5px
h4
font-size 16px
font-weight bold
color #666
margin-bottom 20px
// Styles for only step 2
&:nth-child(2)
div
float left
padding-top 20px
width 45%
&.g-account-otp-scan-qr
margin-right 4%
width 51%
// Mobile Phone Icon
&:before
color rgba(51, 122, 183, 0.25)
content "\e8fd"
display block
float left
font-family "fontello"
font-size 26em
font-style normal
font-weight normal
line-height 280px
margin-right 40px
&.g-account-otp-enter-manual
background #fff
border 1px solid #e3e3e3
border-radius 4px
margin-top 26px
padding 20px
h4
margin-top 0
span
font-weight normal
display block
padding-top 10px
table
tr
th
min-width 80px
#g-user-otp-token
padding 16px 20px
margin-top 12px
.g-user-otp-footer
background-color #3F3B3B
border-radius 0 0 4px 4px
padding 15px 20px 18px
// clearfix
&:after
clear both
content ""
display table
.btn-default
margin 5px 0 0 5px
.btn-primary
float right
font-size 20px

// Responsive Goodness
@media screen and (max-width:1156px)
.g-account-otp-container
.g-account-otp-steps
.g-account-otp-step
&:nth-child(2)
div
&.g-account-otp-scan-qr
&:before
display none

@media screen and (max-width:960px)
.g-account-otp-container
.g-account-otp-steps
.g-account-otp-step
&:nth-child(2)
div
float none
width 100%
&.g-account-otp-scan-qr
margin-right 0
width 100%
10 changes: 10 additions & 0 deletions clients/web/src/templates/body/userAccount.pug
Expand Up @@ -15,6 +15,10 @@ ul.g-account-tabs.nav.nav-tabs
a(href="#g-account-tab-api-keys", data-toggle="tab", name="apikeys")
i.icon-key-inv
| API keys
li
a(href="#g-account-tab-otp", data-toggle="tab", name="otp")
i.icon-mobile
| Two-factor authentication

.tab-content
#g-account-tab-info.tab-pane.active
Expand Down Expand Up @@ -71,3 +75,9 @@ ul.g-account-tabs.nav.nav-tabs
the default expiration for user login sessions will be used.

.g-api-keys-list-container
#g-account-tab-otp.tab-pane
.g-user-settings-container
.g-account-otp-info-text.
Two-factor authentication adds another level of authentication to your account log-in.

.g-account-otp-container
4 changes: 4 additions & 0 deletions clients/web/src/templates/layout/loginDialog.pug
Expand Up @@ -12,6 +12,10 @@
.form-group
label.control-label(for="g-password") Password
input#g-password.input-sm.form-control(type="password", placeholder="Enter password")
#g-login-otp-group.form-group.hidden
label.control-label(for="g-login-otp") Two-factor code
input#g-login-otp.input-sm.form-control(
type='text', minlength=6, maxlength=6, placeholder="Your 6-digit two-factor authentication code, if enabled")
.g-validation-failed-message
.g-bottom-message
if registrationPolicy !== 'closed'
Expand Down
4 changes: 4 additions & 0 deletions clients/web/src/templates/widgets/userOtpBegin.pug
@@ -0,0 +1,4 @@
h2 Enable Two-factor authentication

button#g-user-otp-initialize.btn.btn-primary(type="submit")
| Begin
42 changes: 42 additions & 0 deletions clients/web/src/templates/widgets/userOtpConfirmation.pug
@@ -0,0 +1,42 @@
h2 Enable Two-factor authentication

ol.g-account-otp-steps
li.g-account-otp-step
h3 Install an authenticator app on your mobile device.
div.
There are many compatible apps, including, but not limited to
#[a(href='https://support.google.com/accounts/answer/1066447?hl=en', target='_blank', rel='noopener') Google Authenticator],
#[a(href='https://guide.duo.com/third-party-accounts', target='_blank', rel='noopener') Duo Mobile], and
#[a(href='https://freeotp.github.io/', target='_blank', rel='noopener') FreeOTP].
li.g-account-otp-step
h3 Enter your key into the authenticator app
.g-account-otp-scan-qr
h4 Scan with your mobile device's camera:
canvas#g-user-otp-qr
.g-account-otp-enter-manual
h4 Advanced Users:
span You may enter your information manually, instead:
table
tr
th Service
td
code= totpInfo.issuer
tr
th Account
td
code= totpInfo.account
tr
th Key
td
// Split into 4-character chunks for human readability
code= totpInfo.key.match(/.{1,4}/g).join('-')
tr
th Type
td
span #[code Time-based] / #[code TOTP]
li.g-account-otp-step
h3 Enter the authentication code from your app
input#g-user-otp-token(type='text', minlength=6, maxlength=6, placeholder='Your 6-digit code')
.g-user-otp-footer
a#g-user-otp-cancel.btn.btn-default Cancel
button#g-user-otp-finalize.btn.btn-primary.btn-success(type="submit") Enable Two-Factor
6 changes: 6 additions & 0 deletions clients/web/src/templates/widgets/userOtpDisable.pug
@@ -0,0 +1,6 @@
h2 Two-factor authentication is currently enabled

h3 Disable Two-factor authentication

button#g-user-otp-remove.btn.btn-primary(type="submit")
| Disable
10 changes: 10 additions & 0 deletions clients/web/src/views/body/UserAccountView.js
Expand Up @@ -2,6 +2,7 @@ import $ from 'jquery';
import _ from 'underscore';

import ApiKeyListWidget from 'girder/views/widgets/ApiKeyListWidget';
import UserOtpManagementWidget from 'girder/views/widgets/UserOtpManagementWidget';
import router from 'girder/router';
import UserModel from 'girder/models/UserModel';
import View from 'girder/views/View';
Expand Down Expand Up @@ -114,6 +115,11 @@ var UserAccountView = View.extend({
parentView: this
});

this.userOtpManagementWidget = new UserOtpManagementWidget({
user: this.user,
parentView: this
});

this.render();
},

Expand All @@ -139,6 +145,10 @@ var UserAccountView = View.extend({
if (this.tab === 'apikeys') {
this.apiKeyListWidget.setElement(
this.$('.g-api-keys-list-container')).render();
} else if (this.tab === 'otp') {
this.userOtpManagementWidget
.setElement(this.$('.g-account-otp-container'))
.render();
}
});

Expand Down
14 changes: 12 additions & 2 deletions clients/web/src/views/layout/LoginView.js
Expand Up @@ -25,11 +25,20 @@ var LoginView = View.extend({

const loginName = this.$('#g-login').val();
const password = this.$('#g-password').val();
login(loginName, password)
const otpToken = this.$('#g-login-otp-group').hasClass('hidden') ? null : this.$('#g-login-otp').val();
login(loginName, password, undefined, otpToken)
.done(() => {
this.$el.modal('hide');
})
.fail((err) => {
if (err.responseJSON.message.indexOf('Girder-OTP') !== -1 &&
this.$('#g-login-otp-group').hasClass('hidden')
) {
this.$('#g-login-otp-group').removeClass('hidden');
this.$('#g-login-otp').focus();
return;
}

this.$('.g-validation-failed-message').text(err.responseJSON.message);

if (err.responseJSON.extra === 'emailVerification') {
Expand Down Expand Up @@ -72,7 +81,8 @@ var LoginView = View.extend({
render: function () {
this.$el.html(LoginDialogTemplate({
registrationPolicy: this.registrationPolicy,
enablePasswordLogin: this.enablePasswordLogin
enablePasswordLogin: this.enablePasswordLogin,
showOtp: true
})).girderModal(this)
.on('shown.bs.modal', () => {
this.$('#g-login').focus();
Expand Down